diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 0000000000..8631e2b705 --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,2 @@ +titleAndCommits: true +allowMergeCommits: true \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26abdb2054..747d08ea48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -142,7 +142,7 @@ jobs: if: env.GIT_DIFF - name: test & coverage report creation run: | - cat pkgs.txt.part.${{ matrix.part }} | xargs go test -mod=readonly -timeout 30m -coverprofile=${{ matrix.part }}profile.out -covermode=atomic -tags='norace ledger test_ledger_mock' + cat pkgs.txt.part.${{ matrix.part }} | xargs go test -mod=readonly -timeout 30m -coverprofile=${{ matrix.part }}profile.out -covermode=atomic -tags='norace ledger test_ledger_mock goleveldb' if: env.GIT_DIFF - uses: actions/upload-artifact@v2 with: @@ -224,7 +224,7 @@ jobs: if: env.GIT_DIFF - name: test & coverage report creation run: | - xargs --arg-file=pkgs.txt.part.${{ matrix.part }} go test -mod=readonly -json -timeout 30m -race -tags='cgo ledger test_ledger_mock' | tee ${{ matrix.part }}-race-output.txt + xargs --arg-file=pkgs.txt.part.${{ matrix.part }} go test -mod=readonly -json -timeout 30m -race -tags='cgo ledger test_ledger_mock goleveldb' | tee ${{ matrix.part }}-race-output.txt if: env.GIT_DIFF - uses: actions/upload-artifact@v2 with: diff --git a/.gitignore b/.gitignore index 2bf1816598..ba30e1e40b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,16 +22,19 @@ dist tools-stamp buf-stamp artifacts +.vscode +output +tools/bin/* +examples/build/* # Data - ideally these don't exist baseapp/data/* -client/lcd/keys/* -mytestnet # Testing coverage.txt profile.out sim_log_file +client/keys/home/ # Vagrant .vagrant/ @@ -52,3 +55,4 @@ dependency-graph.png *.aux *.out *.synctex.gz + diff --git a/.golangci.yml b/.golangci.yml index 34738ccf7e..3dd6e69e19 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,24 +4,16 @@ run: # timeout: 5m linters: - disable-all: true + disable-all: false enable: - - bodyclose - - deadcode - - depguard - - dogsled - # - errcheck - - goconst - - gocritic - gofmt + - goconst - goimports - golint - gosec - gosimple - govet - ineffassign - - interfacer - - maligned - misspell - nakedret - prealloc @@ -34,7 +26,12 @@ linters: - unused - unparam - misspell - # - wsl + disable: + - gocritic + - maligned + - errcheck + - interfacer + - wsl - nolintlint issues: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46780a54e9..9da63ff5a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,414 +1,41 @@ -# Contributing +# How to contribute to LBM(Line Blockchain Mainnet) -- [Contributing](#contributing) - - [Architecture Decision Records (ADR)](#architecture-decision-records-adr) - - [Pull Requests](#pull-requests) - - [Process for reviewing PRs](#process-for-reviewing-prs) - - [Updating Documentation](#updating-documentation) - - [Forking](#forking) - - [Dependencies](#dependencies) - - [Protobuf](#protobuf) - - [Testing](#testing) - - [Branching Model and Release](#branching-model-and-release) - - [PR Targeting](#pr-targeting) - - [Development Procedure](#development-procedure) - - [Pull Merge Procedure](#pull-merge-procedure) - - [Release Procedure](#release-procedure) - - [Point Release Procedure](#point-release-procedure) - - [Code Owner Membership](#code-owner-membership) +First of all, thank you so much for taking your time to contribute! +It will be amazing if you could help us by doing any of the following: -Thank you for considering making contributions to Cosmos-SDK and related -repositories! +- File an issue in [the issue tracker](https://github.com/line/lbm/issues) to report bugs and propose new features and + improvements. +- Ask a question by creating a new issue in [the issue tracker](https://github.com/line/lbm/issues). + - Browse [the list of previously answered questions](https://github.com/line/lbm/issues?q=label%3Aquestion). +- Contribute your work by sending [a pull request](https://github.com/line/lbm/pulls). -Contributing to this repo can mean many things such as participated in -discussion or proposing code changes. To ensure a smooth workflow for all -contributors, the general procedure for contributing has been established: +## Contributor license agreement -1. Either [open](https://github.com/cosmos/cosmos-sdk/issues/new/choose) or - [find](https://github.com/cosmos/cosmos-sdk/issues) an issue you'd like to help with -2. Participate in thoughtful discussion on that issue -3. If you would like to contribute: - 1. If the issue is a proposal, ensure that the proposal has been accepted - 2. Ensure that nobody else has already begun working on this issue. If they have, - make sure to contact them to collaborate - 3. If nobody has been assigned for the issue and you would like to work on it, - make a comment on the issue to inform the community of your intentions - to begin work - 4. Follow standard Github best practices: fork the repo, branch from the - HEAD of `master`, make some commits, and submit a PR to `master` - - For core developers working within the cosmos-sdk repo, to ensure a clear - ownership of branches, branches must be named with the convention - `{moniker}/{issue#}-branch-name` - 5. Be sure to submit the PR in `Draft` mode submit your PR early, even if - it's incomplete as this indicates to the community you're working on - something and allows them to provide comments early in the development process - 6. When the code is complete it can be marked `Ready for Review` - 7. Be sure to include a relevant change log entry in the `Unreleased` section - of `CHANGELOG.md` (see file for log format) +When you are sending a pull request and it's a non-trivial change beyond fixing typos, please sign +the ICLA (individual contributor license agreement). Please +[contact us](mailto:dl_oss_dev@linecorp.com) if you need the CCLA (corporate contributor license agreement). -Note that for very small or blatantly obvious problems (such as typos) it is -not required to an open issue to submit a PR, but be aware that for more complex -problems/features, if a PR is opened before an adequate design discussion has -taken place in a github issue, that PR runs a high likelihood of being rejected. +## Code of conduct -Take a peek at our [coding repo](https://github.com/tendermint/coding) for -overall information on repository workflow and standards. Note, we use `make tools` for installing the linting tools. +We expect contributors to follow [our code of conduct](https://github.com/line/lbm/blob/v2/develop/CODE_OF_CONDUCT.md). -Other notes: +## Setting up your IDE -- Looking for a good place to start contributing? How about checking out some - [good first issues](https://github.com/cosmos/cosmos-sdk/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) -- Please make sure to run `make format` before every commit - the easiest way - to do this is have your editor run it for you upon saving a file. Additionally - please ensure that your code is lint compliant by running `golangci-lint run`. - A convenience git `pre-commit` hook that runs the formatters automatically - before each commit is available in the `contrib/githooks/` directory. +TBD -## Architecture Decision Records (ADR) +## Commit message and Pull Request message -When proposing an architecture decision for the SDK, please create an [ADR](./docs/architecture/README.md) -so further discussions can be made. We are following this process so all involved parties are in -agreement before any party begins coding the proposed implementation. If you would like to see some examples -of how these are written refer to [Tendermint ADRs](https://github.com/tendermint/tendermint/tree/master/docs/architecture) +- Follow [Conventional Commit](https://www.conventionalcommits.org) to release note automation. +- Don't mention or link that can't accessable from public. +- Use English only. Because this project will be published to the world-wide open-source world. But no worries. We are fully aware of that most of us are not the English-native. -## Pull Requests +## Pull Request Process -To accommodate review process we suggest that PRs are categorically broken up. -Ideally each PR addresses only a single issue. Additionally, as much as possible -code refactoring and cleanup should be submitted as a separate PRs from bugfixes/feature-additions. +1. Ensure any install or build dependencies are removed before the end of the layer when doing a + build. +2. Update the [README.md](README.md) with details of changes to the interface, this includes new environment + variables, exposed ports, useful file locations and container parameters. +3. Fill out all sections of the pull request template. That makes it easier to review your PR for the reviewers. +4. You may merge the pull request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. -### Process for reviewing PRs - -All PRs require two Reviews before merge (except docs changes, or variable name-changes which only require one). When reviewing PRs please use the following review explanations: - -- `LGTM` without an explicit approval means that the changes look good, but you haven't pulled down the code, run tests locally and thoroughly reviewed it. -- `Approval` through the GH UI means that you understand the code, documentation/spec is updated in the right places, you have pulled down and tested the code locally. In addition: - - You must also think through anything which ought to be included but is not - - You must think through whether any added code could be partially combined (DRYed) with existing code - - You must think through any potential security issues or incentive-compatibility flaws introduced by the changes - - Naming must be consistent with conventions and the rest of the codebase - - Code must live in a reasonable location, considering dependency structures (e.g. not importing testing modules in production code, or including example code modules in production code). - - if you approve of the PR, you are responsible for fixing any of the issues mentioned here and more -- If you sat down with the PR submitter and did a pairing review please note that in the `Approval`, or your PR comments. -- If you are only making "surface level" reviews, submit any notes as `Comments` without adding a review. - -### Updating Documentation - -If you open a PR on the Cosmos SDK, it is mandatory to update the relevant documentation in /docs. - -- If your change relates to the core SDK (baseapp, store, ...), please update the `docs/basics/`, `docs/core/` and/or `docs/building-modules/` folders. -- If your changes relate to the core of the CLI or Light-client (not specifically to module's CLI/Rest), please modify the `docs/interfaces/` folder. -- If your changes relate to a module, please update the module's spec in `x/moduleName/docs/spec/`. - -## Forking - -Please note that Go requires code to live under absolute paths, which complicates forking. -While my fork lives at `https://github.com/rigeyrigerige/cosmos-sdk`, -the code should never exist at `$GOPATH/src/github.com/rigeyrigerige/cosmos-sdk`. -Instead, we use `git remote` to add the fork as a new remote for the original repo, -`$GOPATH/src/github.com/cosmos/cosmos-sdk`, and do all the work there. - -For instance, to create a fork and work on a branch of it, I would: - -- Create the fork on github, using the fork button. -- Go to the original repo checked out locally (i.e. `$GOPATH/src/github.com/cosmos/cosmos-sdk`) -- `git remote rename origin upstream` -- `git remote add origin git@github.com:rigeyrigerige/cosmos-sdk.git` - -Now `origin` refers to my fork and `upstream` refers to the Cosmos-SDK version. -So I can `git push -u origin master` to update my fork, and make pull requests to Cosmos-SDK from there. -Of course, replace `rigeyrigerige` with your git handle. - -To pull in updates from the origin repo, run - -- `git fetch upstream` -- `git rebase upstream/master` (or whatever branch you want) - -Please don't make Pull Requests from `master`. - -## Dependencies - -We use [Go 1.14 Modules](https://github.com/golang/go/wiki/Modules) to manage -dependency versions. - -The master branch of every Cosmos repository should just build with `go get`, -which means they should be kept up-to-date with their dependencies, so we can -get away with telling people they can just `go get` our software. - -Since some dependencies are not under our control, a third party may break our -build, in which case we can fall back on `go mod tidy -v`. - -## Protobuf - -We use [Protocol Buffers](https://developers.google.com/protocol-buffers) along with [gogoproto](https://github.com/gogo/protobuf) to generate code for use in Cosmos-SDK. - -For determinstic behavior around Protobuf tooling, everything is containerized using Docker. Make sure to have Docker installed on your machine, or head to [Docker's website](https://docs.docker.com/get-docker/) to install it. - -For formatting code in `.proto` files, you can run `make proto-format` command. - -For linting and checking breaking changes, we use [buf](https://buf.build/). You can use the commands `make proto-lint` and `make proto-check-breaking` to respectively lint your proto files and check for breaking changes. - -To generate the protobuf stubs, you can run `make proto-gen`. - -We also added the `make proto-all` command to run all the above commands sequentially. - -In order for imports to properly compile in your IDE, you may need to manually set your protobuf path in your IDE's workspace settings/config. - -For example, in vscode your `.vscode/settings.json` should look like: - -``` -{ - "protoc": { - "options": [ - "--proto_path=${workspaceRoot}/proto", - "--proto_path=${workspaceRoot}/third_party/proto" - ] - } -} -``` - -## Testing - -All repos should be hooked up to [CircleCI](https://circleci.com/). - -If they have `.go` files in the root directory, they will be automatically -tested by circle using `go test -v -race ./...`. If not, they will need a -`circle.yml`. Ideally, every repo has a `Makefile` that defines `make test` and -includes its continuous integration status using a badge in the `README.md`. - -We expect tests to use `require` or `assert` rather than `t.Skip` or `t.Fail`, -unless there is a reason to do otherwise. -When testing a function under a variety of different inputs, we prefer to use -[table driven tests](https://github.com/golang/go/wiki/TableDrivenTests). -Table driven test error messages should follow the following format -`, tc #, i #`. -`` is an optional short description of whats failing, `tc` is the -index within the table of the testcase that is failing, and `i` is when there -is a loop, exactly which iteration of the loop failed. -The idea is you should be able to see the -error message and figure out exactly what failed. -Here is an example check: - -```go - -for tcIndex, tc := range cases { - - for i := 0; i < tc.numTxsToTest; i++ { - - require.Equal(t, expectedTx[:32], calculatedTx[:32], - "First 32 bytes of the txs differed. tc #%d, i #%d", tcIndex, i) -``` - -## Branching Model and Release - -User-facing repos should adhere to the trunk based development branching model: https://trunkbaseddevelopment.com/. - -Libraries need not follow the model strictly, but would be wise to. - -The SDK utilizes [semantic versioning](https://semver.org/). - -### PR Targeting - -Ensure that you base and target your PR on the `master` branch. - -All feature additions should be targeted against `master`. Bug fixes for an outstanding release candidate -should be targeted against the release candidate branch. Release candidate branches themselves should be the -only pull requests targeted directly against master. - -### Development Procedure - -- the latest state of development is on `master` -- `master` must never fail `make lint test test-race` -- `master` should not fail `make lint` -- no `--force` onto `master` (except when reverting a broken commit, which should seldom happen) -- create a development branch either on github.com/cosmos/cosmos-sdk, or your fork (using `git remote add origin`) -- before submitting a pull request, begin `git rebase` on top of `master` - -### Pull Merge Procedure - -- ensure pull branch is rebased on `master` -- run `make test` to ensure that all tests pass -- merge pull request - -### Release Procedure - -- Start on `master` -- Create the release candidate branch `rc/v*` (going forward known as **RC**) - and ensure it's protected against pushing from anyone except the release - manager/coordinator - - **no PRs targeting this branch should be merged unless exceptional circumstances arise** -- On the `RC` branch, prepare a new version section in the `CHANGELOG.md` - - All links must be link-ified: `$ python ./scripts/linkify_changelog.py CHANGELOG.md` - - Copy the entries into a `RELEASE_CHANGELOG.md`, this is needed so the bot knows which entries to add to the release page on github. -- Kick off a large round of simulation testing (e.g. 400 seeds for 2k blocks) -- If errors are found during the simulation testing, commit the fixes to `master` - and create a new `RC` branch (making sure to increment the `rcN`) -- After simulation has successfully completed, create the release branch - (`release/vX.XX.X`) from the `RC` branch -- Create a PR to `master` to incorporate the `CHANGELOG.md` updates -- Tag the release (use `git tag -a`) and create a release in Github -- Delete the `RC` branches - -### Point Release Procedure - -At the moment, only a single major release will be supported, so all point releases will be based -off of that release. - -In order to alleviate the burden for a single person to have to cherry-pick and handle merge conflicts -of all desired backporting PRs to a point release, we instead maintain a living backport branch, where -all desired features and bug fixes are merged into as separate PRs. - -Example: - -Current release is `v0.38.4`. We then maintain a (living) branch `sru/release/v0.38.N`, given N as -the next patch release number (currently `0.38.5`) for the `0.38` release series. As bugs are fixed -and PRs are merged into `master`, if a contributor wishes the PR to be released as SRU into the -`v0.38.N` point release, the contributor must: - -1. Add `0.38.N-backport` label -2. Pull latest changes on the desired `sru/release/vX.X.N` branch -3. Create a 2nd PR merging the respective SRU PR into `sru/release/v0.38.N` -4. Update the PR's description and ensure it contains the following information: - - **[Impact]** Explanation of how the bug affects users or developers. - - **[Test Case]** section with detailed instructions on how to reproduce the bug. - - **[Regression Potential]** section with a discussion how regressions are most likely to manifest, or might - manifest even if it's unlikely, as a result of the change. **It is assumed that any SRU candidate PR is - well-tested before it is merged in and has an overall low risk of regression**. - -It is the PR's author's responsibility to fix merge conflicts, update changelog entries, and -ensure CI passes. If a PR originates from an external contributor, it may be a core team member's -responsibility to perform this process instead of the original author. -Lastly, it is core team's responsibility to ensure that the PR meets all the SRU criteria. - -Finally, when a point release is ready to be made: - -1. Create `release/v0.38.N` branch -2. Ensure changelog entries are verified - 1. Be sure changelog entries are added to `RELEASE_CHANGELOG.md` -3. Add release version date to the changelog -4. Push release branch along with the annotated tag: **git tag -a** -5. Create a PR into `master` containing ONLY `CHANGELOG.md` updates - 1. Do not push `RELEASE_CHANGELOG.md` to `master` - -Note, although we aim to support only a single release at a time, the process stated above could be -used for multiple previous versions. - -## Code Owner Membership - -In the ethos of open source projects, and out of necessity to keep the code -alive, the core contributor team will strive to permit special repo privileges -to developers who show an aptitude towards developing with this code base. - -Several different kinds of privileges may be granted however most common -privileges to be granted are merge rights to either part of, or the entirety of the -code base (through the github `CODEOWNERS` file). The on-boarding process for -new code owners is as follows: On a bi-monthly basis (or more frequently if -agreeable) all the existing code owners will privately convene to discuss -potential new candidates as well as the potential for existing code-owners to -exit or "pass on the torch". This private meeting is to be a held as a -phone/video meeting. - -Subsequently after the meeting, and pending final approval from the ICF, -one of the existing code owners should open a PR modifying the `CODEOWNERS` file. -The other code owners should then all approve this PR to publicly display their support. - -Only if unanimous consensus is reached among all the existing code-owners will -an invitation be extended to a new potential-member. Likewise, when an existing -member is suggested to be removed/or have their privileges reduced, the member -in question must agree on the decision for their removal or else no action -should be taken. If however, a code-owner is demonstrably shown to intentionally -have had acted maliciously or grossly negligent, code-owner privileges may be -stripped with no prior warning or consent from the member in question. - -Other potential removal criteria: - * Missing 3 scheduled meetings results in ICF evaluating whether the member should be - removed / replaced - * Violation of Code of Conduct - -Earning this privilege should be considered to be no small feat and is by no -means guaranteed by any quantifiable metric. It is a symbol of great trust of -the community of this project. - - -## Concept & Release Approval Process - -The process for how Cosmos SDK maintainers take features and ADRs from concept to release -is broken up into three distinct stages: **Strategy Discovery**, **Concept Approval**, and -**Implementation & Release Approval** - - -### Strategy Discovery - -* Develop long term priorities, strategy and roadmap for the SDK -* Release committee not yet defined as there is already a roadmap that can be used for the time being - -### Concept Approval - -* Architecture Decision Records (ADRs) may be proposed by any contributors or maintainers of the Cosmos SDK, - and should follow the guidelines outlined in the - [ADR Creation Process](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/PROCESS.md) -* After proposal, a time bound period for Request for Comment (RFC) on ADRs commences -* ADRs are intended to be iterative, and may be merged into `master` while still in a `Proposed` status - -**Time Bound Period** - -* Once a PR for an ADR is opened, reviewers are expected to perform a first - review within 1 week of pull request being open -* Time bound period for individual ADR Pull Requests to be merged should not exceed 2 weeks -* Total time bound period for an ADR to reach a decision (`ABANDONED | ACCEPTED | REJECTED`) should not exceed 4 weeks - -If an individual Pull Request for an ADR needs more time than 2 weeks to reach resolution, it should be merged -in current state (`Draft` or `Proposed`), with its contents updated to summarize -the current state of its discussion. - -If an ADR is taking longer than 4 weeks to reach a final conclusion, the **Concept Approval Committee** -should convene to rectify the situation by either: -- unanimously setting a new time bound period for this ADR -- making changes to the Concept Approval Process (as outlined here) -- making changes to the members of the Concept Approval Committee - -**Approval Committee & Decision Making** - -In absense of general consensus, decision making requires ⅔ vote from the three members -of the **Concept Approval Committee**. - -**Committee Members** - -* Core Members: **Aaron** (Regen), **Bez** (Fission), **Alessio** (AiB) -* Secondary pool of candidates to replace / substitute: - * **Chris Goes** (IG), **Sunny** (Sikka) - -**Committee Criteria** - -Members must: - -* Participate in all or almost all ADR discussions, both on Github as well as in bi-weekly Architecture Review - meetings -* Be active contributors to the SDK, and furthermore should be continuously making substantial contributions - to the project's codebase, review process, documentation and ADRs -* Have stake in the Cosmos SDK project, represented by: - * Being a client / user of the Comsos SDK - * "[giving back](https://www.debian.org/social_contract)" to the software -* Delegate representation in case of vacation or absence - -Code owners need to maintain participation in the process, ideally as members of **Concept Approval Committee** -members, but at the very least as active participants in ADR discussions - -Removal criteria: - -* Missing 3 meetings results in ICF evaluating whether the member should be removed / replaced -* Violation of Code of Conduct - -### Implementation & Release Approval - -The following process should be adhered to both for implementation PRs corresponding to ADRs, as -well as for PRs made as part of a release process: - -* Code reviewers should ensure the PR does exactly what the ADR said it should -* Code reviewers should have more senior engineering capability -* ⅔ approval is required from the **primary repo maintainers** in `CODEOWNERS` - * Secondary pool of candidates to replace / substitute are listed as **secondary repo maintainers** in `CODEOWNERS` - -*Note: For any major or minor release series denoted as a "Stable Release" (e.g. v0.39 "Launchpad"), a separate release -committee is often established. Stable Releases, and their corresponding release committees are documented -separately in [STABLE_RELEASES.md](./STABLE_RELEASES.md)* diff --git a/Makefile b/Makefile index 76b47e860a..3d39d1a2c3 100644 --- a/Makefile +++ b/Makefile @@ -324,8 +324,12 @@ benchmark: ### Linting ### ############################################################################### -lint: +lint: golangci-lint golangci-lint run --out-format=tab + find . -name '*.go' -type f -not -path "*.git*" | xargs gofmt -d -s + +golangci-lint: + @go get github.com/golangci/golangci-lint/cmd/golangci-lint lint-fix: golangci-lint run --fix --out-format=tab --issues-exit-code=0 diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..a4cf81c4de --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ + + +## Description + + +## Motivation and context + + + +## How has this been tested? + + + + +## Screenshots (if appropriate): + +## Checklist: + + +- [ ] I followed the [contributing guidelines](https://github.com/line/link/blob/master/CONTRIBUTING.md). +- [ ] I have updated the documentation accordingly. +- [ ] I have added tests to cover my changes. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..e483ef2747 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,64 @@ +# +# This codecov.yml is the default configuration for +# all repositories on Codecov. You may adjust the settings +# below in your own codecov.yml in your repository. +# +codecov: + require_ci_to_pass: yes + bot: Codecov bot for LINK + +comment: + layout: "reach,diff,flags,tree" + behavior: default # update if exists else create new + require_changes: no + +coverage: + status: + project: + default: + # basic + target: 75 + threshold: 1% # allow this much decrease on project + base: auto + # advanced + branches: null + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false + flags: null + paths: null + changes: false + patch: off + precision: 2 + range: 50...90 + round: down + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +ignore: + - "docs" + - "*.md" + - "*.rst" + - "*.yml" + - "*.yaml" + - "*.sh" + - "*.png" + - "*_test.go" + - "x/**/test_common.go" + - "*_cmd.go" + - "contrib/**/*" + - "client/rpc/**/*_wrapper.go" + - "client/rpc/**/*_alias.go" + - "client/rpc/mock/*.go" + - "statik.go" + - "root.go" + - "x/**/module.go" + - "x/**/errors.go" + - "x/**/key.go" diff --git a/go.mod b/go.mod index 46ee491201..5d516bb534 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,14 @@ module github.com/line/lbm-sdk/v2 require ( github.com/99designs/keyring v1.1.6 + github.com/CosmWasm/wasmvm v0.12.0 + github.com/DataDog/zstd v1.4.5 // indirect github.com/armon/go-metrics v0.3.6 github.com/bgentry/speakeasy v0.1.0 github.com/btcsuite/btcd v0.21.0-beta github.com/btcsuite/btcutil v1.0.2 github.com/confio/ics23/go v0.6.3 + github.com/cosmos/cosmos-sdk v0.39.2 github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/ledger-cosmos-go v0.11.1 github.com/enigmampc/btcutil v1.0.3-0.20200723161021-e2fb6adb2a25 @@ -16,6 +19,7 @@ require ( github.com/gogo/protobuf v1.3.3 github.com/golang/mock v1.4.4 github.com/golang/protobuf v1.4.3 + github.com/google/gofuzz v1.0.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 @@ -37,13 +41,14 @@ require ( github.com/spf13/afero v1.3.4 // indirect github.com/spf13/cast v1.3.1 github.com/spf13/cobra v1.1.1 - github.com/spf13/jwalterweatherman v1.1.0 // indirect; indirects github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.0 github.com/tendermint/btcd v0.1.1 github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 github.com/tendermint/go-amino v0.16.0 + github.com/tendermint/tendermint v0.34.7 + github.com/tendermint/tm-db v0.6.4 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f google.golang.org/grpc v1.35.0 @@ -52,6 +57,10 @@ require ( ) replace ( + github.com/CosmWasm/wasmvm => github.com/line/wasmvm v0.12.0-0.1.0 + github.com/cosmos/cosmos-sdk => github.com/line/lbm-sdk v0.39.2-0.2.0 github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 + github.com/tendermint/tendermint => github.com/tendermint/tendermint v0.33.9 + github.com/tendermint/tm-db => github.com/tendermint/tm-db v0.5.2 google.golang.org/grpc => google.golang.org/grpc v1.33.2 ) diff --git a/go.sum b/go.sum index 1294d1a899..ec5e62d194 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1: github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -49,6 +51,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d h1:1aAija9gr0Hyv4KfQcRcwlmFIrhkDmIj2dz5bkg/s/8= +github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d/go.mod h1:icNx/6QdFblhsEjZehARqbNumymUT/ydwlLojFdv7Sk= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -85,7 +89,6 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/confio/ics23/go v0.0.0-20200817220745-f173e6211efb/go.mod h1:E45NqnlpxGnpfTWL/xauN7MRwEE28T4Dd4uraToOaKg= github.com/confio/ics23/go v0.6.3 h1:PuGK2V1NJWZ8sSkNDq91jgT/cahFEW9RGp4Y5jxulf0= github.com/confio/ics23/go v0.6.3/go.mod h1:E45NqnlpxGnpfTWL/xauN7MRwEE28T4Dd4uraToOaKg= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -101,8 +104,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= -github.com/cosmos/iavl v0.15.0-rc3.0.20201009144442-230e9bdf52cd/go.mod h1:3xOIaNNX19p0QrX0VqWa6voPRoJRGGYtny+DH8NEPvE= -github.com/cosmos/iavl v0.15.0-rc5/go.mod h1:WqoPL9yPTQ85QBMT45OOUzPxG/U/JcJoN7uMjgxke/I= +github.com/cosmos/iavl v0.15.3 h1:xE9r6HW8GeKeoYJN4zefpljZ1oukVScP/7M8oj6SUts= github.com/cosmos/iavl v0.15.3/go.mod h1:OLjQiAQ4fGD2KDZooyJG9yz+p2ao2IAYSbke8mVvSA4= github.com/cosmos/ledger-cosmos-go v0.11.1 h1:9JIYsGnXP613pb2vPjFeMMjBI5lEDsEaF6oYorTy6J4= github.com/cosmos/ledger-cosmos-go v0.11.1/go.mod h1:J8//BsAGTo3OC/vDLjMRFLW6q0WAaXvHnVc7ZmE8iUY= @@ -112,6 +114,7 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -119,7 +122,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/dgraph-io/badger/v2 v2.2007.1/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= github.com/dgraph-io/badger/v2 v2.2007.2 h1:EjjK0KqwaFMlPin1ajhP943VPENHJdEz1KLIegjaI3k= github.com/dgraph-io/badger/v2 v2.2007.2/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA= @@ -180,6 +182,7 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1-0.20190508161146-9fa652df1129/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= @@ -201,6 +204,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -224,10 +229,12 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -236,14 +243,12 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.1/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 h1:FlFbCRLd5Jr4iYXZufAvgWN6Ao0JrI5chLINnUXDDr0= github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.14.7/go.mod h1:oYZKL012gGh6LMyg/xA7Q2yq6j8bu0wa+9w14EEthWU= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= @@ -321,17 +326,23 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/line/iavl/v2 v2.0.0-init.1.0.20210325055816-6304f1fd2f09 h1:11L4wU4iYYRfOP20qo7909Q0gkd1NG8GOjSPu3Bblcg= github.com/line/iavl/v2 v2.0.0-init.1.0.20210325055816-6304f1fd2f09/go.mod h1:lOUXCMWnB9G5wDfungP5nhnHAFgVBwB0/iqFhH44OIs= +github.com/line/lbm-sdk v0.39.2-0.2.0 h1:tlQHZcf+AXejSBAhYgO5Tn1NaoCF0XRNf/6c0jeo3HE= +github.com/line/lbm-sdk v0.39.2-0.2.0/go.mod h1:UTxdYWx+OeRezEP8P5BxipddlFpq4q92uYydSeYN7B0= github.com/line/ostracon v0.34.9-0.20210315041958-2a1f43c788f5/go.mod h1:1THU+kF+6fxLaNYQKcdNyLCO6t9LnqSMaExDMiLozbM= github.com/line/ostracon v0.34.9-0.20210325081149-c7c246b1be58 h1:k+/hsrZ1RFIlL9+PRDzKPY+5YKFDWklReIgbeYmt+c4= github.com/line/ostracon v0.34.9-0.20210325081149-c7c246b1be58/go.mod h1:JQ5id/iSwal6/BAoN119QEzfTKm90DNPqB1mPiCR8Jk= github.com/line/tm-db/v2 v2.0.0-init.1.0.20210325025547-0ea105c02281 h1:HwehzGvsgPHND01825UbjjiKgwfbqIYFAnvn6OheLQs= github.com/line/tm-db/v2 v2.0.0-init.1.0.20210325025547-0ea105c02281/go.mod h1:TiTwPFffNAqep0nV0YWaxPjElbCp6yG4K8SCxy69mE4= +github.com/line/wasmvm v0.12.0-0.1.0 h1:Xul8w8pLWZDcp0kkz1Y9M6tfZ4WnmMt9g0U/d6lXdE4= +github.com/line/wasmvm v0.12.0-0.1.0/go.mod h1:tbXGE9Jz6sYpiJroGr71OQ5TFOufq/P5LWsruA2u6JE= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= @@ -347,6 +358,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -384,12 +396,14 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= @@ -415,6 +429,7 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= @@ -437,6 +452,7 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw= github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM= @@ -466,6 +482,7 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= @@ -487,7 +504,6 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10= github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa h1:0U2s5loxrTy6/VgfVoLuVLFJcURKLH49ie0zSch7gh4= github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -506,6 +522,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.3.4 h1:8q6vk3hthlpb2SouZcnBVKboxWQWMDNF38bwholZrJc= github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= @@ -525,6 +542,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= @@ -538,11 +556,13 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca h1:Ld/zXl5t4+D69SiV4JoN7kkfvJdOWlPpfxrzxpLMoUk= github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= @@ -551,15 +571,16 @@ github.com/tendermint/btcd v0.1.1 h1:0VcxPfflS2zZ3RiOAHkBiFUcPvbtRj5O7zHmcJWHV7s github.com/tendermint/btcd v0.1.1/go.mod h1:DC6/m53jtQzr/NFmMNEu0rxf18/ktVoVtMrnDD5pN+U= github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 h1:hqAk8riJvK4RMWx1aInLzndwxKalgi5rTqgfXxOxbEI= github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk= +github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= +github.com/tendermint/go-amino v0.15.1/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= -github.com/tendermint/tendermint v0.34.0-rc4/go.mod h1:yotsojf2C1QBOw4dZrTcxbyxmPUrT4hNuOQWX9XUwB4= -github.com/tendermint/tendermint v0.34.0-rc6/go.mod h1:ugzyZO5foutZImv0Iyx/gOFCX6mjJTgbLHTwi17VDVg= -github.com/tendermint/tendermint v0.34.0/go.mod h1:Aj3PIipBFSNO21r+Lq3TtzQ+uKESxkbA3yo/INM4QwQ= -github.com/tendermint/tendermint v0.34.7/go.mod h1:JVuu3V1ZexOaZG8VJMRl8lnfrGw6hEB2TVnoUwKRbss= -github.com/tendermint/tm-db v0.6.2/go.mod h1:GYtQ67SUvATOcoY8/+x6ylk8Qo02BQyLrAs+yAcLvGI= -github.com/tendermint/tm-db v0.6.3/go.mod h1:lfA1dL9/Y/Y8wwyPp2NMLyn5P5Ptr/gvDFNWtrCWSf8= -github.com/tendermint/tm-db v0.6.4/go.mod h1:dptYhIpJ2M5kUuenLr+Yyf3zQOv1SgBZcl8/BmWlMBw= +github.com/tendermint/iavl v0.14.3 h1:tuiUAqJdA3OOyPU/9P3pMYnAcd+OL7BUdzNiE3ytUwQ= +github.com/tendermint/iavl v0.14.3/go.mod h1:vHLYxU/zuxBmxxr1v+5Vnd/JzcIsyK17n9P9RDubPVU= +github.com/tendermint/tendermint v0.33.9 h1:rRKIfu5qAXX5f9bwX1oUXSZz/ALFJjDuivhkbGUQxiU= +github.com/tendermint/tendermint v0.33.9/go.mod h1:0yUs9eIuuDq07nQql9BmI30FtYGcEC60Tu5JzB5IezM= +github.com/tendermint/tm-db v0.5.2 h1:QG3IxQZBubWlr7kGQcYIavyTNmZRO+r//nENxoq0g34= +github.com/tendermint/tm-db v0.5.2/go.mod h1:VrPTx04QJhQ9d8TFUTc2GpPBvBf/U9vIdBIzkjBk7Lk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -575,6 +596,7 @@ github.com/zondax/hid v0.9.0 h1:eiT3P6vNxAEVxXMw66eZUAAnU2zD33JBkfG/EnfAKl8= github.com/zondax/hid v0.9.0/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= @@ -603,12 +625,12 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -653,7 +675,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -661,6 +682,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -712,6 +734,7 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= @@ -779,8 +802,6 @@ google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201111145450-ac7456db90a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201119123407-9b1e624d6bc4/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f h1:izedQ6yVIc5mZsRuXzmSreCOlzI0lCU1HpG8yEdMiKw= google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -800,11 +821,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/x/account/alias.go b/x/account/alias.go new file mode 100644 index 0000000000..8d21869b46 --- /dev/null +++ b/x/account/alias.go @@ -0,0 +1,19 @@ +package account + +import ( + "github.com/line/lbm-sdk/v2/x/account/client/cli" + "github.com/line/lbm-sdk/v2/x/account/internal/types" +) + +const ( + ModuleName = types.ModuleName + RouterKey = types.RouterKey +) + +var ( + CreateAccountTxCmd = cli.CreateAccountCmd + EmptyTxCmd = cli.EmptyCmd + NewMsgEmpty = types.NewMsgEmpty + RegisterCodec = types.RegisterCodec + ModuleCdc = types.ModuleCdc +) diff --git a/x/account/client/alias.go b/x/account/client/alias.go new file mode 100644 index 0000000000..3f5100c194 --- /dev/null +++ b/x/account/client/alias.go @@ -0,0 +1,19 @@ +package client + +import ( + cosmoscli "github.com/cosmos/cosmos-sdk/x/auth/client/cli" + "github.com/line/lbm-sdk/v2/x/account/client/cli" + "github.com/line/lbm-sdk/v2/x/account/client/rest" +) + +var ( + GetAccountCmd = cosmoscli.GetAccountCmd + QueryTxsByEventsCmd = cli.QueryTxsByEventsCmd + QueryTxCmd = cli.QueryTxCmd + QueryBlockWithTxResponsesCommand = cli.QueryBlockWithTxResponsesCommand + GetSignCommand = cosmoscli.GetSignCommand + GetMultiSignCommand = cosmoscli.GetMultiSignCommand + GetBroadcastCommand = cosmoscli.GetBroadcastCommand + GetEncodeCommand = cosmoscli.GetEncodeCommand + RegisterTxRoutes = rest.RegisterTxRoutes +) diff --git a/x/account/client/cli/query.go b/x/account/client/cli/query.go new file mode 100644 index 0000000000..e00e27532f --- /dev/null +++ b/x/account/client/cli/query.go @@ -0,0 +1,238 @@ +package cli + +import ( + "fmt" + "strconv" + "strings" + + "github.com/line/lbm-sdk/v2/x/account/client/utils" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/go-amino" + + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/rest" +) + +const ( + flagTags = "tags" + flagPage = "page" + flagLimit = "limit" + flagHeightFrom = "height-from" + flagHeightTo = "height-to" +) + +// ***** +// Original code: `github.com/cosmos/cosmos-sdk/x/auth/client/cli/query.go` +// Difference: referring import path of `utils` +// ***** + +// QueryTxsByEventsCmd returns a command to search through transactions by events. +func QueryTxsByEventsCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "txs", + Short: "Query for paginated transactions that match a set of tags", + Long: strings.TrimSpace(` +Search for transactions that match the exact given tags where results are paginated. + +Example: +$ query txs --tags 'message.action:send&message.sender:yoshi' --page 1 --limit 30 +$ query txs --tags 'message.action:send&message.sender:yoshi' --height-from 77 --height-to 79 + +You can also search by height range without tags: +$ query txs --height-from 77 --height-to 79 +`), + RunE: func(cmd *cobra.Command, args []string) error { + tagsStr := viper.GetString(flagTags) + tagsStr = strings.Trim(tagsStr, "'") + + var tags []string + if len(tagsStr) > 0 { + tags = strings.Split(tagsStr, "&") + } + + var tmTags []string + for _, tag := range tags { + if !strings.Contains(tag, ":") { + return fmt.Errorf("%s should be of the format :", tagsStr) + } else if strings.Count(tag, ":") > 1 { + return fmt.Errorf("%s should only contain one : pair", tagsStr) + } + + keyValue := strings.Split(tag, ":") + if keyValue[0] == tmtypes.TxHeightKey { + tag = fmt.Sprintf("%s=%s", keyValue[0], keyValue[1]) + } else { + tag = fmt.Sprintf("%s='%s'", keyValue[0], keyValue[1]) + } + + tmTags = append(tmTags, tag) + } + + heightFrom := viper.GetInt64(flagHeightFrom) + if heightFrom > 0 { + tag := fmt.Sprintf("%s>=%d", tmtypes.TxHeightKey, heightFrom) + tmTags = append(tmTags, tag) + } + + heightTo := viper.GetInt64(flagHeightTo) + if heightTo > 0 { + tag := fmt.Sprintf("%s<=%d", tmtypes.TxHeightKey, heightTo) + tmTags = append(tmTags, tag) + } + + page := viper.GetInt(flagPage) + limit := viper.GetInt(flagLimit) + + cliCtx := context.NewCLIContext().WithCodec(cdc) + txs, err := utils.QueryTxsByEvents(cliCtx, tmTags, page, limit) + if err != nil { + return err + } + + var output []byte + if cliCtx.Indent { + output, err = cdc.MarshalJSONIndent(txs, "", " ") + } else { + output, err = cdc.MarshalJSON(txs) + } + + if err != nil { + return err + } + + fmt.Println(string(output)) + return nil + }, + } + + cmd.Flags().StringP(flags.FlagNode, "n", "tcp://localhost:26657", "Node to connect to") + err := viper.BindPFlag(flags.FlagNode, cmd.Flags().Lookup(flags.FlagNode)) + if err != nil { + panic(err) + } + cmd.Flags().Bool(flags.FlagTrustNode, false, "Trust connected full node (don't verify proofs for responses)") + err = viper.BindPFlag(flags.FlagTrustNode, cmd.Flags().Lookup(flags.FlagTrustNode)) + if err != nil { + panic(err) + } + cmd.Flags().String(flagTags, "", "tag:value list of tags that must match") + cmd.Flags().Uint32(flagPage, rest.DefaultPage, "Query a specific page of paginated results") + cmd.Flags().Uint32(flagLimit, rest.DefaultLimit, "Query number of transactions results per page returned") + cmd.Flags().Int64(flagHeightFrom, 0, "Filter from a specific block height") + cmd.Flags().Int64(flagHeightTo, 0, "Filter to a specific block height") + + return cmd +} + +// QueryTxCmd implements the default command for a tx query. +func QueryTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "tx [hash]", + Short: "Query for a transaction by hash in a committed block", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + output, err := utils.QueryTx(cliCtx, args[0]) + if err != nil { + return err + } + + if output.Empty() { + return fmt.Errorf("no transaction found with hash %s", args[0]) + } + + return cliCtx.PrintOutput(output) + }, + } + + cmd.Flags().StringP(flags.FlagNode, "n", "tcp://localhost:26657", "Node to connect to") + err := viper.BindPFlag(flags.FlagNode, cmd.Flags().Lookup(flags.FlagNode)) + if err != nil { + panic(err) + } + cmd.Flags().Bool(flags.FlagTrustNode, false, "Trust connected full node (don't verify proofs for responses)") + err = viper.BindPFlag(flags.FlagTrustNode, cmd.Flags().Lookup(flags.FlagTrustNode)) + if err != nil { + panic(err) + } + return cmd +} + +var ( + DefaultBlockFetchSize int64 = 20 +) + +func QueryBlockWithTxResponsesCommand(cdc *amino.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "block-with-tx-result [from_block_height] [fetchsize]", + Short: "Get verified data for the block and tx and tx_result from given `from_block_height` to `fetchsize`.", + Long: "Up to 20 Items can be returned, and more are ignored. \n" + + "The Default fetchsize is 20 and if there are not enough blocks in the fetchsize requested from from_block_height, \n" + + "It will respond to the latest block height from from_block_height param. \n" + + "You can know latest block height by latest_block_height property of result. \n" + + "The direction of hasMore is from low to high blockHeight. \n" + + "Usage:\n" + + " linkcli query block-with-tx-result 1 10\n" + + " linkcli query block-with-tx-result 1 30 (it will return 20 Items)\n" + + " linkcli query block-with-tx-result 1", + + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + fromBlockHeight, fetchSize := parseCmdParams(args) + + latestBlockHeight, err := utils.LatestBlockHeight(cliCtx) + if err != nil { + return err + } + + if fromBlockHeight >= latestBlockHeight { + return fmt.Errorf("the block height does not exist. Requested: %d, Latest: %d", fromBlockHeight, latestBlockHeight) + } + + blockWithTxReponses, err := utils.BlockWithTxResponses(cliCtx, latestBlockHeight, fromBlockHeight, fetchSize) + if err != nil { + return err + } + return cliCtx.PrintOutput(blockWithTxReponses) + }, + } + + cmd.Flags().StringP(flags.FlagNode, "n", "tcp://localhost:26657", "Node to connect to") + err := viper.BindPFlag(flags.FlagNode, cmd.Flags().Lookup(flags.FlagNode)) + if err != nil { + panic(err) + } + + cmd.Flags().Bool(flags.FlagTrustNode, false, "Trust connected full node (don't verify proofs for responses)") + err = viper.BindPFlag(flags.FlagTrustNode, cmd.Flags().Lookup(flags.FlagTrustNode)) + if err != nil { + panic(err) + } + + return cmd +} + +func parseCmdParams(args []string) (int64, int64) { + fromBlockHeight, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + panic(err) + } + if len(args) == 1 { + return fromBlockHeight, DefaultBlockFetchSize + } + fetchSize, err := strconv.ParseInt(args[1], 10, 8) + if err != nil { + panic(err) + } + if fetchSize > DefaultBlockFetchSize { + fetchSize = DefaultBlockFetchSize + } + return fromBlockHeight, fetchSize +} diff --git a/x/account/client/cli/tx.go b/x/account/client/cli/tx.go new file mode 100644 index 0000000000..5281e00902 --- /dev/null +++ b/x/account/client/cli/tx.go @@ -0,0 +1,81 @@ +package cli + +import ( + "bufio" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/line/lbm-sdk/v2/x/account/client/utils" + "github.com/line/lbm-sdk/v2/x/account/internal/types" + "github.com/spf13/cobra" +) + +// GetTxCmd returns the transaction commands for this module +func GetTxCmd(cdc *codec.Codec) *cobra.Command { + txCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Account commands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + txCmd.AddCommand( + CreateAccountCmd(cdc), + EmptyCmd(cdc), + ) + return txCmd +} + +func CreateAccountCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "create-account [from_key_or_address] [target_address]", + Short: "Create an account having target_address", + Args: cobra.ExactArgs(2), + RunE: makeCreateAccountCmd(cdc), + } + + return flags.PostCommands(cmd)[0] +} + +func EmptyCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "empty [from_key_or_address]", + Short: "Do nothing", + Args: cobra.ExactArgs(1), + RunE: makeEmptyCmd(cdc), + } + + return flags.PostCommands(cmd)[0] +} + +func makeCreateAccountCmd(cdc *codec.Codec) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + target, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgCreateAccount(cliCtx.GetFromAddress(), target) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + } +} + +func makeEmptyCmd(cdc *codec.Codec) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + msg := types.NewMsgEmpty(cliCtx.GetFromAddress()) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + } +} diff --git a/x/account/client/rest/query.go b/x/account/client/rest/query.go new file mode 100644 index 0000000000..3363fb69c8 --- /dev/null +++ b/x/account/client/rest/query.go @@ -0,0 +1,141 @@ +package rest + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/gorilla/mux" + "github.com/line/lbm-sdk/v2/x/account/client/utils" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + genutilrest "github.com/cosmos/cosmos-sdk/x/genutil/client/rest" +) + +// ***** +// Original code: `github.com/cosmos/cosmos-sdk/x/auth/client/rest/query.go` +// Difference: referring import path of `utils` +// ***** + +// QueryTxsHandlerFn implements a REST handler that searches for transactions. +// Genesis transactions are returned if the height parameter is set to zero, +// otherwise the transactions are searched for by events. +func QueryTxsRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + rest.WriteErrorResponse( + w, http.StatusBadRequest, + fmt.Sprintf("failed to parse query parameters: %s", err), + ) + return + } + + // if the height query param is set to zero, query for genesis transactions + heightStr := r.FormValue("height") + if heightStr != "" { + if height, err := strconv.ParseInt(heightStr, 10, 64); err == nil && height == 0 { + genutilrest.QueryGenesisTxs(cliCtx, w) + return + } + } + + var ( + events []string + txs []sdk.TxResponse + page, limit int + ) + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + if len(r.Form) == 0 { + rest.PostProcessResponseBare(w, cliCtx, txs) + return + } + + events, page, limit, err = utils.ParseHTTPArgs(r) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + searchResult, err := utils.QueryTxsByEvents(cliCtx, events, page, limit) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + rest.PostProcessResponseBare(w, cliCtx, searchResult) + } +} + +// QueryTxRequestHandlerFn implements a REST handler that queries a transaction +// by hash in a committed block. +func QueryTxRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + hashHexStr := vars["hash"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + output, err := utils.QueryTx(cliCtx, hashHexStr) + if err != nil { + if strings.Contains(err.Error(), hashHexStr) { + rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) + return + } + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + if output.Empty() { + rest.WriteErrorResponse(w, http.StatusNotFound, fmt.Sprintf("no transaction found with hash %s", hashHexStr)) + } + + rest.PostProcessResponseBare(w, cliCtx, output) + } +} + +func QueryBlockWithTxsRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + fromBlockHeight, err := strconv.ParseInt(vars["from_height"], 10, 64) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("couldn't parse block height. Assumed format is '/blocks_with_tx_results/{from_height}'. because of %s", err.Error())) + return + } + fetchSize, err := strconv.ParseInt(r.URL.Query().Get("fetchsize"), 10, 8) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("couldn't parse fetchsize. because of %s", err.Error())) + return + } + + latestBlockHeight, err := utils.LatestBlockHeight(cliCtx) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("couldn't get latestBlockHeight. because of %s", err.Error())) + return + } + + if fromBlockHeight >= latestBlockHeight { + rest.WriteErrorResponse(w, http.StatusNotFound, fmt.Sprintf("the block height does not exist. Requested: %d, Latest: %d", fromBlockHeight, latestBlockHeight)) + return + } + + output, err := utils.BlockWithTxResponses(cliCtx, latestBlockHeight, fromBlockHeight, fetchSize) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("couldn't process request. because of %s", err.Error())) + return + } + rest.PostProcessResponseBare(w, cliCtx, output) + } +} diff --git a/x/account/client/rest/rest.go b/x/account/client/rest/rest.go new file mode 100644 index 0000000000..a05b18bf1b --- /dev/null +++ b/x/account/client/rest/rest.go @@ -0,0 +1,17 @@ +package rest + +import ( + "github.com/cosmos/cosmos-sdk/client/context" + cosmosrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" + "github.com/gorilla/mux" +) + +// RegisterTxRoutes registers all transaction routes on the provided router. +func RegisterTxRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc("/txs/{hash}", QueryTxRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/txs", QueryTxsRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/txs", cosmosrest.BroadcastTxRequest(cliCtx)).Methods("POST") + r.HandleFunc("/txs/encode", cosmosrest.EncodeTxRequestHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc("/txs/simulate", SimulateTxRequest(cliCtx)).Methods("POST") + r.HandleFunc("/blocks_with_tx_results/{from_height}", QueryBlockWithTxsRequestHandlerFn(cliCtx)).Methods("GET") +} diff --git a/x/account/client/rest/simulate.go b/x/account/client/rest/simulate.go new file mode 100644 index 0000000000..a356121080 --- /dev/null +++ b/x/account/client/rest/simulate.go @@ -0,0 +1,90 @@ +package rest + +import ( + "fmt" + "io/ioutil" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// SimulateReq defines a tx simulating request. +type SimulateReq struct { + Tx types.StdTx `json:"tx" yaml:"tx"` + GasAdjustment string `json:"gas_adjustment"` +} + +type ABCIErrorResponse struct { + Codespace string `json:"codespace"` + Code uint32 `json:"code"` + Error string `json:"error"` +} + +// SimulateTxRequest implements a tx simulating handler that is responsible +// for simulating a valid and signed tx. +func SimulateTxRequest(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req SimulateReq + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + err = cliCtx.Codec.UnmarshalJSON(body, &req) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + txBytes, err := cliCtx.Codec.MarshalBinaryLengthPrefixed(req.Tx) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + gasAdj, ok := rest.ParseFloat64OrReturnBadRequest(w, req.GasAdjustment, flags.DefaultGasAdjustment) + if !ok { + // ParseFloat64OrReturnBadRequest has already written error response. + return + } + + if gasAdj < 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("invalid gas adjustment: %g", gasAdj)) + return + } + + _, adjusted, err := utils.CalculateGas(cliCtx.QueryWithData, cliCtx.Codec, txBytes, gasAdj) + if err != nil { + if ctxtErr, ok := err.(*context.Error); ok { + WriteABCIErrorResponse(w, http.StatusInternalServerError, cliCtx.Codec, ctxtErr) + } else { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + } + return + } + + rest.WriteSimulationResponse(w, cliCtx.Codec, adjusted) + } +} + +// nolint: errcheck +func WriteABCIErrorResponse(w http.ResponseWriter, status int, cdc *codec.Codec, err *context.Error) { + errBody := cdc.MustMarshalJSON( + ABCIErrorResponse{ + Codespace: err.Codespace, + Code: err.Code, + Error: err.Message, + }, + ) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _, _ = w.Write(errBody) +} diff --git a/x/account/client/rest/simulate_test.go b/x/account/client/rest/simulate_test.go new file mode 100644 index 0000000000..c12b9b4b50 --- /dev/null +++ b/x/account/client/rest/simulate_test.go @@ -0,0 +1,142 @@ +package rest + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/types" + + "github.com/line/lbm-sdk/v2/x/account/client/utils/mock" +) + +type mockHTTPWriter struct { + statusCode int + bodyBuf bytes.Buffer +} + +func (m *mockHTTPWriter) Header() http.Header { + return http.Header{} +} + +func (m *mockHTTPWriter) Write(body []byte) (int, error) { + return m.bodyBuf.Write(body) +} + +func (m *mockHTTPWriter) WriteHeader(statusCode int) { + m.statusCode = statusCode +} + +var _ http.ResponseWriter = &mockHTTPWriter{} + +func setupCodec() *codec.Codec { + cdc := codec.New() + auth.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + sdk.RegisterCodec(cdc) + return cdc +} + +func TestSimulateTxRequest(t *testing.T) { + cdc := setupCodec() + + // assumes node response is + var gasUsed uint64 = 10000 + adjustment := 1.2 + + // set up mock node response + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + abciRes := &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: codec.Cdc.MustMarshalBinaryBare(sdk.SimulationResponse{ + GasInfo: sdk.GasInfo{ + GasUsed: gasUsed, + }, + }), + }, + } + mockClient.EXPECT().ABCIQueryWithOptions("/app/simulate", gomock.Any(), gomock.Any()).Return(abciRes, nil) + + // request + req := codec.MustMarshalJSONIndent(cdc, + &SimulateReq{ + Tx: types.StdTx{ + Memo: "empty tx", + }, + GasAdjustment: fmt.Sprintf("%f", adjustment), + }, + ) + request := http.Request{Body: ioutil.NopCloser(bytes.NewReader(req))} + + writer := mockHTTPWriter{} + SimulateTxRequest(cliCtx)(&writer, &request) + + res := rest.GasEstimateResponse{} + cdc.MustUnmarshalJSON(writer.bodyBuf.Bytes(), &res) + + require.Equal(t, uint64(float64(gasUsed)*adjustment), res.GasEstimate) +} + +func TestSimulateTxRequestWithABCIError(t *testing.T) { + cdc := setupCodec() + + var code uint32 = 10 + codespace := "codespace" + message := "error message" + adjustment := 1.2 + + // set up mock node response + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + abciRes := &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Code: code, + Codespace: codespace, + Log: message, + }, + } + mockClient.EXPECT().ABCIQueryWithOptions("/app/simulate", gomock.Any(), gomock.Any()).Return(abciRes, nil) + + // request + req := codec.MustMarshalJSONIndent(cdc, + &SimulateReq{ + Tx: types.StdTx{ + Memo: "empty tx", + }, + GasAdjustment: fmt.Sprintf("%f", adjustment), + }, + ) + request := http.Request{Body: ioutil.NopCloser(bytes.NewReader(req))} + + writer := mockHTTPWriter{} + SimulateTxRequest(cliCtx)(&writer, &request) + + res := ABCIErrorResponse{} + cdc.MustUnmarshalJSON(writer.bodyBuf.Bytes(), &res) + + require.Equal(t, code, res.Code) + require.Equal(t, codespace, res.Codespace) + require.Equal(t, message, res.Error) +} diff --git a/x/account/client/types/block_with_txs.go b/x/account/client/types/block_with_txs.go new file mode 100644 index 0000000000..3c9fdd0e45 --- /dev/null +++ b/x/account/client/types/block_with_txs.go @@ -0,0 +1,53 @@ +package types + +import ( + tmtypes "github.com/tendermint/tendermint/types" +) + +type FetchInfo struct { + InclusiveFromHeight int64 + ExclusiveToHeight int64 + HasMore bool + FetchItemCnt int64 + FetchItemRange []int64 +} + +func NewBlockFetchInfo(inclusiveFromHeight int64, exclusiveToHeight int64, hasMore bool) FetchInfo { + fetchItemCnt := exclusiveToHeight - inclusiveFromHeight + fetchItemRange := make([]int64, fetchItemCnt) + for i := range fetchItemRange { + fetchItemRange[i] = inclusiveFromHeight + int64(i) + } + return FetchInfo{inclusiveFromHeight, exclusiveToHeight, hasMore, + fetchItemCnt, fetchItemRange, + } +} + +func NewFetchInfo(latestBlockHeight, fromHeight, fetchSize int64) FetchInfo { + var fetchBlockHeight FetchInfo + switch exclusiveToBlockHeight := fromHeight + fetchSize; { + case latestBlockHeight > exclusiveToBlockHeight-1: + fetchBlockHeight = NewBlockFetchInfo(fromHeight, exclusiveToBlockHeight, true) + case latestBlockHeight == exclusiveToBlockHeight-1: + fetchBlockHeight = NewBlockFetchInfo(fromHeight, exclusiveToBlockHeight, false) + default: + fetchBlockHeight = NewBlockFetchInfo(fromHeight, latestBlockHeight+1, false) + } + return fetchBlockHeight +} + +type HasMoreResponseWrapper struct { + Items []*ResultBlockWithTxResponses `json:"items"` + HasMore bool `json:"has_more"` +} + +type ResultBlockWithTxResponses struct { + ResultBlock *ResultBlock `json:"result_block"` + TxResponses []TxResponse `json:"tx_responses"` +} + +type ResultBlock struct { + BlockSize int `json:"block_size"` + BlockID tmtypes.BlockID `json:"block_id"` + Block *tmtypes.Block `json:"block"` +} diff --git a/x/account/client/types/result.go b/x/account/client/types/result.go new file mode 100644 index 0000000000..786e043f2e --- /dev/null +++ b/x/account/client/types/result.go @@ -0,0 +1,145 @@ +package types + +import ( + "encoding/hex" + "fmt" + "math" + "strings" + + ctypes "github.com/tendermint/tendermint/rpc/core/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/exported" +) + +type TxResponse struct { + Height int64 `json:"height"` + TxHash string `json:"txhash"` + Codespace string `json:"codespace,omitempty"` + Code uint32 `json:"code,omitempty"` + Index uint32 `json:"index"` // additional field + Data string `json:"data,omitempty"` + RawLog string `json:"raw_log,omitempty"` + Logs sdk.ABCIMessageLogs `json:"logs,omitempty"` + Info string `json:"info,omitempty"` + GasWanted int64 `json:"gas_wanted,omitempty"` + GasUsed int64 `json:"gas_used,omitempty"` + Tx sdk.Tx `json:"tx,omitempty"` + Timestamp string `json:"timestamp,omitempty"` +} + +// NewResponseResultTx returns a TxResponse given a ResultTx from tendermint +func NewResponseResultTx(res *ctypes.ResultTx, tx sdk.Tx, timestamp string) TxResponse { + if res == nil { + return TxResponse{} + } + + parsedLogs, err := sdk.ParseABCILogs(res.TxResult.Log) + if err != nil { + parsedLogs = nil + } + + return TxResponse{ + TxHash: res.Hash.String(), + Height: res.Height, + Codespace: res.TxResult.Codespace, + Code: res.TxResult.Code, + Index: res.Index, + Data: strings.ToUpper(hex.EncodeToString(res.TxResult.Data)), + RawLog: res.TxResult.Log, + Logs: parsedLogs, + Info: res.TxResult.Info, + GasWanted: res.TxResult.GasWanted, + GasUsed: res.TxResult.GasUsed, + Tx: tx, + Timestamp: timestamp, + } +} + +func (r TxResponse) String() string { + var sb strings.Builder + sb.WriteString("Response:\n") + + if r.Height > 0 { + sb.WriteString(fmt.Sprintf(" Height: %d\n", r.Height)) + sb.WriteString(fmt.Sprintf(" Index: %d\n", r.Index)) + } + if r.TxHash != "" { + sb.WriteString(fmt.Sprintf(" TxHash: %s\n", r.TxHash)) + } + if r.Codespace != "" { + sb.WriteString(fmt.Sprintf(" Codespace: %s\n", r.Codespace)) + } + if r.Code > 0 { + sb.WriteString(fmt.Sprintf(" Code: %d\n", r.Code)) + } + if r.Data != "" { + sb.WriteString(fmt.Sprintf(" Data: %s\n", r.Data)) + } + if r.RawLog != "" { + sb.WriteString(fmt.Sprintf(" Raw Log: %s\n", r.RawLog)) + } + if r.Logs != nil { + sb.WriteString(fmt.Sprintf(" Logs: %s\n", r.Logs)) + } + if r.Info != "" { + sb.WriteString(fmt.Sprintf(" Info: %s\n", r.Info)) + } + if r.GasWanted != 0 { + sb.WriteString(fmt.Sprintf(" GasWanted: %d\n", r.GasWanted)) + } + if r.GasUsed != 0 { + sb.WriteString(fmt.Sprintf(" GasUsed: %d\n", r.GasUsed)) + } + if r.Timestamp != "" { + sb.WriteString(fmt.Sprintf(" Timestamp: %s\n", r.Timestamp)) + } + + return strings.TrimSpace(sb.String()) +} + +// Empty returns true if the response is empty +func (r TxResponse) Empty() bool { + return r.TxHash == "" && r.Logs == nil +} + +// SearchTxsResult defines a structure for querying txs pageable +type SearchTxsResult struct { + TotalCount int `json:"total_count"` // Count of all txs + Count int `json:"count"` // Count of txs in current page + PageNumber int `json:"page_number"` // Index of current page, start from 1 + PageTotal int `json:"page_total"` // Count of total pages + Limit int `json:"limit"` // Max count txs per page + Txs []TxResponse `json:"txs"` // List of txs in current page +} + +func NewSearchTxsResult(totalCount, count, page, limit int, txs []TxResponse) SearchTxsResult { + return SearchTxsResult{ + TotalCount: totalCount, + Count: count, + PageNumber: page, + PageTotal: int(math.Ceil(float64(totalCount) / float64(limit))), + Limit: limit, + Txs: txs, + } +} + +type SearchGenesisAccountResult struct { + TotalCount int `json:"total_count"` + Count int `json:"count"` + PageNumber int `json:"page_number"` + PageTotal int `json:"page_total"` + Limit int `json:"limit"` + Accounts exported.GenesisAccounts `json:"accounts"` +} + +func NewSearchGenesisAccountResult(totalCount, count, page, limit int, accounts exported.GenesisAccounts) SearchGenesisAccountResult { + return SearchGenesisAccountResult{ + TotalCount: totalCount, + Count: count, + PageNumber: page, + PageTotal: int(math.Ceil(float64(totalCount) / float64(limit))), + Limit: limit, + Accounts: accounts, + } +} diff --git a/x/account/client/utils/alias.go b/x/account/client/utils/alias.go new file mode 100644 index 0000000000..ed9be45fd1 --- /dev/null +++ b/x/account/client/utils/alias.go @@ -0,0 +1,11 @@ +package utils + +import ( + cosmosutils "github.com/cosmos/cosmos-sdk/x/auth/client/utils" +) + +var ( + GenerateOrBroadcastMsgs = cosmosutils.GenerateOrBroadcastMsgs + GetTxEncoder = cosmosutils.GetTxEncoder + WriteGenerateStdTxResponse = cosmosutils.WriteGenerateStdTxResponse +) diff --git a/x/account/client/utils/block_with_txs.go b/x/account/client/utils/block_with_txs.go new file mode 100644 index 0000000000..d1fa115261 --- /dev/null +++ b/x/account/client/utils/block_with_txs.go @@ -0,0 +1,103 @@ +package utils + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/line/lbm-sdk/v2/x/account/client/types" + + tmliteProxy "github.com/tendermint/tendermint/lite/proxy" + ctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +func LatestBlockHeight(cliCtx context.CLIContext) (int64, error) { + node, err := cliCtx.GetNode() + if err != nil { + return -1, err + } + + // Get the latest block + latestBlock, err := node.Block(nil) + if err != nil { + return -1, err + } + + return latestBlock.Block.Height, nil +} + +func BlockWithTxResponses(cliCtx context.CLIContext, latestBlockHeight, fromBlockHeight, fetchSize int64) (blockWithRxResultsWrapper *types.HasMoreResponseWrapper, err error) { + fbh := types.NewFetchInfo(latestBlockHeight, fromBlockHeight, fetchSize) + results := make([]*types.ResultBlockWithTxResponses, len(fbh.FetchItemRange)) + for idx, height := range fbh.FetchItemRange { + block, err := getBlock(cliCtx, height) + if err != nil { + return nil, fmt.Errorf("an error occurred while fetching a block by blockHeight(%d), err(%s)", height, err) + } + txs, err := getTxs(cliCtx, height) + if err != nil { + return nil, fmt.Errorf("an error occurred while fetching a block by blockHeight(%d), err(%s)", height, err) + } + results[idx] = &types.ResultBlockWithTxResponses{ + ResultBlock: &types.ResultBlock{ + BlockSize: block.Block.Size(), + BlockID: block.BlockID, + Block: block.Block, + }, + TxResponses: txs, + } + } + + return &types.HasMoreResponseWrapper{ + Items: results, + HasMore: fbh.HasMore, + }, nil +} + +func getBlock(cliCtx context.CLIContext, height int64) (*ctypes.ResultBlock, error) { + node, err := cliCtx.GetNode() + if err != nil { + return nil, err + } + resultBlock, err := node.Block(&height) + if err != nil { + return nil, err + } + + if !cliCtx.TrustNode { + check, err := cliCtx.Verify(resultBlock.Block.Height) + if err != nil { + return nil, err + } + + if err := tmliteProxy.ValidateHeader(&resultBlock.Block.Header, check); err != nil { + return nil, err + } + + if err = tmliteProxy.ValidateBlock(resultBlock.Block, check); err != nil { + return nil, err + } + } + return resultBlock, nil +} + +func getTxs(cliCtx context.CLIContext, height int64) ([]types.TxResponse, error) { + const defaultLimit = 100 + // nolint:prealloc + txResponses := []types.TxResponse{} + + nextTxPage := 1 + for { + searchResult, err := QueryTxsByEvents(cliCtx, []string{fmt.Sprintf("tx.height=%d", height)}, nextTxPage, defaultLimit) + if err != nil { + return nil, err + } + + txResponses = append(txResponses, searchResult.Txs...) + + nextTxPage++ + if nextTxPage > searchResult.PageTotal { + break + } + } + return txResponses, nil +} diff --git a/x/account/client/utils/block_with_txs_test.go b/x/account/client/utils/block_with_txs_test.go new file mode 100644 index 0000000000..0e3fb46462 --- /dev/null +++ b/x/account/client/utils/block_with_txs_test.go @@ -0,0 +1,262 @@ +package utils + +import ( + "encoding/binary" + "errors" + "fmt" + "math" + "math/rand" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/golang/mock/gomock" + "github.com/line/lbm-sdk/v2/x/account/client/types" + "github.com/line/lbm-sdk/v2/x/account/client/utils/mock" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/client/context" +) + +func TestLatestBlockHeight(t *testing.T) { + cdc := setupCodec() + height := int64(100) + + resBlock := &ctypes.ResultBlock{ + Block: &tmtypes.Block{ + Header: tmtypes.Header{ + Height: height, + Time: time.Time{}, + }, + }, + } + + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + mockClient.EXPECT().Block(nil).Return(resBlock, nil) + + ret, err := LatestBlockHeight(cliCtx) + require.NoError(t, err) + require.Equal(t, height, ret) +} + +func TestBlockWithTxResponses(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + cdc := setupCodec() + + height := int64(100) + + resBlock := &ctypes.ResultBlock{ + Block: &tmtypes.Block{ + Header: tmtypes.Header{ + Height: height, + Time: time.Time{}, + }, + }, + } + + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + mockClient.EXPECT().Block(&height).Return(resBlock, nil) + + resTxSearch := &ctypes.ResultTxSearch{ + Txs: nil, + TotalCount: 0, + } + + const defaultLimit = 100 + mockClient.EXPECT(). + TxSearch(fmt.Sprintf("tx.height=%d", height), !cliCtx.TrustNode, 1, defaultLimit, ""). + Return(resTxSearch, nil) + + actual, err := BlockWithTxResponses(cliCtx, height+1, height, int64(1)) + + results := make([]*types.ResultBlockWithTxResponses, 1) + + txResponses := make([]types.TxResponse, 0) + results[0] = &types.ResultBlockWithTxResponses{ + ResultBlock: &types.ResultBlock{ + BlockSize: resBlock.Block.Size(), + BlockID: resBlock.BlockID, + Block: resBlock.Block, + }, + TxResponses: txResponses, + } + expected := &types.HasMoreResponseWrapper{ + Items: results, + HasMore: true, + } + require.NoError(t, err) + require.Equal(t, expected, actual) +} + +func TestLatestBlockHeightWithError(t *testing.T) { + cdc := setupCodec() + + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + resError := errors.New("error") + mockClient.EXPECT().Block(nil).Return(nil, resError) + + ret, err := LatestBlockHeight(cliCtx) + require.Error(t, err) + require.Equal(t, int64(-1), ret) +} + +func makeTxs(t *testing.T, cdc *codec.Codec, count int, height int64) []*ctypes.ResultTx { + txs := make([]*ctypes.ResultTx, count) + + stdTx := &auth.StdTx{ + Memo: "empty tx", + } + + emptyTx, err := cdc.MarshalBinaryLengthPrefixed(stdTx) + require.NoError(t, err) + + for i := 0; i < count; i++ { + bs := make([]byte, 32) + binary.LittleEndian.PutUint32(bs, uint32(i)) + txs[i] = &ctypes.ResultTx{ + Hash: bs, + Height: height, + Index: uint32(i), + TxResult: abci.ResponseDeliverTx{ + Code: 0, + Codespace: "codespace", + Log: "[]", + }, + Tx: emptyTx, + } + } + return txs +} + +func TestGetTxs(t *testing.T) { + var parameterizedTests = []struct { + name string + txsCount int + }{ + {"multiple pages(txsCount=123)", 123}, + {"single page(txsCount=12)", 12}, + {"multiple pages fill(txsCount=300)", 300}, + {"single page fill(txsCount=100)", 100}, + {"empty(txsCount=0)", 0}, + } + + for _, pt := range parameterizedTests { + txsCount := pt.txsCount + t.Run(pt.name, func(t *testing.T) { + testGetTxsWithCount(t, txsCount) + }) + } +} + +func testGetTxsWithCount(t *testing.T, txsCount int) { + const defaultLimit = 100 + + cdc := setupCodec() + height := int64(100) + txs := makeTxs(t, cdc, txsCount, height) + + resBlock := &ctypes.ResultBlock{ + Block: &tmtypes.Block{ + Header: tmtypes.Header{ + Height: height, + Time: time.Time{}, + }, + }, + } + + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + + mockClient.EXPECT().Block(gomock.Any()).Return(resBlock, nil).AnyTimes() + + if txsCount == 0 { + resTxSearch := &ctypes.ResultTxSearch{ + Txs: nil, + TotalCount: txsCount, + } + mockClient.EXPECT(). + TxSearch(fmt.Sprintf("tx.height=%d", height), !cliCtx.TrustNode, 1, defaultLimit, ""). + Return(resTxSearch, nil) + } else { + for i := 0; i < int(math.Ceil(float64(txsCount)/defaultLimit)); i++ { + start := i * defaultLimit + end := start + defaultLimit + if end > txsCount { + // the last segment + end = txsCount + } + pageTxs := txs[start:end] + resTxSearch := &ctypes.ResultTxSearch{ + Txs: pageTxs, + TotalCount: txsCount, + } + mockClient.EXPECT(). + TxSearch(fmt.Sprintf("tx.height=%d", height), !cliCtx.TrustNode, i+1, defaultLimit, ""). + Return(resTxSearch, nil) + } + } + + ret, err := getTxs(cliCtx, height) + require.NoError(t, err) + require.Len(t, ret, txsCount) +} + +func TestEmptyTxs(t *testing.T) { + const defaultLimit = 100 + + cdc := setupCodec() + height := int64(100) + + resBlock := &ctypes.ResultBlock{ + Block: &tmtypes.Block{ + Header: tmtypes.Header{ + Height: height, + Time: time.Time{}, + }, + }, + } + + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + + mockClient.EXPECT().Block(gomock.Any()).Return(resBlock, nil).AnyTimes() + + resTxSearch := &ctypes.ResultTxSearch{ + Txs: nil, + TotalCount: 0, + } + mockClient.EXPECT(). + TxSearch(fmt.Sprintf("tx.height=%d", height), !cliCtx.TrustNode, 1, defaultLimit, ""). + Return(resTxSearch, nil) + + ret, err := getTxs(cliCtx, height) + require.NoError(t, err) + require.NotNil(t, ret) +} diff --git a/x/account/client/utils/mock/client_mock.go b/x/account/client/utils/mock/client_mock.go new file mode 100644 index 0000000000..6b7800935f --- /dev/null +++ b/x/account/client/utils/mock/client_mock.go @@ -0,0 +1,1146 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: /Users/kfangw/goworkspace/pkg/mod/github.com/tendermint/tendermint@v0.33.3/rpc/client/interface.go + +// Package mock_client is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + bytes "github.com/tendermint/tendermint/libs/bytes" + log "github.com/tendermint/tendermint/libs/log" + client "github.com/tendermint/tendermint/rpc/client" + types "github.com/tendermint/tendermint/rpc/core/types" + types0 "github.com/tendermint/tendermint/types" +) + +// MockClient is a mock of Client interface +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// Start mocks base method +func (m *MockClient) Start() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *MockClientMockRecorder) Start() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockClient)(nil).Start)) +} + +// OnStart mocks base method +func (m *MockClient) OnStart() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OnStart") + ret0, _ := ret[0].(error) + return ret0 +} + +// OnStart indicates an expected call of OnStart +func (mr *MockClientMockRecorder) OnStart() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnStart", reflect.TypeOf((*MockClient)(nil).OnStart)) +} + +// Stop mocks base method +func (m *MockClient) Stop() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stop") + ret0, _ := ret[0].(error) + return ret0 +} + +// Stop indicates an expected call of Stop +func (mr *MockClientMockRecorder) Stop() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockClient)(nil).Stop)) +} + +// OnStop mocks base method +func (m *MockClient) OnStop() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "OnStop") +} + +// OnStop indicates an expected call of OnStop +func (mr *MockClientMockRecorder) OnStop() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnStop", reflect.TypeOf((*MockClient)(nil).OnStop)) +} + +// Reset mocks base method +func (m *MockClient) Reset() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Reset") + ret0, _ := ret[0].(error) + return ret0 +} + +// Reset indicates an expected call of Reset +func (mr *MockClientMockRecorder) Reset() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockClient)(nil).Reset)) +} + +// OnReset mocks base method +func (m *MockClient) OnReset() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OnReset") + ret0, _ := ret[0].(error) + return ret0 +} + +// OnReset indicates an expected call of OnReset +func (mr *MockClientMockRecorder) OnReset() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OnReset", reflect.TypeOf((*MockClient)(nil).OnReset)) +} + +// IsRunning mocks base method +func (m *MockClient) IsRunning() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsRunning") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsRunning indicates an expected call of IsRunning +func (mr *MockClientMockRecorder) IsRunning() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRunning", reflect.TypeOf((*MockClient)(nil).IsRunning)) +} + +// Quit mocks base method +func (m *MockClient) Quit() <-chan struct{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Quit") + ret0, _ := ret[0].(<-chan struct{}) + return ret0 +} + +// Quit indicates an expected call of Quit +func (mr *MockClientMockRecorder) Quit() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Quit", reflect.TypeOf((*MockClient)(nil).Quit)) +} + +// String mocks base method +func (m *MockClient) String() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "String") + ret0, _ := ret[0].(string) + return ret0 +} + +// String indicates an expected call of String +func (mr *MockClientMockRecorder) String() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockClient)(nil).String)) +} + +// SetLogger mocks base method +func (m *MockClient) SetLogger(arg0 log.Logger) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLogger", arg0) +} + +// SetLogger indicates an expected call of SetLogger +func (mr *MockClientMockRecorder) SetLogger(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogger", reflect.TypeOf((*MockClient)(nil).SetLogger), arg0) +} + +// ABCIInfo mocks base method +func (m *MockClient) ABCIInfo() (*types.ResultABCIInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ABCIInfo") + ret0, _ := ret[0].(*types.ResultABCIInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ABCIInfo indicates an expected call of ABCIInfo +func (mr *MockClientMockRecorder) ABCIInfo() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ABCIInfo", reflect.TypeOf((*MockClient)(nil).ABCIInfo)) +} + +// ABCIQuery mocks base method +func (m *MockClient) ABCIQuery(path string, data bytes.HexBytes) (*types.ResultABCIQuery, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ABCIQuery", path, data) + ret0, _ := ret[0].(*types.ResultABCIQuery) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ABCIQuery indicates an expected call of ABCIQuery +func (mr *MockClientMockRecorder) ABCIQuery(path, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ABCIQuery", reflect.TypeOf((*MockClient)(nil).ABCIQuery), path, data) +} + +// ABCIQueryWithOptions mocks base method +func (m *MockClient) ABCIQueryWithOptions(path string, data bytes.HexBytes, opts client.ABCIQueryOptions) (*types.ResultABCIQuery, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ABCIQueryWithOptions", path, data, opts) + ret0, _ := ret[0].(*types.ResultABCIQuery) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ABCIQueryWithOptions indicates an expected call of ABCIQueryWithOptions +func (mr *MockClientMockRecorder) ABCIQueryWithOptions(path, data, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ABCIQueryWithOptions", reflect.TypeOf((*MockClient)(nil).ABCIQueryWithOptions), path, data, opts) +} + +// BroadcastTxCommit mocks base method +func (m *MockClient) BroadcastTxCommit(tx types0.Tx) (*types.ResultBroadcastTxCommit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastTxCommit", tx) + ret0, _ := ret[0].(*types.ResultBroadcastTxCommit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BroadcastTxCommit indicates an expected call of BroadcastTxCommit +func (mr *MockClientMockRecorder) BroadcastTxCommit(tx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastTxCommit", reflect.TypeOf((*MockClient)(nil).BroadcastTxCommit), tx) +} + +// BroadcastTxAsync mocks base method +func (m *MockClient) BroadcastTxAsync(tx types0.Tx) (*types.ResultBroadcastTx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastTxAsync", tx) + ret0, _ := ret[0].(*types.ResultBroadcastTx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BroadcastTxAsync indicates an expected call of BroadcastTxAsync +func (mr *MockClientMockRecorder) BroadcastTxAsync(tx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastTxAsync", reflect.TypeOf((*MockClient)(nil).BroadcastTxAsync), tx) +} + +// BroadcastTxSync mocks base method +func (m *MockClient) BroadcastTxSync(tx types0.Tx) (*types.ResultBroadcastTx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastTxSync", tx) + ret0, _ := ret[0].(*types.ResultBroadcastTx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BroadcastTxSync indicates an expected call of BroadcastTxSync +func (mr *MockClientMockRecorder) BroadcastTxSync(tx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastTxSync", reflect.TypeOf((*MockClient)(nil).BroadcastTxSync), tx) +} + +// Subscribe mocks base method +func (m *MockClient) Subscribe(ctx context.Context, subscriber, query string, outCapacity ...int) (<-chan types.ResultEvent, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, subscriber, query} + for _, a := range outCapacity { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Subscribe", varargs...) + ret0, _ := ret[0].(<-chan types.ResultEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Subscribe indicates an expected call of Subscribe +func (mr *MockClientMockRecorder) Subscribe(ctx, subscriber, query interface{}, outCapacity ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, subscriber, query}, outCapacity...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockClient)(nil).Subscribe), varargs...) +} + +// Unsubscribe mocks base method +func (m *MockClient) Unsubscribe(ctx context.Context, subscriber, query string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Unsubscribe", ctx, subscriber, query) + ret0, _ := ret[0].(error) + return ret0 +} + +// Unsubscribe indicates an expected call of Unsubscribe +func (mr *MockClientMockRecorder) Unsubscribe(ctx, subscriber, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsubscribe", reflect.TypeOf((*MockClient)(nil).Unsubscribe), ctx, subscriber, query) +} + +// UnsubscribeAll mocks base method +func (m *MockClient) UnsubscribeAll(ctx context.Context, subscriber string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnsubscribeAll", ctx, subscriber) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnsubscribeAll indicates an expected call of UnsubscribeAll +func (mr *MockClientMockRecorder) UnsubscribeAll(ctx, subscriber interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnsubscribeAll", reflect.TypeOf((*MockClient)(nil).UnsubscribeAll), ctx, subscriber) +} + +// Genesis mocks base method +func (m *MockClient) Genesis() (*types.ResultGenesis, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Genesis") + ret0, _ := ret[0].(*types.ResultGenesis) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Genesis indicates an expected call of Genesis +func (mr *MockClientMockRecorder) Genesis() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Genesis", reflect.TypeOf((*MockClient)(nil).Genesis)) +} + +// BlockchainInfo mocks base method +func (m *MockClient) BlockchainInfo(minHeight, maxHeight int64) (*types.ResultBlockchainInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockchainInfo", minHeight, maxHeight) + ret0, _ := ret[0].(*types.ResultBlockchainInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockchainInfo indicates an expected call of BlockchainInfo +func (mr *MockClientMockRecorder) BlockchainInfo(minHeight, maxHeight interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockchainInfo", reflect.TypeOf((*MockClient)(nil).BlockchainInfo), minHeight, maxHeight) +} + +// NetInfo mocks base method +func (m *MockClient) NetInfo() (*types.ResultNetInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NetInfo") + ret0, _ := ret[0].(*types.ResultNetInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NetInfo indicates an expected call of NetInfo +func (mr *MockClientMockRecorder) NetInfo() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetInfo", reflect.TypeOf((*MockClient)(nil).NetInfo)) +} + +// DumpConsensusState mocks base method +func (m *MockClient) DumpConsensusState() (*types.ResultDumpConsensusState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DumpConsensusState") + ret0, _ := ret[0].(*types.ResultDumpConsensusState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DumpConsensusState indicates an expected call of DumpConsensusState +func (mr *MockClientMockRecorder) DumpConsensusState() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DumpConsensusState", reflect.TypeOf((*MockClient)(nil).DumpConsensusState)) +} + +// ConsensusState mocks base method +func (m *MockClient) ConsensusState() (*types.ResultConsensusState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConsensusState") + ret0, _ := ret[0].(*types.ResultConsensusState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConsensusState indicates an expected call of ConsensusState +func (mr *MockClientMockRecorder) ConsensusState() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConsensusState", reflect.TypeOf((*MockClient)(nil).ConsensusState)) +} + +// ConsensusParams mocks base method +func (m *MockClient) ConsensusParams(height *int64) (*types.ResultConsensusParams, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConsensusParams", height) + ret0, _ := ret[0].(*types.ResultConsensusParams) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConsensusParams indicates an expected call of ConsensusParams +func (mr *MockClientMockRecorder) ConsensusParams(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConsensusParams", reflect.TypeOf((*MockClient)(nil).ConsensusParams), height) +} + +// Health mocks base method +func (m *MockClient) Health() (*types.ResultHealth, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Health") + ret0, _ := ret[0].(*types.ResultHealth) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Health indicates an expected call of Health +func (mr *MockClientMockRecorder) Health() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockClient)(nil).Health)) +} + +// Block mocks base method +func (m *MockClient) Block(height *int64) (*types.ResultBlock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Block", height) + ret0, _ := ret[0].(*types.ResultBlock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Block indicates an expected call of Block +func (mr *MockClientMockRecorder) Block(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Block", reflect.TypeOf((*MockClient)(nil).Block), height) +} + +// BlockResults mocks base method +func (m *MockClient) BlockResults(height *int64) (*types.ResultBlockResults, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockResults", height) + ret0, _ := ret[0].(*types.ResultBlockResults) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockResults indicates an expected call of BlockResults +func (mr *MockClientMockRecorder) BlockResults(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockResults", reflect.TypeOf((*MockClient)(nil).BlockResults), height) +} + +// Commit mocks base method +func (m *MockClient) Commit(height *int64) (*types.ResultCommit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", height) + ret0, _ := ret[0].(*types.ResultCommit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Commit indicates an expected call of Commit +func (mr *MockClientMockRecorder) Commit(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockClient)(nil).Commit), height) +} + +// Validators mocks base method +func (m *MockClient) Validators(height *int64, page, perPage int) (*types.ResultValidators, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validators", height, page, perPage) + ret0, _ := ret[0].(*types.ResultValidators) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Validators indicates an expected call of Validators +func (mr *MockClientMockRecorder) Validators(height, page, perPage interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validators", reflect.TypeOf((*MockClient)(nil).Validators), height, page, perPage) +} + +// Tx mocks base method +func (m *MockClient) Tx(hash []byte, prove bool) (*types.ResultTx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Tx", hash, prove) + ret0, _ := ret[0].(*types.ResultTx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Tx indicates an expected call of Tx +func (mr *MockClientMockRecorder) Tx(hash, prove interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tx", reflect.TypeOf((*MockClient)(nil).Tx), hash, prove) +} + +// TxSearch mocks base method +func (m *MockClient) TxSearch(query string, prove bool, page, perPage int, orderBy string) (*types.ResultTxSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxSearch", query, prove, page, perPage, orderBy) + ret0, _ := ret[0].(*types.ResultTxSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TxSearch indicates an expected call of TxSearch +func (mr *MockClientMockRecorder) TxSearch(query, prove, page, perPage, orderBy interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxSearch", reflect.TypeOf((*MockClient)(nil).TxSearch), query, prove, page, perPage, orderBy) +} + +// Status mocks base method +func (m *MockClient) Status() (*types.ResultStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(*types.ResultStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Status indicates an expected call of Status +func (mr *MockClientMockRecorder) Status() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockClient)(nil).Status)) +} + +// BroadcastEvidence mocks base method +func (m *MockClient) BroadcastEvidence(ev types0.Evidence) (*types.ResultBroadcastEvidence, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastEvidence", ev) + ret0, _ := ret[0].(*types.ResultBroadcastEvidence) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BroadcastEvidence indicates an expected call of BroadcastEvidence +func (mr *MockClientMockRecorder) BroadcastEvidence(ev interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastEvidence", reflect.TypeOf((*MockClient)(nil).BroadcastEvidence), ev) +} + +// UnconfirmedTxs mocks base method +func (m *MockClient) UnconfirmedTxs(limit int) (*types.ResultUnconfirmedTxs, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnconfirmedTxs", limit) + ret0, _ := ret[0].(*types.ResultUnconfirmedTxs) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UnconfirmedTxs indicates an expected call of UnconfirmedTxs +func (mr *MockClientMockRecorder) UnconfirmedTxs(limit interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnconfirmedTxs", reflect.TypeOf((*MockClient)(nil).UnconfirmedTxs), limit) +} + +// NumUnconfirmedTxs mocks base method +func (m *MockClient) NumUnconfirmedTxs() (*types.ResultUnconfirmedTxs, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NumUnconfirmedTxs") + ret0, _ := ret[0].(*types.ResultUnconfirmedTxs) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NumUnconfirmedTxs indicates an expected call of NumUnconfirmedTxs +func (mr *MockClientMockRecorder) NumUnconfirmedTxs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NumUnconfirmedTxs", reflect.TypeOf((*MockClient)(nil).NumUnconfirmedTxs)) +} + +// MockABCIClient is a mock of ABCIClient interface +type MockABCIClient struct { + ctrl *gomock.Controller + recorder *MockABCIClientMockRecorder +} + +// MockABCIClientMockRecorder is the mock recorder for MockABCIClient +type MockABCIClientMockRecorder struct { + mock *MockABCIClient +} + +// NewMockABCIClient creates a new mock instance +func NewMockABCIClient(ctrl *gomock.Controller) *MockABCIClient { + mock := &MockABCIClient{ctrl: ctrl} + mock.recorder = &MockABCIClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockABCIClient) EXPECT() *MockABCIClientMockRecorder { + return m.recorder +} + +// ABCIInfo mocks base method +func (m *MockABCIClient) ABCIInfo() (*types.ResultABCIInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ABCIInfo") + ret0, _ := ret[0].(*types.ResultABCIInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ABCIInfo indicates an expected call of ABCIInfo +func (mr *MockABCIClientMockRecorder) ABCIInfo() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ABCIInfo", reflect.TypeOf((*MockABCIClient)(nil).ABCIInfo)) +} + +// ABCIQuery mocks base method +func (m *MockABCIClient) ABCIQuery(path string, data bytes.HexBytes) (*types.ResultABCIQuery, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ABCIQuery", path, data) + ret0, _ := ret[0].(*types.ResultABCIQuery) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ABCIQuery indicates an expected call of ABCIQuery +func (mr *MockABCIClientMockRecorder) ABCIQuery(path, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ABCIQuery", reflect.TypeOf((*MockABCIClient)(nil).ABCIQuery), path, data) +} + +// ABCIQueryWithOptions mocks base method +func (m *MockABCIClient) ABCIQueryWithOptions(path string, data bytes.HexBytes, opts client.ABCIQueryOptions) (*types.ResultABCIQuery, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ABCIQueryWithOptions", path, data, opts) + ret0, _ := ret[0].(*types.ResultABCIQuery) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ABCIQueryWithOptions indicates an expected call of ABCIQueryWithOptions +func (mr *MockABCIClientMockRecorder) ABCIQueryWithOptions(path, data, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ABCIQueryWithOptions", reflect.TypeOf((*MockABCIClient)(nil).ABCIQueryWithOptions), path, data, opts) +} + +// BroadcastTxCommit mocks base method +func (m *MockABCIClient) BroadcastTxCommit(tx types0.Tx) (*types.ResultBroadcastTxCommit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastTxCommit", tx) + ret0, _ := ret[0].(*types.ResultBroadcastTxCommit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BroadcastTxCommit indicates an expected call of BroadcastTxCommit +func (mr *MockABCIClientMockRecorder) BroadcastTxCommit(tx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastTxCommit", reflect.TypeOf((*MockABCIClient)(nil).BroadcastTxCommit), tx) +} + +// BroadcastTxAsync mocks base method +func (m *MockABCIClient) BroadcastTxAsync(tx types0.Tx) (*types.ResultBroadcastTx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastTxAsync", tx) + ret0, _ := ret[0].(*types.ResultBroadcastTx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BroadcastTxAsync indicates an expected call of BroadcastTxAsync +func (mr *MockABCIClientMockRecorder) BroadcastTxAsync(tx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastTxAsync", reflect.TypeOf((*MockABCIClient)(nil).BroadcastTxAsync), tx) +} + +// BroadcastTxSync mocks base method +func (m *MockABCIClient) BroadcastTxSync(tx types0.Tx) (*types.ResultBroadcastTx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastTxSync", tx) + ret0, _ := ret[0].(*types.ResultBroadcastTx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BroadcastTxSync indicates an expected call of BroadcastTxSync +func (mr *MockABCIClientMockRecorder) BroadcastTxSync(tx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastTxSync", reflect.TypeOf((*MockABCIClient)(nil).BroadcastTxSync), tx) +} + +// MockSignClient is a mock of SignClient interface +type MockSignClient struct { + ctrl *gomock.Controller + recorder *MockSignClientMockRecorder +} + +// MockSignClientMockRecorder is the mock recorder for MockSignClient +type MockSignClientMockRecorder struct { + mock *MockSignClient +} + +// NewMockSignClient creates a new mock instance +func NewMockSignClient(ctrl *gomock.Controller) *MockSignClient { + mock := &MockSignClient{ctrl: ctrl} + mock.recorder = &MockSignClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockSignClient) EXPECT() *MockSignClientMockRecorder { + return m.recorder +} + +// Block mocks base method +func (m *MockSignClient) Block(height *int64) (*types.ResultBlock, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Block", height) + ret0, _ := ret[0].(*types.ResultBlock) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Block indicates an expected call of Block +func (mr *MockSignClientMockRecorder) Block(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Block", reflect.TypeOf((*MockSignClient)(nil).Block), height) +} + +// BlockResults mocks base method +func (m *MockSignClient) BlockResults(height *int64) (*types.ResultBlockResults, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockResults", height) + ret0, _ := ret[0].(*types.ResultBlockResults) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockResults indicates an expected call of BlockResults +func (mr *MockSignClientMockRecorder) BlockResults(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockResults", reflect.TypeOf((*MockSignClient)(nil).BlockResults), height) +} + +// Commit mocks base method +func (m *MockSignClient) Commit(height *int64) (*types.ResultCommit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", height) + ret0, _ := ret[0].(*types.ResultCommit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Commit indicates an expected call of Commit +func (mr *MockSignClientMockRecorder) Commit(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockSignClient)(nil).Commit), height) +} + +// Validators mocks base method +func (m *MockSignClient) Validators(height *int64, page, perPage int) (*types.ResultValidators, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validators", height, page, perPage) + ret0, _ := ret[0].(*types.ResultValidators) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Validators indicates an expected call of Validators +func (mr *MockSignClientMockRecorder) Validators(height, page, perPage interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validators", reflect.TypeOf((*MockSignClient)(nil).Validators), height, page, perPage) +} + +// Tx mocks base method +func (m *MockSignClient) Tx(hash []byte, prove bool) (*types.ResultTx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Tx", hash, prove) + ret0, _ := ret[0].(*types.ResultTx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Tx indicates an expected call of Tx +func (mr *MockSignClientMockRecorder) Tx(hash, prove interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tx", reflect.TypeOf((*MockSignClient)(nil).Tx), hash, prove) +} + +// TxSearch mocks base method +func (m *MockSignClient) TxSearch(query string, prove bool, page, perPage int, orderBy string) (*types.ResultTxSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TxSearch", query, prove, page, perPage, orderBy) + ret0, _ := ret[0].(*types.ResultTxSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TxSearch indicates an expected call of TxSearch +func (mr *MockSignClientMockRecorder) TxSearch(query, prove, page, perPage, orderBy interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxSearch", reflect.TypeOf((*MockSignClient)(nil).TxSearch), query, prove, page, perPage, orderBy) +} + +// MockHistoryClient is a mock of HistoryClient interface +type MockHistoryClient struct { + ctrl *gomock.Controller + recorder *MockHistoryClientMockRecorder +} + +// MockHistoryClientMockRecorder is the mock recorder for MockHistoryClient +type MockHistoryClientMockRecorder struct { + mock *MockHistoryClient +} + +// NewMockHistoryClient creates a new mock instance +func NewMockHistoryClient(ctrl *gomock.Controller) *MockHistoryClient { + mock := &MockHistoryClient{ctrl: ctrl} + mock.recorder = &MockHistoryClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockHistoryClient) EXPECT() *MockHistoryClientMockRecorder { + return m.recorder +} + +// Genesis mocks base method +func (m *MockHistoryClient) Genesis() (*types.ResultGenesis, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Genesis") + ret0, _ := ret[0].(*types.ResultGenesis) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Genesis indicates an expected call of Genesis +func (mr *MockHistoryClientMockRecorder) Genesis() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Genesis", reflect.TypeOf((*MockHistoryClient)(nil).Genesis)) +} + +// BlockchainInfo mocks base method +func (m *MockHistoryClient) BlockchainInfo(minHeight, maxHeight int64) (*types.ResultBlockchainInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BlockchainInfo", minHeight, maxHeight) + ret0, _ := ret[0].(*types.ResultBlockchainInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlockchainInfo indicates an expected call of BlockchainInfo +func (mr *MockHistoryClientMockRecorder) BlockchainInfo(minHeight, maxHeight interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlockchainInfo", reflect.TypeOf((*MockHistoryClient)(nil).BlockchainInfo), minHeight, maxHeight) +} + +// MockStatusClient is a mock of StatusClient interface +type MockStatusClient struct { + ctrl *gomock.Controller + recorder *MockStatusClientMockRecorder +} + +// MockStatusClientMockRecorder is the mock recorder for MockStatusClient +type MockStatusClientMockRecorder struct { + mock *MockStatusClient +} + +// NewMockStatusClient creates a new mock instance +func NewMockStatusClient(ctrl *gomock.Controller) *MockStatusClient { + mock := &MockStatusClient{ctrl: ctrl} + mock.recorder = &MockStatusClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockStatusClient) EXPECT() *MockStatusClientMockRecorder { + return m.recorder +} + +// Status mocks base method +func (m *MockStatusClient) Status() (*types.ResultStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(*types.ResultStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Status indicates an expected call of Status +func (mr *MockStatusClientMockRecorder) Status() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockStatusClient)(nil).Status)) +} + +// MockNetworkClient is a mock of NetworkClient interface +type MockNetworkClient struct { + ctrl *gomock.Controller + recorder *MockNetworkClientMockRecorder +} + +// MockNetworkClientMockRecorder is the mock recorder for MockNetworkClient +type MockNetworkClientMockRecorder struct { + mock *MockNetworkClient +} + +// NewMockNetworkClient creates a new mock instance +func NewMockNetworkClient(ctrl *gomock.Controller) *MockNetworkClient { + mock := &MockNetworkClient{ctrl: ctrl} + mock.recorder = &MockNetworkClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockNetworkClient) EXPECT() *MockNetworkClientMockRecorder { + return m.recorder +} + +// NetInfo mocks base method +func (m *MockNetworkClient) NetInfo() (*types.ResultNetInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NetInfo") + ret0, _ := ret[0].(*types.ResultNetInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NetInfo indicates an expected call of NetInfo +func (mr *MockNetworkClientMockRecorder) NetInfo() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetInfo", reflect.TypeOf((*MockNetworkClient)(nil).NetInfo)) +} + +// DumpConsensusState mocks base method +func (m *MockNetworkClient) DumpConsensusState() (*types.ResultDumpConsensusState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DumpConsensusState") + ret0, _ := ret[0].(*types.ResultDumpConsensusState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DumpConsensusState indicates an expected call of DumpConsensusState +func (mr *MockNetworkClientMockRecorder) DumpConsensusState() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DumpConsensusState", reflect.TypeOf((*MockNetworkClient)(nil).DumpConsensusState)) +} + +// ConsensusState mocks base method +func (m *MockNetworkClient) ConsensusState() (*types.ResultConsensusState, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConsensusState") + ret0, _ := ret[0].(*types.ResultConsensusState) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConsensusState indicates an expected call of ConsensusState +func (mr *MockNetworkClientMockRecorder) ConsensusState() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConsensusState", reflect.TypeOf((*MockNetworkClient)(nil).ConsensusState)) +} + +// ConsensusParams mocks base method +func (m *MockNetworkClient) ConsensusParams(height *int64) (*types.ResultConsensusParams, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConsensusParams", height) + ret0, _ := ret[0].(*types.ResultConsensusParams) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConsensusParams indicates an expected call of ConsensusParams +func (mr *MockNetworkClientMockRecorder) ConsensusParams(height interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConsensusParams", reflect.TypeOf((*MockNetworkClient)(nil).ConsensusParams), height) +} + +// Health mocks base method +func (m *MockNetworkClient) Health() (*types.ResultHealth, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Health") + ret0, _ := ret[0].(*types.ResultHealth) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Health indicates an expected call of Health +func (mr *MockNetworkClientMockRecorder) Health() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockNetworkClient)(nil).Health)) +} + +// MockEventsClient is a mock of EventsClient interface +type MockEventsClient struct { + ctrl *gomock.Controller + recorder *MockEventsClientMockRecorder +} + +// MockEventsClientMockRecorder is the mock recorder for MockEventsClient +type MockEventsClientMockRecorder struct { + mock *MockEventsClient +} + +// NewMockEventsClient creates a new mock instance +func NewMockEventsClient(ctrl *gomock.Controller) *MockEventsClient { + mock := &MockEventsClient{ctrl: ctrl} + mock.recorder = &MockEventsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockEventsClient) EXPECT() *MockEventsClientMockRecorder { + return m.recorder +} + +// Subscribe mocks base method +func (m *MockEventsClient) Subscribe(ctx context.Context, subscriber, query string, outCapacity ...int) (<-chan types.ResultEvent, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, subscriber, query} + for _, a := range outCapacity { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Subscribe", varargs...) + ret0, _ := ret[0].(<-chan types.ResultEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Subscribe indicates an expected call of Subscribe +func (mr *MockEventsClientMockRecorder) Subscribe(ctx, subscriber, query interface{}, outCapacity ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, subscriber, query}, outCapacity...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockEventsClient)(nil).Subscribe), varargs...) +} + +// Unsubscribe mocks base method +func (m *MockEventsClient) Unsubscribe(ctx context.Context, subscriber, query string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Unsubscribe", ctx, subscriber, query) + ret0, _ := ret[0].(error) + return ret0 +} + +// Unsubscribe indicates an expected call of Unsubscribe +func (mr *MockEventsClientMockRecorder) Unsubscribe(ctx, subscriber, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsubscribe", reflect.TypeOf((*MockEventsClient)(nil).Unsubscribe), ctx, subscriber, query) +} + +// UnsubscribeAll mocks base method +func (m *MockEventsClient) UnsubscribeAll(ctx context.Context, subscriber string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnsubscribeAll", ctx, subscriber) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnsubscribeAll indicates an expected call of UnsubscribeAll +func (mr *MockEventsClientMockRecorder) UnsubscribeAll(ctx, subscriber interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnsubscribeAll", reflect.TypeOf((*MockEventsClient)(nil).UnsubscribeAll), ctx, subscriber) +} + +// MockMempoolClient is a mock of MempoolClient interface +type MockMempoolClient struct { + ctrl *gomock.Controller + recorder *MockMempoolClientMockRecorder +} + +// MockMempoolClientMockRecorder is the mock recorder for MockMempoolClient +type MockMempoolClientMockRecorder struct { + mock *MockMempoolClient +} + +// NewMockMempoolClient creates a new mock instance +func NewMockMempoolClient(ctrl *gomock.Controller) *MockMempoolClient { + mock := &MockMempoolClient{ctrl: ctrl} + mock.recorder = &MockMempoolClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockMempoolClient) EXPECT() *MockMempoolClientMockRecorder { + return m.recorder +} + +// UnconfirmedTxs mocks base method +func (m *MockMempoolClient) UnconfirmedTxs(limit int) (*types.ResultUnconfirmedTxs, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnconfirmedTxs", limit) + ret0, _ := ret[0].(*types.ResultUnconfirmedTxs) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UnconfirmedTxs indicates an expected call of UnconfirmedTxs +func (mr *MockMempoolClientMockRecorder) UnconfirmedTxs(limit interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnconfirmedTxs", reflect.TypeOf((*MockMempoolClient)(nil).UnconfirmedTxs), limit) +} + +// NumUnconfirmedTxs mocks base method +func (m *MockMempoolClient) NumUnconfirmedTxs() (*types.ResultUnconfirmedTxs, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NumUnconfirmedTxs") + ret0, _ := ret[0].(*types.ResultUnconfirmedTxs) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NumUnconfirmedTxs indicates an expected call of NumUnconfirmedTxs +func (mr *MockMempoolClientMockRecorder) NumUnconfirmedTxs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NumUnconfirmedTxs", reflect.TypeOf((*MockMempoolClient)(nil).NumUnconfirmedTxs)) +} + +// MockEvidenceClient is a mock of EvidenceClient interface +type MockEvidenceClient struct { + ctrl *gomock.Controller + recorder *MockEvidenceClientMockRecorder +} + +// MockEvidenceClientMockRecorder is the mock recorder for MockEvidenceClient +type MockEvidenceClientMockRecorder struct { + mock *MockEvidenceClient +} + +// NewMockEvidenceClient creates a new mock instance +func NewMockEvidenceClient(ctrl *gomock.Controller) *MockEvidenceClient { + mock := &MockEvidenceClient{ctrl: ctrl} + mock.recorder = &MockEvidenceClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockEvidenceClient) EXPECT() *MockEvidenceClientMockRecorder { + return m.recorder +} + +// BroadcastEvidence mocks base method +func (m *MockEvidenceClient) BroadcastEvidence(ev types0.Evidence) (*types.ResultBroadcastEvidence, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BroadcastEvidence", ev) + ret0, _ := ret[0].(*types.ResultBroadcastEvidence) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BroadcastEvidence indicates an expected call of BroadcastEvidence +func (mr *MockEvidenceClientMockRecorder) BroadcastEvidence(ev interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastEvidence", reflect.TypeOf((*MockEvidenceClient)(nil).BroadcastEvidence), ev) +} diff --git a/x/account/client/utils/query.go b/x/account/client/utils/query.go new file mode 100644 index 0000000000..eb64cc6f34 --- /dev/null +++ b/x/account/client/utils/query.go @@ -0,0 +1,359 @@ +package utils + +import ( + "encoding/hex" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + ctypes "github.com/tendermint/tendermint/rpc/core/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + cutils "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/auth/types" + gtypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + + local "github.com/line/lbm-sdk/v2/x/account/client/types" +) + +const MaxPerPage = 100 + +// QueryTxsByEvents performs a search for transactions for a given set of events +// via the Tendermint RPC. An event takes the form of: +// "{eventAttribute}.{attributeKey} = '{attributeValue}'". Each event is +// concatenated with an 'AND' operand. It returns a slice of Info object +// containing txs and metadata. An error is returned if the query fails. +func QueryTxsByEvents(cliCtx context.CLIContext, events []string, page, limit int) (*local.SearchTxsResult, error) { + if len(events) == 0 { + return nil, errors.New("must declare at least one event to search") + } + + if page <= 0 { + return nil, errors.New("page must greater than 0") + } + + if limit <= 0 { + return nil, errors.New("limit must greater than 0") + } + + // XXX: implement ANY + query := strings.Join(events, " AND ") + + node, err := cliCtx.GetNode() + if err != nil { + return nil, err + } + + prove := !cliCtx.TrustNode + + resTxs, err := node.TxSearch(query, prove, page, limit, "") + if err != nil { + return nil, err + } + + if prove { + for _, tx := range resTxs.Txs { + err := cutils.ValidateTxResult(cliCtx, tx) + if err != nil { + return nil, err + } + } + } + + resBlocks, err := getBlocksForTxResults(cliCtx, resTxs.Txs) + if err != nil { + return nil, err + } + + txs, err := formatTxResults(cliCtx.Codec, resTxs.Txs, resBlocks) + if err != nil { + return nil, err + } + + result := local.NewSearchTxsResult(resTxs.TotalCount, len(txs), page, limit, txs) + + return &result, nil +} + +// QueryTx queries for a single transaction by a hash string in hex format. An +// error is returned if the transaction does not exist or cannot be queried. +func QueryTx(cliCtx context.CLIContext, hashHexStr string) (local.TxResponse, error) { + hash, err := hex.DecodeString(hashHexStr) + if err != nil { + return local.TxResponse{}, err + } + + node, err := cliCtx.GetNode() + if err != nil { + return local.TxResponse{}, err + } + + resTx, err := node.Tx(hash, !cliCtx.TrustNode) + if err != nil { + return local.TxResponse{}, err + } + + if !cliCtx.TrustNode { + if err = cutils.ValidateTxResult(cliCtx, resTx); err != nil { + return local.TxResponse{}, err + } + } + + resBlocks, err := getBlocksForTxResults(cliCtx, []*ctypes.ResultTx{resTx}) + if err != nil { + return local.TxResponse{}, err + } + + out, err := formatTxResult(cliCtx.Codec, resTx, resBlocks[resTx.Height]) + if err != nil { + return out, err + } + + return out, nil +} + +func QueryGenesisTx(cliCtx context.CLIContext) ([]sdk.Tx, error) { + node, err := cliCtx.GetNode() + if err != nil { + return []sdk.Tx{}, nil + } + + resultGenesis, err := node.Genesis() + if err != nil { + return []sdk.Tx{}, err + } + + appState, err := gtypes.GenesisStateFromGenDoc(cliCtx.Codec, *resultGenesis.Genesis) + if err != nil { + return []sdk.Tx{}, err + } + + genState := gtypes.GetGenesisStateFromAppState(cliCtx.Codec, appState) + genTxs := make([]sdk.Tx, len(genState.GenTxs)) + for i, tx := range genState.GenTxs { + err := cliCtx.Codec.UnmarshalJSON(tx, &genTxs[i]) + if err != nil { + return []sdk.Tx{}, err + } + } + return genTxs, nil +} + +func QueryGenesisAccount(cliCtx context.CLIContext, page, perPage int) (local.SearchGenesisAccountResult, error) { + node, err := cliCtx.GetNode() + if err != nil { + return local.SearchGenesisAccountResult{}, err + } + + resultGenesis, err := node.Genesis() + if err != nil { + return local.SearchGenesisAccountResult{}, err + } + + appState, err := gtypes.GenesisStateFromGenDoc(cliCtx.Codec, *resultGenesis.Genesis) + if err != nil { + return local.SearchGenesisAccountResult{}, err + } + + genAccounts := types.GetGenesisStateFromAppState(cliCtx.Codec, appState).Accounts + totalCount := len(genAccounts) + + perPage, err = validatePerPage(perPage) + if err != nil { + return local.SearchGenesisAccountResult{}, err + } + + page, err = validatePage(page, perPage, totalCount) + if err != nil { + return local.SearchGenesisAccountResult{}, err + } + start, end := getCountIndexRange(page, perPage, totalCount) + resultAccounts := genAccounts[start:end] + + return local.NewSearchGenesisAccountResult(totalCount, len(resultAccounts), page, perPage, resultAccounts), nil +} + +func validatePage(page, perPage, totalCount int) (int, error) { + if page < 1 { + return 1, fmt.Errorf("the page must greater than 0") + } + + pages := ((totalCount - 1) / perPage) + 1 + if pages == 0 { + pages = 1 + } + if page < 0 || page > pages { + return 1, fmt.Errorf("the page should be within [1, %d] range, given %d", pages, page) + } + + return page, nil +} + +func validatePerPage(perPage int) (int, error) { + if perPage < 1 { + return 1, fmt.Errorf("the limit must greater than 0") + } + + if perPage > MaxPerPage { + return MaxPerPage, nil + } + return perPage, nil +} + +func getCountIndexRange(page, perPage, totalCount int) (int, int) { + start := (page - 1) * perPage + end := start + perPage + if start < 0 { + return 0, end + } + if end > totalCount { + end = totalCount + } + + return start, end +} + +// formatTxResults parses the indexed txs into a slice of TxResponse objects. +func formatTxResults(cdc *codec.Codec, resTxs []*ctypes.ResultTx, resBlocks map[int64]*ctypes.ResultBlock) ([]local.TxResponse, error) { + var err error + out := make([]local.TxResponse, len(resTxs)) + for i := range resTxs { + out[i], err = formatTxResult(cdc, resTxs[i], resBlocks[resTxs[i].Height]) + if err != nil { + return nil, err + } + } + + return out, nil +} + +func getBlocksForTxResults(cliCtx context.CLIContext, resTxs []*ctypes.ResultTx) (map[int64]*ctypes.ResultBlock, error) { + node, err := cliCtx.GetNode() + if err != nil { + return nil, err + } + + resBlocks := make(map[int64]*ctypes.ResultBlock) + + for _, resTx := range resTxs { + if _, ok := resBlocks[resTx.Height]; !ok { + resBlock, err := node.Block(&resTx.Height) + if err != nil { + return nil, err + } + + resBlocks[resTx.Height] = resBlock + } + } + + return resBlocks, nil +} + +func formatTxResult(cdc *codec.Codec, resTx *ctypes.ResultTx, resBlock *ctypes.ResultBlock) (local.TxResponse, error) { + tx, err := parseTx(cdc, resTx.Tx) + if err != nil { + return local.TxResponse{}, err + } + + return local.NewResponseResultTx(resTx, tx, resBlock.Block.Time.Format(time.RFC3339)), nil +} + +func parseTx(cdc *codec.Codec, txBytes []byte) (sdk.Tx, error) { + var tx types.StdTx + + err := cdc.UnmarshalBinaryLengthPrefixed(txBytes, &tx) + if err != nil { + return nil, err + } + + return tx, nil +} + +// ParseHTTPArgs parses the request's URL and returns a slice containing all +// arguments pairs. It separates page and limit used for pagination. +func ParseHTTPArgs(r *http.Request) (tags []string, page, limit int, err error) { + tags = make([]string, 0, len(r.Form)) + for key, values := range r.Form { + if key == "page" || key == "limit" || key == "height.from" || key == "height.to" { + continue + } + + var value string + value, err = url.QueryUnescape(values[0]) + if err != nil { + return tags, page, limit, err + } + + var tag string + if key == tmtypes.TxHeightKey { + tag = fmt.Sprintf("%s=%s", key, value) + } else { + tag = fmt.Sprintf("%s='%s'", key, value) + } + tags = append(tags, tag) + } + + heightFromStr := r.FormValue("height.from") + if heightFromStr != "" { + heightFrom, err := strconv.ParseInt(heightFromStr, 10, 64) + switch { + case err != nil: + return tags, page, limit, err + case heightFrom <= 0: + return tags, page, limit, errors.New("height.from must greater than 0") + default: + tags = append(tags, fmt.Sprintf("%s>=%d", tmtypes.TxHeightKey, heightFrom)) + } + } + + heightToStr := r.FormValue("height.to") + if heightToStr != "" { + heightTo, err := strconv.ParseInt(heightToStr, 10, 64) + switch { + case err != nil: + return tags, page, limit, err + case heightTo <= 0: + return tags, page, limit, errors.New("height.to must greater than 0") + default: + tags = append(tags, fmt.Sprintf("%s<=%d", tmtypes.TxHeightKey, heightTo)) + } + } + + if len(tags) == 0 { + return tags, page, limit, errors.New("must declare at least one event to search") + } + + pageStr := r.FormValue("page") + if pageStr == "" { + page = rest.DefaultPage + } else { + page, err = strconv.Atoi(pageStr) + if err != nil { + return tags, page, limit, err + } else if page <= 0 { + return tags, page, limit, errors.New("page must greater than 0") + } + } + + limitStr := r.FormValue("limit") + if limitStr == "" { + limit = rest.DefaultLimit + } else { + limit, err = strconv.Atoi(limitStr) + if err != nil { + return tags, page, limit, err + } else if limit <= 0 { + return tags, page, limit, errors.New("limit must greater than 0") + } + } + + return tags, page, limit, nil +} diff --git a/x/account/client/utils/query_test.go b/x/account/client/utils/query_test.go new file mode 100644 index 0000000000..f544bf946b --- /dev/null +++ b/x/account/client/utils/query_test.go @@ -0,0 +1,407 @@ +package utils + +import ( + "encoding/hex" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/line/lbm-sdk/v2/x/account/client/utils/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth" + + atypes "github.com/line/lbm-sdk/v2/x/account/client/types" +) + +type mockNodeResponses struct { + resTx *ctypes.ResultTx + resBlock *ctypes.ResultBlock + resTxSearch *ctypes.ResultTxSearch +} + +// nolint:unparam +func setupMockNodeResponses( + t *testing.T, + cdc *codec.Codec, + hashString string, + height int64, + index uint32, + code uint32, + codespace string, +) mockNodeResponses { + hash, err := hex.DecodeString(hashString) + assert.NoError(t, err) + + stdTx := &auth.StdTx{ + Memo: "empty tx", + } + + bz, err := cdc.MarshalBinaryLengthPrefixed(stdTx) + assert.NoError(t, err) + resTx := &ctypes.ResultTx{ + Hash: hash, + Height: height, + Index: index, + TxResult: abci.ResponseDeliverTx{ + Code: code, + Codespace: codespace, + Log: "[]", + }, + Tx: bz, + } + resBlock := &ctypes.ResultBlock{ + Block: &tmtypes.Block{ + Header: tmtypes.Header{ + Height: height, + Time: time.Time{}, + }, + }, + } + resTxSearch := &ctypes.ResultTxSearch{ + Txs: []*ctypes.ResultTx{resTx}, + TotalCount: 1, + } + + return mockNodeResponses{ + resTx: resTx, + resBlock: resBlock, + resTxSearch: resTxSearch, + } +} + +func setupCodec() *codec.Codec { + cdc := codec.New() + auth.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + sdk.RegisterCodec(cdc) + return cdc +} + +func TestQueryTxsByEventsResponseContainsIndexCodeCodespace(t *testing.T) { + // nolint:goconst + hashString := "15E23C9F72602046D86BC9F0ECAE53E43A8206C113A29D94454476B9887AAB7F" + height := int64(100) + index := uint32(10) + code := uint32(0) + // nolint:goconst + codespace := "codespace" + + cdc := setupCodec() + nodeResponses := setupMockNodeResponses(t, cdc, hashString, height, index, code, codespace) + + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + mockClient.EXPECT().TxSearch("tx.height=0", !cliCtx.TrustNode, 1, 30, "").Return(nodeResponses.resTxSearch, nil) + + mockClient.EXPECT().Block(&nodeResponses.resTx.Height).Return(nodeResponses.resBlock, nil) + + res, err := QueryTxsByEvents(cliCtx, []string{"tx.height=0"}, 1, 30) + assert.NoError(t, err) + assert.Equal(t, 1, res.Count) + assertTxResponse(t, hashString, height, index, code, codespace, res.Txs[0]) +} + +func TestQueryTxResponseContainsIndexCodeCodespace(t *testing.T) { + hashString := "15E23C9F72602046D86BC9F0ECAE53E43A8206C113A29D94454476B9887AAB7F" + height := int64(100) + index := uint32(10) + code := uint32(0) + codespace := "codespace" + + cdc := setupCodec() + nodeResponses := setupMockNodeResponses(t, cdc, hashString, height, index, code, codespace) + + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + + hash, err := hex.DecodeString(hashString) + assert.NoError(t, err) + mockClient.EXPECT().Tx(hash, !cliCtx.TrustNode).Return(nodeResponses.resTx, nil) + mockClient.EXPECT().Block(&nodeResponses.resTx.Height).Return(nodeResponses.resBlock, nil) + + res, err := QueryTx(cliCtx, hashString) + assert.NoError(t, err) + assertTxResponse(t, hashString, height, index, code, codespace, res) +} + +func assertTxResponse( + t *testing.T, + hashString string, + height int64, + index, code uint32, + codespace string, + res atypes.TxResponse, +) { + assert.Equal(t, height, res.Height) + assert.Equal(t, index, res.Index) + assert.Equal(t, hashString, res.TxHash) + assert.Equal(t, code, res.Code) + assert.Equal(t, codespace, res.Codespace) +} + +// nolint:dupl +func TestQueryTxMarshalledResponseContainsIndexCodeCodespace(t *testing.T) { + hashString := "15E23C9F72602046D86BC9F0ECAE53E43A8206C113A29D94454476B9887AAB7F" + height := int64(100) + index := uint32(10) + code := uint32(1) + codespace := "codespace" + + cdc := setupCodec() + nodeResponses := setupMockNodeResponses(t, cdc, hashString, height, index, code, codespace) + + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + + hash, err := hex.DecodeString(hashString) + assert.NoError(t, err) + mockClient.EXPECT().Tx(hash, !cliCtx.TrustNode).Return(nodeResponses.resTx, nil) + mockClient.EXPECT().Block(&nodeResponses.resTx.Height).Return(nodeResponses.resBlock, nil) + + res, err := QueryTx(cliCtx, hashString) + assert.NoError(t, err) + + out, err := cdc.MarshalJSONIndent(res, "", " ") + assert.NoError(t, err) + + var m map[string]interface{} + err = json.Unmarshal(out, &m) + assert.NoError(t, err) + + assert.Contains(t, m, "index") + assert.Contains(t, m, "code") + assert.Contains(t, m, "codespace") +} + +// nolint:dupl +func TestQueryTxMarshalledResponseEmptyIndexCodeCodespace(t *testing.T) { + hashString := "15E23C9F72602046D86BC9F0ECAE53E43A8206C113A29D94454476B9887AAB7F" + height := int64(100) + index := uint32(0) + code := uint32(0) + codespace := "" + + cdc := setupCodec() + nodeResponses := setupMockNodeResponses(t, cdc, hashString, height, index, code, codespace) + + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + + hash, err := hex.DecodeString(hashString) + assert.NoError(t, err) + mockClient.EXPECT().Tx(hash, !cliCtx.TrustNode).Return(nodeResponses.resTx, nil) + mockClient.EXPECT().Block(&nodeResponses.resTx.Height).Return(nodeResponses.resBlock, nil) + + res, err := QueryTx(cliCtx, hashString) + assert.NoError(t, err) + + out, err := cdc.MarshalJSONIndent(res, "", " ") + assert.NoError(t, err) + + var m map[string]interface{} + err = json.Unmarshal(out, &m) + assert.NoError(t, err) + + assert.Contains(t, m, "index") + assert.NotContains(t, m, "code") + assert.NotContains(t, m, "codespace") +} + +func TestQueryGenesisTxs(t *testing.T) { + cdc := setupCodec() + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + + // exist genesis tx + genesisDoc := tmtypes.GenesisDoc{ + AppState: json.RawMessage(`{"genutil":{"gentxs":[{"type":"cosmos-sdk/StdTx","value":{"memo":"test_genesis"}}]}}`), + } + genesisResult := ctypes.ResultGenesis{ + Genesis: &genesisDoc, + } + mockClient.EXPECT().Genesis().Return(&genesisResult, nil) + + genesisTxs, err := QueryGenesisTx(cliCtx) + assert.NoError(t, err) + assert.NotEmpty(t, genesisTxs) + + // not exist genesis tx + genesisDoc = tmtypes.GenesisDoc{ + AppState: json.RawMessage(`{"genutil":{"gentxs":[]}`), + } + genesisResult = ctypes.ResultGenesis{ + Genesis: &genesisDoc, + } + mockClient.EXPECT().Genesis().Return(&genesisResult, nil) + genesisTxs, err = QueryGenesisTx(cliCtx) + assert.Empty(t, genesisTxs) + assert.Error(t, err) +} + +func TestQueryGenesisAccount(t *testing.T) { + testQueryGenesisAccount(t, "link") + testQueryGenesisAccount(t, "tlink") +} + +func testQueryGenesisAccount(t *testing.T, prefix string) { + config := sdk.GetConfig() + config.SetBech32PrefixForAccount(prefix, prefix+sdk.PrefixPublic) + + cdc := setupCodec() + mockClient := mock.NewMockClient(gomock.NewController(t)) + cliCtx := context.CLIContext{ + Client: mockClient, + TrustNode: true, + Codec: cdc, + } + + // exist genesis account + var addr string + if config.GetBech32AccountAddrPrefix() == "tlink" { + addr = "tlink19rqsvml8ldr0yrhaewgv9smcdvrew5panjryj3" + } else { + addr = "link19rqsvml8ldr0yrhaewgv9smcdvrew5pah9j5t5" + } + genesisDoc := tmtypes.GenesisDoc{ + AppState: json.RawMessage(`{"auth":{"accounts":[{"type":"cosmos-sdk/Account","value":{"address":"` + addr + `","coins":[]}}]}}`), + } + genesisResult := ctypes.ResultGenesis{ + Genesis: &genesisDoc, + } + mockClient.EXPECT().Genesis().Return(&genesisResult, nil).AnyTimes() + res, err := QueryGenesisAccount(cliCtx, 1, 2) + assert.NoError(t, err) + assert.Equal(t, 1, len(res.Accounts)) + + // no exist page + res, err = QueryGenesisAccount(cliCtx, 2, 1) + assert.Error(t, err) + assert.Equal(t, 0, len(res.Accounts)) + + // page=0 + res, err = QueryGenesisAccount(cliCtx, 0, 1) + assert.Error(t, err) + assert.Equal(t, 0, len(res.Accounts)) + + // page=-1 + res, err = QueryGenesisAccount(cliCtx, -1, 1) + assert.Error(t, err) + assert.Equal(t, 0, len(res.Accounts)) + + // limit=-1 + res, err = QueryGenesisAccount(cliCtx, 1, -1) + assert.Error(t, err) + assert.Equal(t, 0, len(res.Accounts)) +} + +func TestParseHTTPArgs(t *testing.T) { + reqE0 := mustNewGetRequest(t, "/") + + req0 := mustNewGetRequest(t, "/?foo=faa") + + req1 := mustNewGetRequest(t, "/?foo=faa&limit=5") + req2 := mustNewGetRequest(t, "/?foo=faa&page=5") + req3 := mustNewGetRequest(t, "/?foo=faa&page=5&limit=5") + + reqE1 := mustNewGetRequest(t, "/?foo=faa&page=-1") + reqE2 := mustNewGetRequest(t, "/?foo=faa&limit=-1") + reqE3 := mustNewGetRequest(t, "/?page=5&limit=5") + + req4 := mustNewGetRequest(t, "/?foo=faa&height.from=1") + req5 := mustNewGetRequest(t, "/?foo=faa&height.to=1") + + reqE4 := mustNewGetRequest(t, "/?foo=faa&height.from=-1") + reqE5 := mustNewGetRequest(t, "/?foo=faa&height.to=-1") + + defaultPage := rest.DefaultPage + defaultLimit := rest.DefaultLimit + + tests := []struct { + name string + req *http.Request + w http.ResponseWriter + tags []string + page int + limit int + err bool + }{ + {"no params", reqE0, httptest.NewRecorder(), []string{}, defaultPage, defaultLimit, true}, + + {"tags", req0, httptest.NewRecorder(), []string{"foo='faa'"}, defaultPage, defaultLimit, false}, + + {"limit", req1, httptest.NewRecorder(), []string{"foo='faa'"}, defaultPage, 5, false}, + {"page", req2, httptest.NewRecorder(), []string{"foo='faa'"}, 5, defaultLimit, false}, + {"page and limit", req3, httptest.NewRecorder(), []string{"foo='faa'"}, 5, 5, false}, + + {"error page 0", reqE1, httptest.NewRecorder(), []string{}, defaultPage, defaultLimit, true}, + {"error limit 0", reqE2, httptest.NewRecorder(), []string{}, defaultPage, defaultLimit, true}, + {"no tags", reqE3, httptest.NewRecorder(), []string{}, defaultPage, defaultLimit, true}, + + {"height from", req4, httptest.NewRecorder(), []string{"foo='faa'", "tx.height>=1"}, defaultPage, defaultLimit, false}, + {"height to", req5, httptest.NewRecorder(), []string{"foo='faa'", "tx.height<=1"}, defaultPage, defaultLimit, false}, + + {"error height from", reqE4, httptest.NewRecorder(), []string{}, defaultPage, defaultLimit, true}, + {"error height to", reqE5, httptest.NewRecorder(), []string{}, defaultPage, defaultLimit, true}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + tags, page, limit, err := ParseHTTPArgs(tt.req) + if tt.err { + require.NotNil(t, err) + } else { + require.Nil(t, err) + require.Equal(t, tt.tags, tags) + require.Equal(t, tt.page, page) + require.Equal(t, tt.limit, limit) + } + }) + } +} + +func mustNewRequest(t *testing.T, method, url string, body io.Reader) *http.Request { + req, err := http.NewRequest(method, url, body) + require.NoError(t, err) + err = req.ParseForm() + require.NoError(t, err) + return req +} + +func mustNewGetRequest(t *testing.T, url string) *http.Request { + return mustNewRequest(t, "GET", url, nil) +} diff --git a/x/account/handler.go b/x/account/handler.go new file mode 100644 index 0000000000..86be7b3458 --- /dev/null +++ b/x/account/handler.go @@ -0,0 +1,61 @@ +package account + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/line/lbm-sdk/v2/x/account/internal/types" +) + +// NewHandler returns a handler for "account" type messages. +func NewHandler(k auth.AccountKeeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + + switch msg := msg.(type) { + case types.MsgCreateAccount: + return handleMsgCreateAccount(ctx, k, msg) + case types.MsgEmpty: + return handleMsgEmpty(ctx, msg) + default: + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized account message type: %T", msg) + } + } +} + +func handleMsgCreateAccount(ctx sdk.Context, keeper auth.AccountKeeper, msg types.MsgCreateAccount) (*sdk.Result, error) { + if keeper.GetAccount(ctx, msg.Target) != nil { + return nil, types.ErrAccountAlreadyExist + } + + acc := keeper.NewAccountWithAddress(ctx, msg.Target) + + keeper.SetAccount(ctx, acc) + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventCreateAccount, + sdk.NewAttribute(types.AttributeKeyCreateAccountFrom, msg.From.String()), + sdk.NewAttribute(types.AttributeKeyCreateAccountTarget, msg.Target.String()), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + sdk.NewAttribute(sdk.AttributeKeyAction, types.EventCreateAccount), + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgEmpty(ctx sdk.Context, msg types.MsgEmpty) (*sdk.Result, error) { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeyAction, types.EventEmpty), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/account/handler_test.go b/x/account/handler_test.go new file mode 100644 index 0000000000..0554e50133 --- /dev/null +++ b/x/account/handler_test.go @@ -0,0 +1,105 @@ +package account + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + "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/x/auth" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/line/lbm-sdk/v2/x/account/internal/types" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/secp256k1" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" +) + +var ( + priv1 = secp256k1.GenPrivKey() + addr1 = sdk.AccAddress(priv1.PubKey().Address()) + priv2 = secp256k1.GenPrivKey() + addr2 = sdk.AccAddress(priv2.PubKey().Address()) +) + +type TestInput struct { + Cdc *codec.Codec + Ctx sdk.Context + Ak auth.AccountKeeper +} + +func newTestCodec() *codec.Codec { + cdc := codec.New() + auth.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + return cdc +} + +func setupTestInput(t *testing.T) TestInput { + keyAuth := sdk.NewKVStoreKey(auth.StoreKey) + keyParams := sdk.NewKVStoreKey(params.StoreKey) + tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) + + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyAuth, sdk.StoreTypeIAVL, db) + err := ms.LoadLatestVersion() + require.NoError(t, err) + + cdc := newTestCodec() + + // init params keeper and subspaces + paramsKeeper := params.NewKeeper(cdc, keyParams, tkeyParams) + authSubspace := paramsKeeper.Subspace(auth.DefaultParamspace) + + ctx := sdk.NewContext(ms, abci.Header{ChainID: "test-chain-id"}, false, log.NewNopLogger()) + + // add keepers + accountKeeper := auth.NewAccountKeeper(cdc, keyAuth, authSubspace, auth.ProtoBaseAccount) + accountKeeper.NewAccountWithAddress(ctx, addr1) + + return TestInput{Cdc: cdc, Ctx: ctx, Ak: accountKeeper} +} + +func TestHandlerCreateAccount(t *testing.T) { + input := setupTestInput(t) + ctx, keeper := input.Ctx, input.Ak + + h := NewHandler(keeper) + + // creating the account addr2 succeeds at first + { + msg := types.NewMsgCreateAccount(addr1, addr2) + _, err := h(ctx, msg) + require.NoError(t, err) + } + + // creating the account addr2 twice fails + { + msg := types.NewMsgCreateAccount(addr1, addr2) + _, err := h(ctx, msg) + require.Error(t, err) + } +} + +func TestHandlerEmpty(t *testing.T) { + input := setupTestInput(t) + ctx, keeper := input.Ctx, input.Ak + + h := NewHandler(keeper) + + // message test + { + msg := types.MsgEmpty{From: addr1} + _, err := h(ctx, msg) + require.NoError(t, err) + } + + // invalid message + { + msg := types.MsgEmpty{From: nil} + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing signer address").Error()) + } +} diff --git a/x/account/internal/types/codec.go b/x/account/internal/types/codec.go new file mode 100644 index 0000000000..0ba95936d6 --- /dev/null +++ b/x/account/internal/types/codec.go @@ -0,0 +1,19 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +var ModuleCdc *codec.Codec + +func init() { + ModuleCdc = codec.New() + RegisterCodec(ModuleCdc) + ModuleCdc.Seal() +} + +// Register concrete types on codec +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(MsgCreateAccount{}, "account/MsgCreateAccount", nil) + cdc.RegisterConcrete(MsgEmpty{}, "account/MsgEmpty", nil) +} diff --git a/x/account/internal/types/errors.go b/x/account/internal/types/errors.go new file mode 100644 index 0000000000..d77d5d8ec7 --- /dev/null +++ b/x/account/internal/types/errors.go @@ -0,0 +1,9 @@ +package types + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var ( + ErrAccountAlreadyExist = sdkerrors.Register(ModuleName, 1, "Target account already exists") +) diff --git a/x/account/internal/types/events.go b/x/account/internal/types/events.go new file mode 100644 index 0000000000..387b6bb57b --- /dev/null +++ b/x/account/internal/types/events.go @@ -0,0 +1,16 @@ +package types + +var ( + EventCreateAccount = "create_account" + EventEmpty = "empty" + + MsgTypeCreateAccount = EventCreateAccount + MsgTypeEmpty = EventEmpty + + AttributeKeyCreateAccountFrom = "create_account_from" + AttributeKeyCreateAccountTarget = "create_account_target" + + AttributeKeyFrom = "from" + + AttributeValueCategory = ModuleName +) diff --git a/x/account/internal/types/key.go b/x/account/internal/types/key.go new file mode 100644 index 0000000000..e567b6f820 --- /dev/null +++ b/x/account/internal/types/key.go @@ -0,0 +1,7 @@ +package types + +const ( + ModuleName = "account" + + RouterKey = ModuleName +) diff --git a/x/account/internal/types/msgs.go b/x/account/internal/types/msgs.go new file mode 100644 index 0000000000..0d8cb7e884 --- /dev/null +++ b/x/account/internal/types/msgs.go @@ -0,0 +1,81 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +type MsgEmpty struct { + From sdk.AccAddress `json:"from"` +} + +var _ sdk.Msg = MsgEmpty{} + +// NewMsgCreateAccount - construct create account msg. +func NewMsgEmpty(from sdk.AccAddress) MsgEmpty { + return MsgEmpty{From: from} +} + +// Route Implements Msg. +func (msg MsgEmpty) Route() string { return RouterKey } + +// Type Implements Msg. +func (msg MsgEmpty) Type() string { return MsgTypeEmpty } + +// ValidateBasic Implements Msg. +func (msg MsgEmpty) ValidateBasic() error { + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing signer address") + } + return nil +} + +// GetSignBytes Implements Msg. +func (msg MsgEmpty) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners Implements Msg. +func (msg MsgEmpty) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +// MsgCreateAccount - create account transaction of the account module +type MsgCreateAccount struct { + From sdk.AccAddress `json:"from"` + Target sdk.AccAddress `json:"target"` +} + +var _ sdk.Msg = MsgCreateAccount{} + +// NewMsgCreateAccount - construct create account msg. +func NewMsgCreateAccount(fromAddr, targetAddr sdk.AccAddress) MsgCreateAccount { + return MsgCreateAccount{From: fromAddr, Target: targetAddr} +} + +// Route Implements Msg. +func (msg MsgCreateAccount) Route() string { return RouterKey } + +// Type Implements Msg. +func (msg MsgCreateAccount) Type() string { return MsgTypeCreateAccount } + +// ValidateBasic Implements Msg. +func (msg MsgCreateAccount) ValidateBasic() error { + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing signer address") + } + if msg.Target.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing target address to be created") + } + return nil +} + +// GetSignBytes Implements Msg. +func (msg MsgCreateAccount) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners Implements Msg. +func (msg MsgCreateAccount) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} diff --git a/x/account/module.go b/x/account/module.go new file mode 100644 index 0000000000..da5d5f56bb --- /dev/null +++ b/x/account/module.go @@ -0,0 +1,100 @@ +package account + +import ( + "encoding/json" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/gorilla/mux" + "github.com/spf13/cobra" + abci "github.com/tendermint/tendermint/abci/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// app module basics object +type AppModuleBasic struct{} + +// module name +func (AppModuleBasic) Name() string { + return ModuleName +} + +// register module codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// default genesis state +func (AppModuleBasic) DefaultGenesis() json.RawMessage { return nil } + +// module validate genesis +func (AppModuleBasic) ValidateGenesis(_ json.RawMessage) error { return nil } + +// register rest routes +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { +} + +// get the root tx command of this module +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + return nil +} + +// get the root query command of this module +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { return nil } + +// ___________________________ +// app module +type AppModule struct { + AppModuleBasic + keeper auth.AccountKeeper +} + +func NewAppModule(keeper auth.AccountKeeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// module name +func (AppModule) Name() string { return ModuleName } + +// register invariants +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// module message route name +func (AppModule) Route() string { return RouterKey } + +// module handler +func (am AppModule) NewHandler() sdk.Handler { return NewHandler(am.keeper) } + +// module querier route name +func (AppModule) QuerierRoute() string { return RouterKey } + +// module querier +func (am AppModule) NewQuerierHandler() sdk.Querier { + return auth.NewQuerier(am.keeper) +} + +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + return ModuleCdc.MustMarshalJSON(nil) +} + +// module begin-block +func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// module end-block +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/account/spec/01_concept.md b/x/account/spec/01_concept.md new file mode 100644 index 0000000000..bee5c528cf --- /dev/null +++ b/x/account/spec/01_concept.md @@ -0,0 +1,2 @@ +# Concept +TBD diff --git a/x/account/spec/02_keepers.md b/x/account/spec/02_keepers.md new file mode 100644 index 0000000000..bddc31f1af --- /dev/null +++ b/x/account/spec/02_keepers.md @@ -0,0 +1,2 @@ +# Keepers +TBD diff --git a/x/account/spec/03_messages.md b/x/account/spec/03_messages.md new file mode 100644 index 0000000000..bc6d8f9dd1 --- /dev/null +++ b/x/account/spec/03_messages.md @@ -0,0 +1,36 @@ +# Messages + +## MsgEmpty + +```golang +type MsgEmpty struct { + From string `json:"name"` +} +``` + +**Do nothing** +- This message does nothing +- Signer(`From`) of this message is required +- You can pay the fee for any other message with this message + +### MsgCreateAccount + +```golang +type MsgCreateAccount struct { + From sdk.AccAddress `json:"from"` + Target sdk.AccAddress `json:"target"` +} +``` + +**Create an account** +- Signer(`FromAddress`) of this message must already been registered before +- `TargetAddress` must never been registered before + +# Syntax +| Message/Attributes | Tag | Type | +| ---- | ---- | ---- | +| Message | account/MsgCreateAccount | github.com/line/link/x/account/internal/types.MsgCreateAccount | + | Attributes | from | []uint8 | + | Attributes | target | []uint8 | +| Message | account/MsgEmpty | github.com/line/link/x/account/internal/types.MsgEmpty | + | Attributes | from | []uint8 | diff --git a/x/account/spec/04_events.md b/x/account/spec/04_events.md new file mode 100644 index 0000000000..a521bf0e42 --- /dev/null +++ b/x/account/spec/04_events.md @@ -0,0 +1,20 @@ +# Events +**Not fully documented yet** +The account module emits the following events: + + +### MsgCreateAccount +| Type | Attribute Key | Attribute Value | +|-----------------|-----------------------|-------------------| +| message | module | account | +| message | sender | {fromAddress} | +| message | action | create_account | +| create_account | create_account_from | {fromAddress} | +| create_account | create_account_target | {targetAddress} | + +### MsgEmpty +| Type | Attribute Key | Attribute Value | +|-----------------|---------------------|---------------------| +| message | module | account | +| message | sender | {from} | +| message | action | empty | diff --git a/x/account/spec/README.md b/x/account/spec/README.md new file mode 100644 index 0000000000..72345b3dc8 --- /dev/null +++ b/x/account/spec/README.md @@ -0,0 +1,14 @@ +# Account module specification + + + +## Abstract + +This document specifies the account module of the Link Network. + +## Contents + +1. **[Concept](01_concept.md)** +2. **[Keepers](02_keepers.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** diff --git a/x/coin/alias.go b/x/coin/alias.go new file mode 100644 index 0000000000..8d51bab8ab --- /dev/null +++ b/x/coin/alias.go @@ -0,0 +1,31 @@ +package coin + +import ( + "github.com/line/lbm-sdk/v2/x/coin/client/cli" + "github.com/line/lbm-sdk/v2/x/coin/internal/keeper" + "github.com/line/lbm-sdk/v2/x/coin/internal/types" +) + +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey +) + +type ( + Keeper = keeper.Keeper + + MsgSend = types.MsgSend + MsgMultiSend = types.MsgMultiSend + + Input = types.Input + Output = types.Output +) + +var ( + SendTxCmd = cli.SendTxCmd + NewMsgSend = types.NewMsgSend + NewKeeper = keeper.NewKeeper + ActionTransferTo = types.ActionTransferTo + ErrCanNotTransferToBlacklisted = types.ErrCanNotTransferToBlacklisted +) diff --git a/x/coin/client/cli/tx.go b/x/coin/client/cli/tx.go new file mode 100644 index 0000000000..c4bc067246 --- /dev/null +++ b/x/coin/client/cli/tx.go @@ -0,0 +1,64 @@ +package cli + +import ( + "bufio" + + "github.com/line/lbm-sdk/v2/x/coin/internal/types" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" +) + +// GetTxCmd returns the transaction commands for this module +func GetTxCmd(cdc *codec.Codec) *cobra.Command { + txCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Coin transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + txCmd.AddCommand( + SendTxCmd(cdc), + ) + return txCmd +} + +// SendTxCmd will create a send tx and sign it with the given key. +func SendTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "send [from_key_or_address] [to_address] [amount]", + Short: "Create and sign a send tx", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + to, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + // parse coins trying to be sent + coins, err := sdk.ParseCoins(args[2]) + if err != nil { + return err + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgSend(cliCtx.GetFromAddress(), to, coins) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd = flags.PostCommands(cmd)[0] + + return cmd +} diff --git a/x/coin/client/rest/query.go b/x/coin/client/rest/query.go new file mode 100644 index 0000000000..7a2bee8680 --- /dev/null +++ b/x/coin/client/rest/query.go @@ -0,0 +1,85 @@ +package rest + +import ( + "fmt" + "net/http" + "strings" + + "github.com/line/lbm-sdk/v2/x/coin/internal/types" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" +) + +// query accountREST Handler +func QueryBalancesRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + bech32addr := vars["address"] + denom := vars["denom"] // "" if the key is not exists + + arrBech32addrs := strings.Split(bech32addr, ",") + if len(arrBech32addrs) > 1 { + addrs := make([]sdk.AccAddress, len(arrBech32addrs)) + + for i, bech32addr := range arrBech32addrs { + addr, err := sdk.AccAddressFromBech32(bech32addr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + addrs[i] = addr + } + + params := types.NewQueryBulkBalanceParams(addrs) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + queryBalancesRequest(w, r, cliCtx, bz, "bulk_balances") + } else { + addr, err := sdk.AccAddressFromBech32(bech32addr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + params := types.NewQueryBalanceParams(addr, denom) + bz, err := cliCtx.Codec.MarshalJSON(params) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + queryBalancesRequest(w, r, cliCtx, bz, "balances") + } + } +} + +func queryBalancesRequest(w http.ResponseWriter, r *http.Request, cliCtx context.CLIContext, bz []byte, path string) { + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/coin/%s", path), bz) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + // the query will return empty if there is no data for this account + if len(res) == 0 { + rest.PostProcessResponse(w, cliCtx, sdk.Coins{}) + return + } + + rest.PostProcessResponse(w, cliCtx, res) +} diff --git a/x/coin/client/rest/tx.go b/x/coin/client/rest/tx.go new file mode 100644 index 0000000000..5d6f0bc029 --- /dev/null +++ b/x/coin/client/rest/tx.go @@ -0,0 +1,60 @@ +package rest + +import ( + "net/http" + + "github.com/line/lbm-sdk/v2/x/coin/internal/types" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" +) + +// RegisterRoutes - Central function to define routes that get registered by the main application +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc("/coin/accounts/{address}/transfers", SendRequestHandlerFn(cliCtx)).Methods("POST") + r.HandleFunc("/coin/balances/{address}", QueryBalancesRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/coin/balances/{address}/{denom}", QueryBalancesRequestHandlerFn(cliCtx)).Methods("GET") +} + +// SendReq defines the properties of a send request's body. +type SendReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Amount sdk.Coins `json:"amount" yaml:"amount"` +} + +// SendRequestHandlerFn - http request handler to send coins to a address. +func SendRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + bech32Addr := vars["address"] + + toAddr, err := sdk.AccAddressFromBech32(bech32Addr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + var req SendReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + msg := types.NewMsgSend(fromAddr, toAddr, req.Amount) + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/coin/handler.go b/x/coin/handler.go new file mode 100644 index 0000000000..38c74a6a29 --- /dev/null +++ b/x/coin/handler.go @@ -0,0 +1,69 @@ +package coin + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/coin/internal/keeper" + "github.com/line/lbm-sdk/v2/x/coin/internal/types" +) + +func NewHandler(k keeper.Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + + switch msg := msg.(type) { + case types.MsgSend: + return handleMsgSend(ctx, k, msg) + + case types.MsgMultiSend: + return handleMsgMultiSend(ctx, k, msg) + + default: + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized coin message type: %T", msg) + } + } +} + +// Handle MsgSend. +func handleMsgSend(ctx sdk.Context, k keeper.Keeper, msg types.MsgSend) (*sdk.Result, error) { + err := k.SendCoins(ctx, msg.From, msg.To, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(types.AttributeKeySender, msg.From.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +// Handle MsgMultiSend. +func handleMsgMultiSend(ctx sdk.Context, k keeper.Keeper, msg types.MsgMultiSend) (*sdk.Result, error) { + err := k.InputOutputCoins(ctx, msg.Inputs, msg.Outputs) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + ), + ) + + for _, in := range msg.Inputs { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(types.AttributeKeySender, in.Address.String()), + ), + ) + } + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/coin/handler_test.go b/x/coin/handler_test.go new file mode 100644 index 0000000000..54fbfd01f1 --- /dev/null +++ b/x/coin/handler_test.go @@ -0,0 +1,138 @@ +package coin + +import ( + "strings" + "testing" + + "github.com/line/lbm-sdk/v2/x/coin/internal/keeper" + "github.com/line/lbm-sdk/v2/x/coin/internal/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/stretchr/testify/require" +) + +func TestInvalidMsg(t *testing.T) { + h := NewHandler(keeper.Keeper{}) + + _, err := h(sdk.NewContext(nil, abci.Header{}, false, nil), sdk.NewTestMsg()) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "unrecognized coin message type")) +} + +func TestHandlerSend(t *testing.T) { + input := keeper.SetupTestInput() + ctx, _, ak := input.Ctx, input.K, input.Ak + + h := NewHandler(input.K) + + const ( + length3Denom = "foo" + length5Denom = "f2345" + length6Denom = "f23456" + length5Denom2 = "f2346" + ) + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr1")) + + acc := ak.NewAccountWithAddress(ctx, addr1) + + err := acc.SetCoins(sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 100), sdk.NewInt64Coin(length5Denom, 100))) + require.NoError(t, err) + ak.SetAccount(ctx, acc) + + { + coins := sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 10)) + msg := types.NewMsgSend(addr1, addr2, coins) + _, err := h(ctx, msg) + require.NoError(t, err) + } + + { + coins := sdk.NewCoins(sdk.NewInt64Coin(length6Denom, 10)) + msg := types.NewMsgSend(addr1, addr2, coins) + _, err := h(ctx, msg) + require.Error(t, err) + } + + { + inputs := []Input{ + types.NewInput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 4), sdk.NewInt64Coin(length5Denom, 2))), + types.NewInput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 3))), + } + + outputs := []Output{ + types.NewOutput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 7))), + types.NewOutput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length5Denom, 2))), + } + msg := types.NewMsgMultiSend(inputs, outputs) + _, err := h(ctx, msg) + require.NoError(t, err) + } + + { + inputs := []Input{ + types.NewInput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 4), sdk.NewInt64Coin(length5Denom, 2))), + types.NewInput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 3))), + } + + outputs := []Output{ + types.NewOutput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 7))), + types.NewOutput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length5Denom2, 2))), + } + msg := types.NewMsgMultiSend(inputs, outputs) + require.Panics(t, func() { h(ctx, msg) }) // nolint + } +} + +func TestHandlerSendRestricted(t *testing.T) { + input := keeper.SetupTestInput() + ctx, _, ak := input.Ctx, input.K, input.Ak + + h := NewHandler(input.K) + + const ( + length3Denom = "foo" + length8Denom = "f2345678" + ) + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr1")) + + acc := ak.NewAccountWithAddress(ctx, addr1) + + err := acc.SetCoins(sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 100), sdk.NewInt64Coin(length8Denom, 100))) + require.NoError(t, err) + ak.SetAccount(ctx, acc) + + { + coins := sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 10)) + msg := types.NewMsgSend(addr1, addr2, coins) + require.NoError(t, msg.ValidateBasic()) + _, err := h(ctx, msg) + require.NoError(t, err) + } + + { + coins := sdk.NewCoins(sdk.NewInt64Coin(length8Denom, 10)) + msg := types.NewMsgSend(addr1, addr2, coins) + require.Error(t, msg.ValidateBasic()) + } + + { + inputs := []Input{ + types.NewInput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 4), sdk.NewInt64Coin(length8Denom, 2))), + types.NewInput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 3))), + } + + outputs := []Output{ + types.NewOutput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 7))), + types.NewOutput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length8Denom, 2))), + } + msg := types.NewMsgMultiSend(inputs, outputs) + _, err := h(ctx, msg) + require.Error(t, err) + } +} diff --git a/x/coin/internal/keeper/blacklist.go b/x/coin/internal/keeper/blacklist.go new file mode 100644 index 0000000000..3c9cffc5cd --- /dev/null +++ b/x/coin/internal/keeper/blacklist.go @@ -0,0 +1,19 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/coin/internal/types" +) + +func (keeper Keeper) BlacklistAccountAction(ctx sdk.Context, addr sdk.AccAddress, action string) { + store := ctx.KVStore(keeper.storeKey) + + // value is just a key w/o the module prefix + v := addr.String() + ":" + action + store.Set(types.BlacklistKey(addr, action), []byte(v)) +} + +func (keeper Keeper) IsBlacklistedAccountAction(ctx sdk.Context, addr sdk.AccAddress, action string) bool { + store := ctx.KVStore(keeper.storeKey) + return store.Has(types.BlacklistKey(addr, action)) +} diff --git a/x/coin/internal/keeper/hooks.go b/x/coin/internal/keeper/hooks.go new file mode 100644 index 0000000000..8c3db2e774 --- /dev/null +++ b/x/coin/internal/keeper/hooks.go @@ -0,0 +1,11 @@ +package keeper + +// Hooks wrapper struct for safety box keeper +type Hooks struct { + keeper Keeper +} + +// Return the wrapper struct +func (keeper Keeper) Hooks() *Hooks { + return &Hooks{keeper} +} diff --git a/x/coin/internal/keeper/keeper.go b/x/coin/internal/keeper/keeper.go new file mode 100644 index 0000000000..1d73bb9409 --- /dev/null +++ b/x/coin/internal/keeper/keeper.go @@ -0,0 +1,104 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/coin/internal/types" +) + +type Keeper struct { + bk types.BankKeeper + storeKey sdk.StoreKey +} + +func NewKeeper(bk types.BankKeeper, storeKey sdk.StoreKey) Keeper { + return Keeper{ + bk: bk, + storeKey: storeKey, + } +} + +// SendCoins moves coins from one account to another +func (keeper Keeper) SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error { + // reject if to address is blacklisted (safety box addresses) + if keeper.IsBlacklistedAccountAction(ctx, toAddr, types.ActionTransferTo) { + return sdkerrors.Wrapf(types.ErrCanNotTransferToBlacklisted, "Addr: %s", toAddr.String()) + } + + _, err := keeper.bk.SubtractCoins(ctx, fromAddr, amt) + if err != nil { + return err + } + + _, err = keeper.bk.AddCoins(ctx, toAddr, amt) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransfer, + sdk.NewAttribute(types.AttributeKeySender, fromAddr.String()), + sdk.NewAttribute(types.AttributeKeyRecipient, toAddr.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, amt.String()), + ), + }) + + return nil +} + +// InputOutputCoins handles a list of inputs and outputs +func (keeper Keeper) InputOutputCoins(ctx sdk.Context, inputs []types.Input, outputs []types.Output) error { + if err := types.ValidateInputsOutputs(inputs, outputs); err != nil { + return err + } + + for _, in := range inputs { + _, err := keeper.bk.SubtractCoins(ctx, in.Address, in.Coins) + if err != nil { + return err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeTransfer, + sdk.NewAttribute(types.AttributeKeySender, in.Address.String()), + ), + ) + } + + for _, out := range outputs { + // reject if to address is blacklisted (safety box addresses) + if keeper.IsBlacklistedAccountAction(ctx, out.Address, types.ActionTransferTo) { + return sdkerrors.Wrapf(types.ErrCanNotTransferToBlacklisted, "Addr: %s", out.Address.String()) + } + + _, err := keeper.bk.AddCoins(ctx, out.Address, out.Coins) + if err != nil { + return err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeTransfer, + sdk.NewAttribute(types.AttributeKeyRecipient, out.Address.String()), + ), + ) + } + + return nil +} + +// GetCoins returns the coins at the addr. +func (keeper Keeper) GetCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins { + return keeper.bk.GetCoins(ctx, addr) +} + +// HasCoins returns whether or not an account has at least amt coins. +func (keeper Keeper) HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) bool { + return keeper.GetCoins(ctx, addr).IsAllGTE(amt) +} + +func (keeper Keeper) BlacklistedAddr(creator sdk.AccAddress) bool { + return keeper.bk.BlacklistedAddr(creator) +} diff --git a/x/coin/internal/keeper/keeper_test.go b/x/coin/internal/keeper/keeper_test.go new file mode 100644 index 0000000000..33f34158e5 --- /dev/null +++ b/x/coin/internal/keeper/keeper_test.go @@ -0,0 +1,78 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/coin/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper(t *testing.T) { + input := SetupTestInput() + ctx := input.Ctx + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr2")) + addr3 := sdk.AccAddress([]byte("addr3")) + acc := input.Ak.NewAccountWithAddress(ctx, addr1) + + // Test GetCoins/SetCoins + input.Ak.SetAccount(ctx, acc) + require.True(t, input.K.GetCoins(ctx, addr1).IsEqual(sdk.NewCoins())) + + acc = input.Ak.GetAccount(ctx, acc.GetAddress()) + err := acc.SetCoins(sdk.NewCoins(sdk.NewInt64Coin("fooc", 15))) + require.NoError(t, err) + input.Ak.SetAccount(ctx, acc) + require.True(t, input.K.GetCoins(ctx, addr1).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("fooc", 15)))) + + // Test HasCoins + require.True(t, input.K.HasCoins(ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin("fooc", 15)))) + require.True(t, input.K.HasCoins(ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin("fooc", 5)))) + require.False(t, input.K.HasCoins(ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin("fooc", 20)))) + require.False(t, input.K.HasCoins(ctx, addr1, sdk.NewCoins(sdk.NewInt64Coin("barc", 5)))) + + // Test SendCoins + err = input.K.SendCoins(ctx, addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin("fooc", 5))) + require.NoError(t, err) + require.True(t, input.K.GetCoins(ctx, addr1).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("fooc", 10)))) + require.True(t, input.K.GetCoins(ctx, addr2).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("fooc", 5)))) + + err = input.K.SendCoins(ctx, addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin("fooc", 50))) + require.Error(t, err) + require.True(t, input.K.GetCoins(ctx, addr1).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("fooc", 10)))) + require.True(t, input.K.GetCoins(ctx, addr2).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("fooc", 5)))) + + // Test InputOutputCoins + input1 := types.NewInput(addr2, sdk.NewCoins(sdk.NewInt64Coin("fooc", 2))) + output1 := types.NewOutput(addr1, sdk.NewCoins(sdk.NewInt64Coin("fooc", 2))) + err = input.K.InputOutputCoins(ctx, []types.Input{input1}, []types.Output{output1}) + require.NoError(t, err) + require.True(t, input.K.GetCoins(ctx, addr1).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("fooc", 12)))) + require.True(t, input.K.GetCoins(ctx, addr2).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("fooc", 3)))) + + acc = input.Ak.GetAccount(ctx, acc.GetAddress()) + coins := acc.GetCoins().Add(sdk.NewCoins(sdk.NewInt64Coin("barc", 15))...) + err = acc.SetCoins(coins) + require.NoError(t, err) + input.Ak.SetAccount(ctx, acc) + require.True(t, input.K.GetCoins(ctx, addr1).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("fooc", 12), sdk.NewInt64Coin("barc", 15)))) + require.True(t, input.K.GetCoins(ctx, addr2).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("fooc", 3)))) + + inputs := []types.Input{ + types.NewInput(addr1, sdk.NewCoins(sdk.NewInt64Coin("barc", 4), sdk.NewInt64Coin("fooc", 2))), + types.NewInput(addr2, sdk.NewCoins(sdk.NewInt64Coin("fooc", 3))), + } + + outputs := []types.Output{ + types.NewOutput(addr1, sdk.NewCoins(sdk.NewInt64Coin("barc", 1))), + types.NewOutput(addr2, sdk.NewCoins(sdk.NewInt64Coin("barc", 1))), + types.NewOutput(addr3, sdk.NewCoins(sdk.NewInt64Coin("barc", 2), sdk.NewInt64Coin("fooc", 5))), + } + err = input.K.InputOutputCoins(ctx, inputs, outputs) + require.NoError(t, err) + require.True(t, input.K.GetCoins(ctx, addr1).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("barc", 12), sdk.NewInt64Coin("fooc", 10)))) + require.True(t, input.K.GetCoins(ctx, addr2).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("barc", 1)))) + require.True(t, input.K.GetCoins(ctx, addr3).IsEqual(sdk.NewCoins(sdk.NewInt64Coin("barc", 2), sdk.NewInt64Coin("fooc", 5)))) +} diff --git a/x/coin/internal/keeper/querier.go b/x/coin/internal/keeper/querier.go new file mode 100644 index 0000000000..fa36a62a79 --- /dev/null +++ b/x/coin/internal/keeper/querier.go @@ -0,0 +1,81 @@ +package keeper + +import ( + "github.com/line/lbm-sdk/v2/x/coin/internal/types" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + // query balance path + QueryBalance = "balances" + QueryBulkBalances = "bulk_balances" +) + +// NewQuerier returns a new sdk.Keeper instance. +func NewQuerier(k Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) { + switch path[0] { + case QueryBalance: + return queryBalance(ctx, req, k) + case QueryBulkBalances: + return queryBulkBalances(ctx, req, k) + + default: + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown query path: %s", path[0]) + } + } +} + +// queryBalance fetch an account's balance for the supplied height. +// Height and account address are passed as first and second path components respectively. +func queryBalance(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) { + var params types.QueryBalanceParams + + if err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + if len(params.Denom) == 0 { + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, k.GetCoins(ctx, params.Address)) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil + } + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, k.GetCoins(ctx, params.Address).AmountOf(params.Denom)) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil +} + +func queryBulkBalances(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) { + var params types.QueryBulkBalancesParams + + if err := types.ModuleCdc.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + if len(params.Addresses) > types.RequestGetsLimit { + return nil, sdkerrors.Wrapf(types.ErrRequestGetsLimit, "Limit: %d", types.RequestGetsLimit) + } + + res := make([]types.QueryBulkBalancesResult, len(params.Addresses)) + for idx, addr := range params.Addresses { + res[idx] = types.NewQueryBulkBalancesResult(addr, k.GetCoins(ctx, addr)) + } + + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, res) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil +} diff --git a/x/coin/internal/keeper/querier_test.go b/x/coin/internal/keeper/querier_test.go new file mode 100644 index 0000000000..7d5386d1fe --- /dev/null +++ b/x/coin/internal/keeper/querier_test.go @@ -0,0 +1,111 @@ +package keeper + +import ( + "fmt" + "testing" + + "github.com/line/lbm-sdk/v2/x/coin/internal/types" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/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" +) + +func TestBalances(t *testing.T) { + input := SetupTestInput() + req := abci.RequestQuery{ + Path: fmt.Sprintf("custom/coin/%s", QueryBalance), + Data: []byte{}, + } + + querier := NewQuerier(input.K) + + res, err := querier(input.Ctx, []string{"balances"}, req) + require.NotNil(t, err) + require.Nil(t, res) + + _, _, addr := authtypes.KeyTestPubAddr() + req.Data = input.Cdc.MustMarshalJSON(types.NewQueryBalanceParams(addr, "")) + res, err = querier(input.Ctx, []string{"balances"}, req) + require.Nil(t, err) // the account does not exist, no error returned anyway + require.NotNil(t, res) + + var coins sdk.Coins + require.NoError(t, input.Cdc.UnmarshalJSON(res, &coins)) + require.True(t, coins.IsZero()) + + acc := input.Ak.NewAccountWithAddress(input.Ctx, addr) + require.NoError(t, acc.SetCoins(sdk.NewCoins(sdk.NewInt64Coin("foo", 10)))) + input.Ak.SetAccount(input.Ctx, acc) + res, err = querier(input.Ctx, []string{"balances"}, req) + require.Nil(t, err) + require.NotNil(t, res) + require.NoError(t, input.Cdc.UnmarshalJSON(res, &coins)) + require.True(t, coins.AmountOf("foo").Equal(sdk.NewInt(10))) + + // Query with denomination + var amount sdk.Int + req.Data = input.Cdc.MustMarshalJSON(types.NewQueryBalanceParams(addr, "foo")) + res, err = querier(input.Ctx, []string{"balances"}, req) + require.Nil(t, err) // the account does not exist, no error returned anyway + require.NotNil(t, res) + + require.NoError(t, input.Cdc.UnmarshalJSON(res, &amount)) + require.True(t, amount.Equal(sdk.NewInt(10))) +} + +func TestQuerierRouteNotFound(t *testing.T) { + input := SetupTestInput() + req := abci.RequestQuery{ + Path: "custom/coin/notfound", + Data: []byte{}, + } + + querier := NewQuerier(input.K) + _, err := querier(input.Ctx, []string{"notfound"}, req) + require.Error(t, err) +} + +func TestBulkBalances(t *testing.T) { + input := SetupTestInput() + req := abci.RequestQuery{ + Path: fmt.Sprintf("custom/coin/%s", QueryBulkBalances), + Data: []byte{}, + } + + querier := NewQuerier(input.K) + + res, err := querier(input.Ctx, []string{"bulk_balances"}, req) + require.NotNil(t, err) + require.Nil(t, res) + + addrs := make([]sdk.AccAddress, 0, 101) + _, _, addr := authtypes.KeyTestPubAddr() + addrs = append(addrs, addr) + req.Data = input.Cdc.MustMarshalJSON(types.NewQueryBulkBalanceParams(addrs)) + res, err = querier(input.Ctx, []string{"bulk_balances"}, req) + require.Nil(t, err) + require.NotNil(t, res) + + for i := 0; i < 99; i++ { + _, _, addr = authtypes.KeyTestPubAddr() + addrs = append(addrs, addr) + } + + req.Data = input.Cdc.MustMarshalJSON(types.NewQueryBulkBalanceParams(addrs)) + res, err = querier(input.Ctx, []string{"bulk_balances"}, req) + require.Nil(t, err) + require.NotNil(t, res) + + _, _, addr = authtypes.KeyTestPubAddr() + addrs = append(addrs, addr) + + req.Data = input.Cdc.MustMarshalJSON(types.NewQueryBulkBalanceParams(addrs)) + res, err = querier(input.Ctx, []string{"bulk_balances"}, req) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrRequestGetsLimit, "Limit: %d", types.RequestGetsLimit).Error()) + require.Nil(t, res) +} diff --git a/x/coin/internal/keeper/test_common.go b/x/coin/internal/keeper/test_common.go new file mode 100644 index 0000000000..e09f7b7a44 --- /dev/null +++ b/x/coin/internal/keeper/test_common.go @@ -0,0 +1,67 @@ +package keeper + +// DONTCOVER + +import ( + "github.com/line/lbm-sdk/v2/x/coin/internal/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/params" +) + +type TestInput struct { + Cdc *codec.Codec + Ctx sdk.Context + K Keeper + Ak auth.AccountKeeper + Pk params.Keeper +} + +func SetupTestInput() TestInput { + db := dbm.NewMemDB() + + cdc := codec.New() + auth.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + + authCapKey := sdk.NewKVStoreKey("authCapKey") + keyParams := sdk.NewKVStoreKey("params") + tkeyParams := sdk.NewTransientStoreKey("transient_params") + keyBank := sdk.NewKVStoreKey(types.StoreKey) + + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(authCapKey, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyBank, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + + if err := ms.LoadLatestVersion(); err != nil { + panic(err) + } + + blacklistedAddrs := make(map[string]bool) + blacklistedAddrs[sdk.AccAddress([]byte("moduleAcc")).String()] = true + + pk := params.NewKeeper(cdc, keyParams, tkeyParams) + + ak := auth.NewAccountKeeper( + cdc, authCapKey, pk.Subspace(auth.DefaultParamspace), auth.ProtoBaseAccount, + ) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "test-chain-id"}, false, log.NewNopLogger()) + + ak.SetParams(ctx, auth.DefaultParams()) + + bankKeeper := bank.NewBaseKeeper(ak, pk.Subspace(bank.DefaultParamspace), blacklistedAddrs) + bankKeeper.SetSendEnabled(ctx, true) + + keeper := NewKeeper(bankKeeper, keyBank) + + return TestInput{Cdc: cdc, Ctx: ctx, K: keeper, Ak: ak, Pk: pk} +} diff --git a/x/coin/internal/types/codec.go b/x/coin/internal/types/codec.go new file mode 100644 index 0000000000..172e3ebcff --- /dev/null +++ b/x/coin/internal/types/codec.go @@ -0,0 +1,18 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +var ModuleCdc *codec.Codec + +func init() { + ModuleCdc = codec.New() + RegisterCodec(ModuleCdc) + ModuleCdc.Seal() +} + +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(MsgSend{}, "coin/MsgSend", nil) + cdc.RegisterConcrete(MsgMultiSend{}, "coin/MsgMultiSend", nil) +} diff --git a/x/coin/internal/types/errors.go b/x/coin/internal/types/errors.go new file mode 100644 index 0000000000..95259f82cd --- /dev/null +++ b/x/coin/internal/types/errors.go @@ -0,0 +1,17 @@ +package types + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const Codespace = ModuleName + +var ( + ErrNoInputs = sdkerrors.Register(Codespace, 1, "no inputs to send transaction") + ErrNoOutputs = sdkerrors.Register(Codespace, 2, "no outputs to send transaction") + ErrInputOutputMismatch = sdkerrors.Register(Codespace, 3, "sum inputs != sum outputs") + ErrSendDisabled = sdkerrors.Register(Codespace, 4, "send transactions are disabled") + ErrCanNotTransferToBlacklisted = sdkerrors.Register(Codespace, 5, "Cannot transfer to safety box addresses") + ErrRequestGetsLimit = sdkerrors.Register(Codespace, 6, "the gets should be limited") + ErrInvalidDenom = sdkerrors.Register(Codespace, 7, "invalid denom") +) diff --git a/x/coin/internal/types/events.go b/x/coin/internal/types/events.go new file mode 100644 index 0000000000..5e65f0ed7f --- /dev/null +++ b/x/coin/internal/types/events.go @@ -0,0 +1,11 @@ +package types + +// Coin module event types +var ( + EventTypeTransfer = "transfer" + + AttributeKeyRecipient = "recipient" + AttributeKeySender = "sender" + + AttributeValueCategory = ModuleName +) diff --git a/x/coin/internal/types/expected_keepers.go b/x/coin/internal/types/expected_keepers.go new file mode 100644 index 0000000000..2345e5bc38 --- /dev/null +++ b/x/coin/internal/types/expected_keepers.go @@ -0,0 +1,26 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/exported" +) + +// AccountKeeper defines the account contract that must be fulfilled when +type AccountKeeper interface { + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) exported.Account + + GetAccount(ctx sdk.Context, addr sdk.AccAddress) exported.Account + GetAllAccounts(ctx sdk.Context) []exported.Account + SetAccount(ctx sdk.Context, acc exported.Account) + + IterateAccounts(ctx sdk.Context, process func(exported.Account) bool) +} + +// BankKeeper defines the expected bank keeper (noalias) +type BankKeeper interface { + GetCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) bool + SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, error) + AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, error) + BlacklistedAddr(creator sdk.AccAddress) bool +} diff --git a/x/coin/internal/types/key.go b/x/coin/internal/types/key.go new file mode 100644 index 0000000000..9b88dcd0ce --- /dev/null +++ b/x/coin/internal/types/key.go @@ -0,0 +1,21 @@ +package types + +import sdk "github.com/cosmos/cosmos-sdk/types" + +const ( + // module name + ModuleName = "coin" + StoreKey = ModuleName + + ActionTransferTo = "transferTo" +) + +var ( + BlacklistKeyPrefix = []byte{0x00} +) + +func BlacklistKey(addr sdk.AccAddress, action string) []byte { + key := append(BlacklistKeyPrefix, addr...) + key = append(key, []byte(":"+action)...) + return key +} diff --git a/x/coin/internal/types/msgs.go b/x/coin/internal/types/msgs.go new file mode 100644 index 0000000000..643bde96d6 --- /dev/null +++ b/x/coin/internal/types/msgs.go @@ -0,0 +1,202 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const RouterKey = ModuleName + +// MsgSend - high level transaction of the coin module +type MsgSend struct { + From sdk.AccAddress `json:"from"` + To sdk.AccAddress `json:"to"` + Amount sdk.Coins `json:"amount"` +} + +var _ sdk.Msg = MsgSend{} + +// NewMsgSend - construct arbitrary multi-in, multi-out send msg. +func NewMsgSend(fromAddr, toAddr sdk.AccAddress, amount sdk.Coins) MsgSend { + return MsgSend{From: fromAddr, To: toAddr, Amount: amount} +} + +// Route Implements Msg. +func (msg MsgSend) Route() string { return RouterKey } + +// Type Implements Msg. +func (msg MsgSend) Type() string { return "send" } + +// ValidateBasic Implements Msg. +func (msg MsgSend) ValidateBasic() error { + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing sender address") + } + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "missing recipient address") + } + if !msg.Amount.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String()) + } + if !msg.Amount.IsAllPositive() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String()) + } + if err := validateDenomination(msg.Amount); err != nil { + return err + } + return nil +} + +// GetSignBytes Implements Msg. +func (msg MsgSend) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners Implements Msg. +func (msg MsgSend) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +// MsgMultiSend - high level transaction of the coin module +type MsgMultiSend struct { + Inputs []Input `json:"inputs" yaml:"inputs"` + Outputs []Output `json:"outputs" yaml:"outputs"` +} + +var _ sdk.Msg = MsgMultiSend{} + +// NewMsgMultiSend - construct arbitrary multi-in, multi-out send msg. +func NewMsgMultiSend(in []Input, out []Output) MsgMultiSend { + return MsgMultiSend{Inputs: in, Outputs: out} +} + +// Route Implements Msg +func (msg MsgMultiSend) Route() string { return RouterKey } + +// Type Implements Msg +func (msg MsgMultiSend) Type() string { return "multisend" } + +// ValidateBasic Implements Msg. +func (msg MsgMultiSend) ValidateBasic() error { + // this just makes sure all the inputs and outputs are properly formatted, + // not that they actually have the money inside + if len(msg.Inputs) == 0 { + return ErrNoInputs + } + if len(msg.Outputs) == 0 { + return ErrNoOutputs + } + + return ValidateInputsOutputs(msg.Inputs, msg.Outputs) +} + +// GetSignBytes Implements Msg. +func (msg MsgMultiSend) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +// GetSigners Implements Msg. +func (msg MsgMultiSend) GetSigners() []sdk.AccAddress { + addrs := make([]sdk.AccAddress, len(msg.Inputs)) + for i, in := range msg.Inputs { + addrs[i] = in.Address + } + return addrs +} + +// Input models transaction input +type Input struct { + Address sdk.AccAddress `json:"address" yaml:"address"` + Coins sdk.Coins `json:"coins" yaml:"coins"` +} + +// ValidateBasic - validate transaction input +func (in Input) ValidateBasic() error { + if len(in.Address) == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "input address missing") + } + if !in.Coins.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, in.Coins.String()) + } + if !in.Coins.IsAllPositive() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, in.Coins.String()) + } + if err := validateDenomination(in.Coins); err != nil { + return err + } + return nil +} + +// NewInput - create a transaction input, used with MsgMultiSend +func NewInput(addr sdk.AccAddress, coins sdk.Coins) Input { + return Input{ + Address: addr, + Coins: coins, + } +} + +// Output models transaction outputs +type Output struct { + Address sdk.AccAddress `json:"address" yaml:"address"` + Coins sdk.Coins `json:"coins" yaml:"coins"` +} + +// ValidateBasic - validate transaction output +func (out Output) ValidateBasic() error { + if len(out.Address) == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "output address missing") + } + if !out.Coins.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, out.Coins.String()) + } + if !out.Coins.IsAllPositive() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, out.Coins.String()) + } + if err := validateDenomination(out.Coins); err != nil { + return err + } + return nil +} + +// NewOutput - create a transaction output, used with MsgMultiSend +func NewOutput(addr sdk.AccAddress, coins sdk.Coins) Output { + return Output{ + Address: addr, + Coins: coins, + } +} + +// ValidateInputsOutputs validates that each respective input and output is +// valid and that the sum of inputs is equal to the sum of outputs. +func ValidateInputsOutputs(inputs []Input, outputs []Output) error { + var totalIn, totalOut sdk.Coins + + for _, in := range inputs { + if err := in.ValidateBasic(); err != nil { + return err + } + totalIn = totalIn.Add(in.Coins...) + } + + for _, out := range outputs { + if err := out.ValidateBasic(); err != nil { + return err + } + totalOut = totalOut.Add(out.Coins...) + } + + // make sure inputs and outputs match + if !totalIn.IsEqual(totalOut) { + return ErrInputOutputMismatch + } + + return nil +} +func validateDenomination(coins sdk.Coins) error { + for _, coin := range coins { + if err := ValidateSymbolReserved(coin.Denom); err != nil { + return sdkerrors.Wrapf(ErrInvalidDenom, "invalid denom [%s] send message supports 3~5 length denom only", coin.Denom) + } + } + return nil +} diff --git a/x/coin/internal/types/msgs_test.go b/x/coin/internal/types/msgs_test.go new file mode 100644 index 0000000000..816df651c1 --- /dev/null +++ b/x/coin/internal/types/msgs_test.go @@ -0,0 +1,97 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestMsgs(t *testing.T) { + const ( + length3Denom = "foo" + length5Denom = "f2345" + length6Denom = "f23456" + length8Denom = "f2345678" + ) + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr1")) + + { + coins := sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 10)) + msg := NewMsgSend(addr1, addr2, coins) + require.NoError(t, msg.ValidateBasic()) + require.Equal(t, addr1.String(), msg.GetSigners()[0].String()) + } + { + coins := sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 10)) + msg := NewMsgSend(addr1, nil, coins) + require.Error(t, msg.ValidateBasic()) + } + { + coins := sdk.NewCoins(sdk.NewInt64Coin(length6Denom, 10)) + msg := NewMsgSend(addr1, addr2, coins) + require.Error(t, msg.ValidateBasic()) + } + + { + inputs := []Input{ + NewInput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 4), sdk.NewInt64Coin(length5Denom, 2))), + NewInput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 3))), + } + + outputs := []Output{ + NewOutput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 7))), + NewOutput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length5Denom, 2))), + } + msg := NewMsgMultiSend(inputs, outputs) + require.NoError(t, msg.ValidateBasic()) + require.Equal(t, addr1.String(), msg.GetSigners()[0].String()) + require.Equal(t, addr2.String(), msg.GetSigners()[1].String()) + } + // InputOutputMismatch + { + inputs := []Input{ + NewInput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 4), sdk.NewInt64Coin(length5Denom, 2))), + NewInput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 3))), + } + + outputs := []Output{ + NewOutput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length3Denom, 7))), + NewOutput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length5Denom, 1))), + } + msg := NewMsgMultiSend(inputs, outputs) + require.Error(t, msg.ValidateBasic()) + } + // Validate Denom + { + inputs := []Input{ + NewInput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length6Denom, 4), sdk.NewInt64Coin(length8Denom, 2))), + NewInput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length6Denom, 3))), + } + + outputs := []Output{ + NewOutput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length6Denom, 7))), + NewOutput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length8Denom, 2))), + } + msg := NewMsgMultiSend(inputs, outputs) + require.Error(t, msg.ValidateBasic()) + } + // NoInput or NoOutput + { + inputs := []Input{ + NewInput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length6Denom, 4), sdk.NewInt64Coin(length8Denom, 2))), + NewInput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length6Denom, 3))), + } + + outputs := []Output{ + NewOutput(addr1, sdk.NewCoins(sdk.NewInt64Coin(length6Denom, 7))), + NewOutput(addr2, sdk.NewCoins(sdk.NewInt64Coin(length8Denom, 2))), + } + msg := NewMsgMultiSend([]Input{}, outputs) + require.Error(t, msg.ValidateBasic()) + msg = NewMsgMultiSend(inputs, []Output{}) + require.Error(t, msg.ValidateBasic()) + } +} diff --git a/x/coin/internal/types/querier.go b/x/coin/internal/types/querier.go new file mode 100644 index 0000000000..2e33b895a0 --- /dev/null +++ b/x/coin/internal/types/querier.go @@ -0,0 +1,35 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const RequestGetsLimit = 100 + +// QueryBalanceOfParams defines the params for querying an account balance. +type QueryBalanceParams struct { + Address sdk.AccAddress + Denom string +} + +type QueryBulkBalancesParams struct { + Addresses []sdk.AccAddress `json:"addresses"` +} + +type QueryBulkBalancesResult struct { + Address sdk.AccAddress `json:"address"` + Coins sdk.Coins `json:"coins"` +} + +// NewQueryBalanceOfParams creates a new instance of QueryBalanceParams. +func NewQueryBalanceParams(addr sdk.AccAddress, denom string) QueryBalanceParams { + return QueryBalanceParams{Address: addr, Denom: denom} +} + +func NewQueryBulkBalanceParams(addrs []sdk.AccAddress) QueryBulkBalancesParams { + return QueryBulkBalancesParams{Addresses: addrs} +} + +func NewQueryBulkBalancesResult(address sdk.AccAddress, coins sdk.Coins) QueryBulkBalancesResult { + return QueryBulkBalancesResult{Address: address, Coins: coins} +} diff --git a/x/coin/internal/types/symbol.go b/x/coin/internal/types/symbol.go new file mode 100644 index 0000000000..961e275413 --- /dev/null +++ b/x/coin/internal/types/symbol.go @@ -0,0 +1,24 @@ +package types + +import ( + "fmt" + "regexp" +) + +const ( + /* #nosec */ + reSymbolStringReserved = `[a-z][a-z0-9]{2,4}` +) + +var ( + reSymbolReserved = regexp.MustCompile(fmt.Sprintf(`^%s$`, reSymbolStringReserved)) +) + +func ValidateReg(symbol string, reg *regexp.Regexp) error { + if !reg.MatchString(symbol) { + return fmt.Errorf("symbol [%s] mismatched to [%s]", symbol, reg.String()) + } + return nil +} + +func ValidateSymbolReserved(symbol string) error { return ValidateReg(symbol, reSymbolReserved) } diff --git a/x/coin/module.go b/x/coin/module.go new file mode 100644 index 0000000000..ee8c62c2a3 --- /dev/null +++ b/x/coin/module.go @@ -0,0 +1,106 @@ +package coin + +import ( + "encoding/json" + + "github.com/line/lbm-sdk/v2/x/coin/client/cli" + "github.com/line/lbm-sdk/v2/x/coin/client/rest" + "github.com/line/lbm-sdk/v2/x/coin/internal/keeper" + "github.com/line/lbm-sdk/v2/x/coin/internal/types" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// app module basics object +type AppModuleBasic struct{} + +// module name +func (AppModuleBasic) Name() string { return types.ModuleName } + +// register module codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { types.RegisterCodec(cdc) } + +// default genesis state +func (AppModuleBasic) DefaultGenesis() json.RawMessage { return nil } + +// module validate genesis +func (AppModuleBasic) ValidateGenesis(_ json.RawMessage) error { return nil } + +// register rest routes +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { + rest.RegisterRoutes(ctx, rtr) +} + +// get the root tx command of this module +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetTxCmd(cdc) +} + +// get the root query command of this module +func (AppModuleBasic) GetQueryCmd(_ *codec.Codec) *cobra.Command { return nil } + +// ___________________________ +// app module +type AppModule struct { + AppModuleBasic + keeper keeper.Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper keeper.Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// module name +func (AppModule) Name() string { return types.ModuleName } + +// register invariants +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// module message route name +func (AppModule) Route() string { return types.RouterKey } + +// module handler +func (am AppModule) NewHandler() sdk.Handler { return NewHandler(am.keeper) } + +// module querier route name +func (AppModule) QuerierRoute() string { return types.RouterKey } + +// module querier +func (am AppModule) NewQuerierHandler() sdk.Querier { + return keeper.NewQuerier(am.keeper) +} + +// module init-genesis +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + // TODO: fill the init + return []abci.ValidatorUpdate{} +} + +// module export genesis +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + // TODO: fill the export + return types.ModuleCdc.MustMarshalJSON(nil) +} + +func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/coin/spec/01_state.md b/x/coin/spec/01_state.md new file mode 100644 index 0000000000..639c27fc05 --- /dev/null +++ b/x/coin/spec/01_state.md @@ -0,0 +1,3 @@ +# State +TBD + diff --git a/x/coin/spec/02_keepers.md b/x/coin/spec/02_keepers.md new file mode 100644 index 0000000000..3ec8b7c59c --- /dev/null +++ b/x/coin/spec/02_keepers.md @@ -0,0 +1,3 @@ +# Keepers +TBD + diff --git a/x/coin/spec/03_messages.md b/x/coin/spec/03_messages.md new file mode 100644 index 0000000000..07bdbb0833 --- /dev/null +++ b/x/coin/spec/03_messages.md @@ -0,0 +1,57 @@ +# Messages + +## MsgSend + +```golang +type MsgSend struct { + From sdk.AccAddress `json:"from"` + To sdk.AccAddress `json:"to"` + Amount sdk.Coins `json:"amount"` +} +``` + +## MsgMultiSend +```golang +type MsgMultiSend struct { + Inputs []Input `json:"inputs"` + Outputs []Output `json:"outputs"` +} +``` + +```golang +type Input struct { + Address sdk.AccAddress `json:"address"` + Coins sdk.Coins `json:"coins"` +} +``` +```golang +type Output struct { + Address sdk.AccAddress `json:"address"` + Coins sdk.Coins `json:"coins"` +} +``` + +``` +handleMsgSend(msg MsgSend) + inputSum = 0 + for input in inputs + inputSum += input.Amount + outputSum = 0 + for output in outputs + outputSum += output.Amount + if inputSum != outputSum: + fail with "input/output amount mismatch" + + return inputOutputCoins(msg.Inputs, msg.Outputs) +``` + +# Syntax +| Message/Attributes | Tag | Type | +| ---- | ---- | ---- | +| Message | coin/MsgSend | github.com/line/link/x/coin/internal/types.MsgSend | + | Attributes | from | []uint8 | + | Attributes | to | []uint8 | + | Attributes | amount | []github.com/cosmos/cosmos-sdk/types.Coin | +| Message | coin/MsgMultiSend | github.com/line/link/x/coin/internal/types.MsgMultiSend | + | Attributes | inputs | []github.com/line/link/x/coin/internal/types.Input | + | Attributes | outputs | []github.com/line/link/x/coin/internal/types.Output | \ No newline at end of file diff --git a/x/coin/spec/04_events.md b/x/coin/spec/04_events.md new file mode 100644 index 0000000000..dd0285544b --- /dev/null +++ b/x/coin/spec/04_events.md @@ -0,0 +1,24 @@ +# Events + +The coin module emits the following events: + +## Handlers + +### MsgSend + +| Type | Attribute Key | Attribute Value | +|----------|---------------|--------------------| +| message | module | coin | +| message | action | send | +| message | sender | {senderAddress} | +| transfer | recipient | {recipientAddress} | +| transfer | amount | {amount} | + +### MsgMultiSend + +| Type | Attribute Key | Attribute Value | +|----------|---------------|--------------------| +| message | module | coin | +| message | action | multisend | +| message | sender | {senderAddress} | +| transfer | recipient | {recipientAddress} | diff --git a/x/coin/spec/README.md b/x/coin/spec/README.md new file mode 100644 index 0000000000..dca7df25be --- /dev/null +++ b/x/coin/spec/README.md @@ -0,0 +1,18 @@ +# Coin module specification + +## Abstract + +This document specifies the coin module of the LINK + +## Contents + +1. **[State](01_state.md)** +2. **[Keepers](02_keepers.md)** + - [Common Types](02_keepers.md#common-types) + - [BaseKeeper](02_keepers.md#basekeeper) + - [SendKeeper](02_keepers.md#sendkeeper) + - [ViewKeeper](02_keepers.md#viewkeeper) +3. **[Messages](03_messages.md)** + - [MsgSend](03_messages.md#msgsend) +4. **[Events](04_events.md)** + - [Handlers](04_events.md#handlers) diff --git a/x/collection/alias.go b/x/collection/alias.go new file mode 100644 index 0000000000..6037c6b55f --- /dev/null +++ b/x/collection/alias.go @@ -0,0 +1,107 @@ +package collection + +import ( + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/querier" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + DefaultParamspace = types.DefaultParamspace + EncodeRouterKey = types.EncodeRouterKey +) + +type ( + Token = types.Token + Tokens = types.Tokens + Coins = types.Coins + FT = types.FT + NFT = types.NFT + + TokenType = types.TokenType + + Collection = types.BaseCollection + + MsgCreateCollection = types.MsgCreateCollection + MsgIssueFT = types.MsgIssueFT + MsgIssueNFT = types.MsgIssueNFT + MsgMintNFT = types.MsgMintNFT + MsgBurnNFT = types.MsgBurnNFT + MsgBurnNFTFrom = types.MsgBurnNFTFrom + MsgMintFT = types.MsgMintFT + MsgBurnFT = types.MsgBurnFT + MsgBurnFTFrom = types.MsgBurnFTFrom + + MsgGrantPermission = types.MsgGrantPermission + MsgRevokePermission = types.MsgRevokePermission + + MsgModify = types.MsgModify + + MsgTransferFT = types.MsgTransferFT + MsgTransferNFT = types.MsgTransferNFT + + MsgTransferFTFrom = types.MsgTransferFTFrom + MsgTransferNFTFrom = types.MsgTransferNFTFrom + + MsgAttach = types.MsgAttach + MsgDetach = types.MsgDetach + + MsgAttachFrom = types.MsgAttachFrom + MsgDetachFrom = types.MsgDetachFrom + + MsgApproveCollection = types.MsgApprove + MsgDisapproveCollection = types.MsgDisapprove + + Permissions = types.Permissions + Permission = types.Permission + + MintNFTParam = types.MintNFTParam + + Keeper = keeper.Keeper +) + +var ( + NewMsgCreateCollection = types.NewMsgCreateCollection + NewMsgIssueFT = types.NewMsgIssueFT + NewMsgIssueNFT = types.NewMsgIssueNFT + NewMsgMintNFT = types.NewMsgMintNFT + NewMsgBurnNFT = types.NewMsgBurnNFT + NewMsgBurnNFTFrom = types.NewMsgBurnNFTFrom + NewMsgBurnFTFrom = types.NewMsgBurnFTFrom + NewMsgMintFT = types.NewMsgMintFT + NewMsgBurnFT = types.NewMsgBurnFT + NewMsgGrantPermission = types.NewMsgGrantPermission + NewMsgRevokePermission = types.NewMsgRevokePermission + NewMsgModify = types.NewMsgModify + NewChangesWithMap = types.NewChangesWithMap + NewMsgTransferFT = types.NewMsgTransferFT + NewMsgTransferNFT = types.NewMsgTransferNFT + NewMsgTransferFTFrom = types.NewMsgTransferFTFrom + NewMsgTransferNFTFrom = types.NewMsgTransferNFTFrom + NewMsgAttach = types.NewMsgAttach + NewMsgDetach = types.NewMsgDetach + NewMsgAttachFrom = types.NewMsgAttachFrom + NewMsgDetachFrom = types.NewMsgDetachFrom + NewMsgApprove = types.NewMsgApprove + NewMsgDisapprove = types.NewMsgDisapprove + NewMintNFTParam = types.NewMintNFTParam + NewCoin = types.NewCoin + NewPermissions = types.NewPermissions + + NewMintPermission = types.NewMintPermission + NewBurnPermission = types.NewBurnPermission + NewIssuePermission = types.NewIssuePermission + NewModifyPermission = types.NewModifyPermission + + ModuleCdc = types.ModuleCdc + RegisterCodec = types.RegisterCodec + + NewKeeper = keeper.NewKeeper + NewQuerier = querier.NewQuerier + + NewMsgEncodeHandler = keeper.NewMsgEncodeHandler + NewQueryEncoder = querier.NewQueryEncoder +) diff --git a/x/collection/client/cli/query.go b/x/collection/client/cli/query.go new file mode 100644 index 0000000000..1b2877c4c0 --- /dev/null +++ b/x/collection/client/cli/query.go @@ -0,0 +1,465 @@ +package cli + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/line/lbm-sdk/v2/x/collection/client/internal/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func GetQueryCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Querying commands for the collection module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + cmd.AddCommand( + GetBalanceCmd(cdc), + GetBalancesCmd(cdc), + GetTokenCmd(cdc), + GetTokensCmd(cdc), + GetTokenTypeCmd(cdc), + GetTokenTypesCmd(cdc), + GetCollectionCmd(cdc), + GetTokenTotalCmd(cdc), + GetTokenCountCmd(cdc), + GetPermsCmd(cdc), + GetParentCmd(cdc), + GetRootCmd(cdc), + GetChildrenCmd(cdc), + GetApproversCmd(cdc), + GetIsApprovedCmd(cdc), + ) + + return cmd +} + +func GetBalanceCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "balance [contract_id] [token_id] [addr]", + Short: "Query balance of the account", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + tokenID := args[1] + addr, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + supply, height, err := retriever.GetAccountBalance(cliCtx, contractID, tokenID, addr) + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(supply) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetBalancesCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "balances [contract_id] [addr]", + Short: "Query balances of the account for each token_id", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + addr, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + coins, height, err := retriever.GetAccountBalances(cliCtx, contractID, addr) + if err != nil { + return err + } + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(coins) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetCollectionCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "collection [contract_id]", + Short: "Query collection", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + collection, height, err := retriever.GetCollection(cliCtx, contractID) + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(collection) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetTokenTypeCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "tokentype [contract_id] [token-type]", + Short: "Query collection token-type with collection contract_id and token-type", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + tokenTypeID := args[1] + tokenType, height, err := retriever.GetTokenType(cliCtx, contractID, tokenTypeID) + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + + return cliCtx.PrintOutput(tokenType) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetTokenTypesCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "tokentypes [contract_id]", + Short: "Query all collection token-types with collection contract_id", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + + tokenTypes, height, err := retriever.GetTokenTypes(cliCtx, contractID) + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(tokenTypes) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetTokenCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "token [contract_id] [token_id]", + Short: "Query collection token with collection contractID and token's token_id", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + tokenID := args[1] + token, height, err := retriever.GetToken(cliCtx, contractID, tokenID) + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + + return cliCtx.PrintOutput(token) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetTokensCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "tokens [contract_id]", + Short: "Query all collection tokens with collection contractID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + var tokens types.Tokens + var height int64 + var err error + + tokenType := viper.GetString(flagTokenType) + if len(tokenType) > 0 { + tokens, height, err = retriever.GetTokensWithTokenType(cliCtx, contractID, tokenType) + } else { + tokens, height, err = retriever.GetTokens(cliCtx, contractID) + } + + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(tokens) + }, + } + cmd.Flags().String(flagTokenType, DefaultTokenType, "get tokens belong to the token-type") + + return flags.GetCommands(cmd)[0] +} + +func GetTokenTotalCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "total [supply|mint|burn] [contract_id] [token_id]", + Short: "Query supply/mint/burn of collection token with contract-id and tokens's token_id.", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + target := args[0] + contractID := args[1] + tokenID := args[2] + + supply, height, err := retriever.GetTotal(cliCtx, contractID, tokenID, target) + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(supply) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetTokenCountCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "count [total|mint|burn] [contract_id] [token_type]", + Short: "Query count of collection tokens with collection contractID and the type_type.", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + target := args[0] + switch target { + case "total": + target = types.QueryNFTCount + case "mint": + target = types.QueryNFTMint + case "burn": + target = types.QueryNFTBurn + default: + return fmt.Errorf("argument is not total, mint, or burn %s", target) + } + + contractID := args[1] + tokenType := args[2] + + supply, height, err := retriever.GetCollectionNFTCount(cliCtx, contractID, tokenType, target) + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(supply) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetPermsCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "perm [addr] [contract_id]", + Short: "Get Permission of the Account", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + addr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + contractID := args[1] + pms, height, err := retriever.GetAccountPermission(cliCtx, contractID, addr) + if err != nil { + return err + } + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(pms) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetParentCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "parent [contract_id] [token-id]", + Short: "Query parent token with contractID and token-id", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + tokenGetter := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + tokenID := args[1] + + if err := tokenGetter.EnsureExists(cliCtx, contractID, tokenID); err != nil { + return err + } + + token, _, err := tokenGetter.GetParent(cliCtx, contractID, tokenID) + if err != nil { + return err + } + + return cliCtx.PrintOutput(token) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetRootCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "root [contract_id] [token-id]", + Short: "Query root token with contractID and token-id", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + tokenGetter := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + tokenID := args[1] + + if err := tokenGetter.EnsureExists(cliCtx, contractID, tokenID); err != nil { + return err + } + + token, _, err := tokenGetter.GetRoot(cliCtx, contractID, tokenID) + if err != nil { + return err + } + + return cliCtx.PrintOutput(token) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetChildrenCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "children [contract_id] [token-id]", + Short: "Query children tokens with contractID and token-id", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + tokenGetter := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + tokenID := args[1] + + if err := tokenGetter.EnsureExists(cliCtx, contractID, tokenID); err != nil { + return err + } + + tokens, _, err := tokenGetter.GetChildren(cliCtx, contractID, tokenID) + if err != nil { + return err + } + + return cliCtx.PrintOutput(tokens) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetApproversCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "approvers [contract_id] [proxy]", + Short: "Query approvers by the proxy", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + + proxy, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + approvers, height, err := retriever.GetApprovers(cliCtx, contractID, proxy) + if err != nil { + return err + } + + return cliCtx.WithHeight(height).PrintOutput(approvers) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetIsApprovedCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "approved [contract_id] [proxy] [approver]", + Short: "Query whether a proxy is approved by approver on a collection", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + + proxy, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + approver, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + approved, height, err := retriever.IsApproved(cliCtx, contractID, proxy, approver) + if err != nil { + return err + } + + return cliCtx.WithHeight(height).PrintOutput(approved) + }, + } + + return flags.GetCommands(cmd)[0] +} diff --git a/x/collection/client/cli/tx.go b/x/collection/client/cli/tx.go new file mode 100644 index 0000000000..659a42eb63 --- /dev/null +++ b/x/collection/client/cli/tx.go @@ -0,0 +1,706 @@ +package cli + +import ( + "bufio" + "errors" + "strings" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" +) + +var ( + flagTotalSupply = "total-supply" + flagDecimals = "decimals" + flagMintable = "mintable" + flagTokenType = "token-type" + flagTokenIndex = "token-index" +) + +const ( + DefaultDecimals = 8 + DefaultTotalSupply = 1 + DefaultTokenType = "" + DefaultTokenIndex = "" +) + +// GetTxCmd returns the transaction commands for this module +func GetTxCmd(cdc *codec.Codec) *cobra.Command { + txCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Token transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + txCmd.AddCommand( + CreateCollectionTxCmd(cdc), + IssueFTTxCmd(cdc), + IssueNFTTxCmd(cdc), + MintFTTxCmd(cdc), + MintNFTTxCmd(cdc), + BurnFTTxCmd(cdc), + BurnFTFromTxCmd(cdc), + BurnNFTTxCmd(cdc), + BurnNFTFromTxCmd(cdc), + TransferFTTxCmd(cdc), + TransferFTFromTxCmd(cdc), + TransferNFTTxCmd(cdc), + TransferNFTFromTxCmd(cdc), + AttachTxCmd(cdc), + AttachFromTxCmd(cdc), + DetachTxCmd(cdc), + DetachFromTxCmd(cdc), + ApproveCollectionTxCmd(cdc), + DisapproveCollectionTxCmd(cdc), + GrantPermTxCmd(cdc), + RevokePermTxCmd(cdc), + ModifyCmd(cdc), + ) + return txCmd +} + +func ModifyCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "modify [owner_address] [contract_id] [field] [new_value]", + Short: "Create and sign a modify tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + field := args[2] + newValue := args[3] + tokenType := viper.GetString(flagTokenType) + tokenIndex := viper.GetString(flagTokenIndex) + + msg := types.NewMsgModify(cliCtx.FromAddress, contractID, tokenType, tokenIndex, + types.NewChanges(types.NewChange(field, newValue))) + err := msg.ValidateBasic() + if err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().String(flagTokenType, DefaultTokenType, "token type") + cmd.Flags().String(flagTokenIndex, DefaultTokenIndex, "token index") + + return flags.PostCommands(cmd)[0] +} + +func CreateCollectionTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "create [from_key_or_address] [name] [meta] [base_img_uri]", + Short: "Create and sign an create collection tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + owner := cliCtx.FromAddress + name := args[1] + meta := args[2] + baseImgURI := args[3] + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgCreateCollection(owner, name, meta, baseImgURI) + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func IssueNFTTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "issue-nft [from_key_or_address] [contract_id] [name] [meta]", + Short: "Create and sign an issue-nft tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + to := cliCtx.FromAddress + contractID := args[1] + name := args[2] + meta := args[3] + + msg := types.NewMsgIssueNFT(to, contractID, name, meta) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return flags.PostCommands(cmd)[0] +} + +func IssueFTTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "issue-ft [from_key_or_address] [contract_id] [to] [name] [meta]", + Short: "Create and sign an issue-ft tx", + Long: ` +[Fungible Token] +linkcli tx collection issue-ft [from_key_or_address] [contract_id] [to] [name] [meta] +--decimals=[decimals] +--mintable=[mintable] +--total-supply=[initial amount of the token] +`, + Args: cobra.ExactArgs(5), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + owner := cliCtx.FromAddress + contractID := args[1] + to, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + name := args[3] + meta := args[4] + supply := viper.GetInt64(flagTotalSupply) + decimals := viper.GetInt64(flagDecimals) + mintable := viper.GetBool(flagMintable) + + if decimals < 0 || decimals > 18 { + return errors.New("invalid decimals. 0 <= decimals <= 18") + } + + msg := types.NewMsgIssueFT(owner, to, contractID, name, meta, sdk.NewInt(supply), sdk.NewInt(decimals), mintable) + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().Int64(flagTotalSupply, DefaultTotalSupply, "total supply") + cmd.Flags().Int64(flagDecimals, DefaultDecimals, "set decimals") + cmd.Flags().Bool(flagMintable, false, "set mintable") + + return flags.PostCommands(cmd)[0] +} + +func MintNFTTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "mint-nft [from_key_or_address] [contract_id] [to] [token_type:name:meta][,[token_type:name:meta]]", + Short: "Create and sign an mint-nft tx", + Long: ` +[NonFungible Token] +linkcli tx collection mint-nft [from_key_or_address] [contract_id] [to] [token_type] [name] [meta] +`, + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + from := cliCtx.FromAddress + to, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + mintNFTParamStrs := strings.Split(args[3], ",") + + mintNFTParams := make([]types.MintNFTParam, len(mintNFTParamStrs)) + for i, mintNFTParamStr := range mintNFTParamStrs { + strs := strings.Split(mintNFTParamStr, ":") + if len(strs) != 3 { + return errors.New("invalid format: ") + } + + mintNFTParams[i] = types.NewMintNFTParam(strs[1], strs[2], strs[0]) + } + + msg := types.NewMsgMintNFT(from, contractID, to, mintNFTParams...) + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().String(flagTokenType, "", "token-type for the nft") + + return flags.PostCommands(cmd)[0] +} + +func BurnNFTTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "burn-nft [from_key_or_address] [contract_id] [token_id]", + Short: "Create and sign an burn-nft tx", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + tokenID := args[2] + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgBurnNFT(cliCtx.GetFromAddress(), contractID, tokenID) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func BurnNFTFromTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "burn-nft-from [proxy_key_or_address] [contract_id] [from_address] [token_id]", + Short: "Create and sign an burn-nft-from tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + from, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + tokenID := args[3] + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgBurnNFTFrom(cliCtx.GetFromAddress(), contractID, from, tokenID) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +// nolint:dupl +func TransferFTTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "transfer-ft [from_key_or_address] [contract_id] [to_address] [amount]", + Short: "Create and sign a tx transferring non-reserved collective fungible tokens", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + to, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + amount, err := types.ParseCoins(args[3]) + if err != nil { + return sdkerrors.Wrap(types.ErrInvalidAmount, args[3]) + } + + msg := types.NewMsgTransferFT(cliCtx.GetFromAddress(), contractID, to, amount...) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd = flags.PostCommands(cmd)[0] + + return cmd +} + +// nolint:dupl +func TransferNFTTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "transfer-nft [from_key_or_address] [contract_id] [to_address] [token_id][,[token_id]]", + Short: "Create and sign a tx transferring a collective non-fungible token", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + to, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + tokenIDs := strings.Split(args[3], ",") + + msg := types.NewMsgTransferNFT(cliCtx.GetFromAddress(), contractID, to, tokenIDs...) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func TransferFTFromTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "transfer-ft-from [proxy_key_or_address] [contract_id] [from_address] [to_address] [amount]", + Short: "Create and sign a tx transferring non-reserved collective fungible tokens by approved proxy", + Args: cobra.ExactArgs(5), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + from, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + to, err := sdk.AccAddressFromBech32(args[3]) + if err != nil { + return err + } + + amount, err := types.ParseCoins(args[4]) + if err != nil { + return sdkerrors.Wrap(types.ErrInvalidAmount, args[4]) + } + + msg := types.NewMsgTransferFTFrom(cliCtx.GetFromAddress(), contractID, from, to, amount...) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd = flags.PostCommands(cmd)[0] + + return cmd +} + +// nolint:dupl +func TransferNFTFromTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "transfer-nft-from [proxy_key_or_address] [contract_id] [from_address] [to_address] [token_id]", + Short: "Create and sign a tx transferring a collective non-fungible token by approved proxy", + Args: cobra.ExactArgs(5), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + from, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + to, err := sdk.AccAddressFromBech32(args[3]) + if err != nil { + return err + } + + tokenIDs := strings.Split(args[4], ",") + + msg := types.NewMsgTransferNFTFrom(cliCtx.GetFromAddress(), contractID, from, to, tokenIDs...) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func AttachTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "attach [from_key_or_address] [contract_id] [to_token_id] [token_id]", + Short: "Create and sign a tx attaching a token to other", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + msg := types.NewMsgAttach(cliCtx.GetFromAddress(), args[1], args[2], args[3]) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func DetachTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "detach [from_key_or_address] [contract_id] [token_id]", + Short: "Create and sign a tx detaching a token", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + msg := types.NewMsgDetach(cliCtx.GetFromAddress(), args[1], args[2]) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +// nolint:dupl +func MintFTTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "mint-ft [from_key_or_address] [contract_id] [to] [amount]", + Short: "Create and sign a mint token tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + to, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + amount, err := types.ParseCoins(args[3]) + if err != nil { + return sdkerrors.Wrap(types.ErrInvalidAmount, args[3]) + } + + msg := types.NewMsgMintFT(cliCtx.GetFromAddress(), contractID, to, amount...) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func BurnFTTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "burn-ft [from_key_or_address] [contract_id] [token-id] [amount]", + Short: "Create and sign a mint token tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + contractID := args[1] + tokenID := args[2] + if err := types.ValidateDenom(tokenID); err != nil { + return errors.New("invalid tokenID") + } + amount, ok := sdk.NewIntFromString(args[3]) + if !ok { + return errors.New("invalid amount") + } + + msg := types.NewMsgBurnFT(cliCtx.GetFromAddress(), contractID, types.NewCoin(tokenID, amount)) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +// nolint:dupl +func BurnFTFromTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "burn-ft-from [proxy_key_or_address] [contract_id] [from_address] [token-id] [amount]", + Short: "Create and sign a mint token tx", + Args: cobra.ExactArgs(5), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + contractID := args[1] + from, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + tokenID := args[3] + if err := types.ValidateDenom(tokenID); err != nil { + return errors.New("invalid tokenID") + } + amount, ok := sdk.NewIntFromString(args[4]) + if !ok { + return errors.New("invalid amount") + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgBurnFTFrom(cliCtx.GetFromAddress(), contractID, from, types.NewCoin(tokenID, amount)) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func AttachFromTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "attach-from [proxy_key_or_address] [contract_id] [from_address] [to_token_id] [token_id]", + Short: "Create and sign a tx attaching a token to other by approved proxy", + Args: cobra.ExactArgs(5), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + from, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + msg := types.NewMsgAttachFrom(cliCtx.GetFromAddress(), contractID, from, args[3], args[4]) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +// nolint:dupl +func DetachFromTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "detach-from [proxy_key_or_address] [contract_id] [from_address] [token_id]", + Short: "Create and sign a tx detaching a token by approved proxy", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + from, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + msg := types.NewMsgDetachFrom(cliCtx.GetFromAddress(), contractID, from, args[3]) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +// nolint:dupl +func ApproveCollectionTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "approve [approver_key_or_address] [contract_id] [proxy_address]", + Short: "Create and sign a tx approve all token operations of a collection to a proxy", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + proxy, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + msg := types.NewMsgApprove(cliCtx.GetFromAddress(), contractID, proxy) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +// nolint:dupl +func DisapproveCollectionTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "disapprove [approver_key_or_address] [contract_id] [proxy_address]", + Short: "Create and sign a tx disapprove all token operations of a collection to a proxy", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + proxy, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + msg := types.NewMsgDisapprove(cliCtx.GetFromAddress(), contractID, proxy) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func GrantPermTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "grant [from_key_or_address] [contract_id] [to] [action]", + Short: "Create and sign a grant permission for token tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + to, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + perm := types.Permission(args[3]) + if !perm.Validate() { + return errors.New("permission invalid") + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgGrantPermission(cliCtx.GetFromAddress(), contractID, to, perm) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func RevokePermTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "revoke [from_key_or_address] [contract_id] [action]", + Short: "Create and sign a revoke permission for token tx", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + perm := types.Permission(args[2]) + if !perm.Validate() { + return errors.New("permission invalid") + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgRevokePermission(cliCtx.GetFromAddress(), contractID, perm) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} diff --git a/x/collection/client/internal/types/retriever.go b/x/collection/client/internal/types/retriever.go new file mode 100644 index 0000000000..f2e75242f1 --- /dev/null +++ b/x/collection/client/internal/types/retriever.go @@ -0,0 +1,316 @@ +package types + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type Retriever struct { + querier types.NodeQuerier +} + +func NewRetriever(querier types.NodeQuerier) Retriever { + return Retriever{querier: querier} +} + +func (r Retriever) query(path, contractID string, data []byte) ([]byte, int64, error) { + return r.querier.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", types.QuerierRoute, path, contractID), data) +} + +func (r Retriever) GetAccountBalance(ctx context.CLIContext, contractID, tokenID string, addr sdk.AccAddress) (sdk.Int, int64, error) { + var balance sdk.Int + bs, err := ctx.Codec.MarshalJSON(types.NewQueryTokenIDAccAddressParams(tokenID, addr)) + if err != nil { + return balance, 0, err + } + + res, height, err := r.query(types.QueryBalance, contractID, bs) + if err != nil { + return balance, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &balance); err != nil { + return balance, height, err + } + + return balance, height, nil +} + +func (r Retriever) GetAccountBalances(ctx context.CLIContext, contractID string, addr sdk.AccAddress) (types.Coins, int64, error) { + var coins types.Coins + bs, err := ctx.Codec.MarshalJSON(types.NewQueryAccAddressParams(addr)) + if err != nil { + return coins, 0, err + } + + res, height, err := r.query(types.QueryBalances, contractID, bs) + + if err != nil { + return coins, height, err + } + if err := ctx.Codec.UnmarshalJSON(res, &coins); err != nil { + return coins, height, err + } + return coins, height, nil +} + +func (r Retriever) GetAccountPermission(ctx context.CLIContext, contractID string, addr sdk.AccAddress) (types.Permissions, int64, error) { + var pms types.Permissions + bs, err := ctx.Codec.MarshalJSON(types.NewQueryAccAddressParams(addr)) + if err != nil { + return pms, 0, err + } + + res, height, err := r.query(types.QueryPerms, contractID, bs) + if err != nil { + return pms, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &pms); err != nil { + return pms, height, err + } + + return pms, height, nil +} + +func (r Retriever) GetCollection(ctx context.CLIContext, contractID string) (types.BaseCollection, int64, error) { + var collection types.BaseCollection + res, height, err := r.query(types.QueryCollections, contractID, nil) + if err != nil { + return collection, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &collection); err != nil { + return collection, height, err + } + + return collection, height, nil +} + +func (r Retriever) GetCollectionNFTCount(ctx context.CLIContext, contractID, tokenID, target string) (sdk.Int, int64, error) { + var nftcount sdk.Int + bs, err := ctx.Codec.MarshalJSON(types.NewQueryTokenIDParams(tokenID)) + if err != nil { + return nftcount, 0, err + } + if target != types.QueryNFTCount && target != types.QueryNFTMint && target != types.QueryNFTBurn { + return nftcount, 0, fmt.Errorf("invalid target : %s", target) + } + + res, height, err := r.query(target, contractID, bs) + if err != nil { + return nftcount, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &nftcount); err != nil { + return nftcount, height, err + } + + return nftcount, height, nil +} + +func (r Retriever) GetTotal(ctx context.CLIContext, contractID, tokenID, target string) (sdk.Int, int64, error) { + var supply sdk.Int + bs, err := ctx.Codec.MarshalJSON(types.NewQueryTokenIDParams(tokenID)) + if err != nil { + return supply, 0, err + } + + res, height, err := r.query(target, contractID, bs) + if err != nil { + return supply, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &supply); err != nil { + return supply, height, err + } + + return supply, height, nil +} + +func (r Retriever) GetToken(ctx context.CLIContext, contractID, tokenID string) (types.Token, int64, error) { + var token types.Token + bs, err := types.ModuleCdc.MarshalJSON(types.NewQueryTokenIDParams(tokenID)) + if err != nil { + return token, 0, err + } + + res, height, err := r.query(types.QueryTokens, contractID, bs) + if err != nil { + return token, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &token); err != nil { + return token, height, err + } + return token, height, nil +} + +func (r Retriever) GetTokens(ctx context.CLIContext, contractID string) (types.Tokens, int64, error) { + var tokens types.Tokens + res, height, err := r.query(types.QueryTokens, contractID, nil) + if err != nil { + return tokens, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &tokens); err != nil { + return tokens, height, err + } + return tokens, height, nil +} + +func (r Retriever) GetTokensWithTokenType(ctx context.CLIContext, contractID string, tokenType string) (types.Tokens, int64, error) { + var tokens types.Tokens + bs, err := types.ModuleCdc.MarshalJSON(types.NewQueryTokenTypeParams(tokenType)) + + if err != nil { + return tokens, 0, err + } + if err = types.ValidateTokenType(tokenType); err != nil { + return tokens, 0, err + } + res, height, err := r.query(types.QueryTokensWithTokenType, contractID, bs) + if err != nil { + return tokens, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &tokens); err != nil { + return tokens, height, err + } + return tokens, height, nil +} + +func (r Retriever) GetTokenType(ctx context.CLIContext, contractID, tokenTypeID string) (types.TokenType, int64, error) { + var tokenType types.TokenType + bs, err := types.ModuleCdc.MarshalJSON(types.NewQueryTokenIDParams(tokenTypeID)) + if err != nil { + return tokenType, 0, err + } + + res, height, err := r.query(types.QueryTokenTypes, contractID, bs) + if err != nil { + return tokenType, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &tokenType); err != nil { + return tokenType, height, err + } + return tokenType, height, nil +} + +func (r Retriever) GetTokenTypes(ctx context.CLIContext, contractID string) (types.TokenTypes, int64, error) { + var tokenTypes types.TokenTypes + + res, height, err := r.query(types.QueryTokenTypes, contractID, nil) + if err != nil { + return tokenTypes, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &tokenTypes); err != nil { + return tokenTypes, height, err + } + return tokenTypes, height, nil +} + +func (r Retriever) GetApprovers(ctx context.CLIContext, contractID string, proxy sdk.AccAddress) (accAdds []sdk.AccAddress, height int64, err error) { + bs, err := ctx.Codec.MarshalJSON(types.NewQueryApproverParams(proxy)) + if err != nil { + return accAdds, 0, err + } + res, height, err := r.query(types.QueryApprovers, contractID, bs) + if err != nil { + return accAdds, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &accAdds); err != nil { + return accAdds, height, err + } + + return accAdds, height, nil +} + +func (r Retriever) IsApproved(ctx context.CLIContext, contractID string, proxy sdk.AccAddress, approver sdk.AccAddress) (approved bool, height int64, err error) { + bs, err := types.ModuleCdc.MarshalJSON(types.NewQueryIsApprovedParams(proxy, approver)) + if err != nil { + return false, 0, err + } + + res, height, err := r.query(types.QueryIsApproved, contractID, bs) + if err != nil { + return false, 0, err + } + + err = ctx.Codec.UnmarshalJSON(res, &approved) + if err != nil { + return false, 0, err + } + + return approved, height, nil +} + +func (r Retriever) EnsureExists(ctx context.CLIContext, contractID, tokenID string) error { + if _, _, err := r.GetToken(ctx, contractID, tokenID); err != nil { + return err + } + return nil +} + +func (r Retriever) GetParent(ctx context.CLIContext, contractID, tokenID string) (types.Token, int64, error) { + bs, err := types.ModuleCdc.MarshalJSON(types.NewQueryTokenIDParams(tokenID)) + if err != nil { + return nil, 0, err + } + + res, height, err := r.query(types.QueryParent, contractID, bs) + if res == nil { + return nil, 0, err + } + + var token types.Token + if err := ctx.Codec.UnmarshalJSON(res, &token); err != nil { + return nil, 0, err + } + + return token, height, nil +} + +func (r Retriever) GetRoot(ctx context.CLIContext, contractID, tokenID string) (types.Token, int64, error) { + bs, err := types.ModuleCdc.MarshalJSON(types.NewQueryTokenIDParams(tokenID)) + if err != nil { + return nil, 0, err + } + + res, height, err := r.query(types.QueryRoot, contractID, bs) + if res == nil { + return nil, 0, err + } + + var token types.Token + if err := ctx.Codec.UnmarshalJSON(res, &token); err != nil { + return nil, 0, err + } + + return token, height, nil +} + +func (r Retriever) GetChildren(ctx context.CLIContext, contractID, tokenID string) (types.Tokens, int64, error) { + bs, err := types.ModuleCdc.MarshalJSON(types.NewQueryTokenIDParams(tokenID)) + if err != nil { + return nil, 0, err + } + + res, height, err := r.query(types.QueryChildren, contractID, bs) + if res == nil { + return nil, 0, err + } + + var tokens types.Tokens + if err := ctx.Codec.UnmarshalJSON(res, &tokens); err != nil { + return nil, 0, err + } + + return tokens, height, nil +} diff --git a/x/collection/client/rest/query.go b/x/collection/client/rest/query.go new file mode 100644 index 0000000000..f3269bd5f8 --- /dev/null +++ b/x/collection/client/rest/query.go @@ -0,0 +1,524 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/line/lbm-sdk/v2/x/collection/client/internal/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/types/rest" +) + +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc("/collection/{contract_id}/fts/{token_id}/supply", QueryTokenTotalRequestHandlerFn(cliCtx, types.QuerySupply)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/fts/{token_id}/mint", QueryTokenTotalRequestHandlerFn(cliCtx, types.QueryMint)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/fts/{token_id}/burn", QueryTokenTotalRequestHandlerFn(cliCtx, types.QueryBurn)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/fts/{token_id}", QueryTokenRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/nfts/{token_id}/parent", QueryParentRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/nfts/{token_id}/root", QueryRootRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/nfts/{token_id}/children", QueryChildrenRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/nfts/{token_id}", QueryTokenRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/tokens", QueryTokensRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/tokentypes/{token_type}/tokens", QueryTokensWithTokenTypeRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/tokentypes/{token_type}/count", QueryCountRequestHandlerFn(cliCtx, types.QueryNFTCount)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/tokentypes/{token_type}/mint", QueryCountRequestHandlerFn(cliCtx, types.QueryNFTMint)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/tokentypes/{token_type}/burn", QueryCountRequestHandlerFn(cliCtx, types.QueryNFTBurn)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/tokentypes/{token_type}", QueryTokenTypeRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/tokentypes", QueryTokenTypesRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/accounts/{address}/permissions", QueryPermRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/accounts/{address}/proxies/{approver}", QueryIsApprovedRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/accounts/{address}/balances", QueryBalancesRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/accounts/{address}/approvers", QueryApproversRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/accounts/{address}/balances/{token_id}", QueryBalanceRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/collection/{contract_id}/collection", QueryCollectionRequestHandlerFn(cliCtx)).Methods("GET") +} + +func QueryBalancesRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + addr, err := sdk.AccAddressFromBech32(vars["address"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("addr[%s] cannot parsed: %s", vars["address"], err)) + return + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + coins, height, err := retriever.GetAccountBalances(cliCtx, contractID, addr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, coins) + } +} + +func QueryBalanceRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + tokenID := vars["token_id"] + addr, err := sdk.AccAddressFromBech32(vars["address"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("addr[%s] cannot parsed: %s", vars["address"], err)) + return + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + supply, height, err := retriever.GetAccountBalance(cliCtx, contractID, tokenID, addr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, supply) + } +} + +func QueryTokenTypeRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + tokenTypeID := vars["token_type"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + tokenType, height, err := retriever.GetTokenType(cliCtx, contractID, tokenTypeID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, tokenType) + } +} + +func QueryTokenTypesRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + tokenTypes, height, err := retriever.GetTokenTypes(cliCtx, contractID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, tokenTypes) + } +} + +func QueryTokenRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + tokenID := vars["token_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + token, height, err := retriever.GetToken(cliCtx, contractID, tokenID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, token) + } +} + +func QueryTokensRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + tokens, height, err := retriever.GetTokens(cliCtx, contractID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, tokens) + } +} +func QueryCollectionRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + collection, height, err := retriever.GetCollection(cliCtx, contractID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, collection) + } +} + +func QueryTokenTotalRequestHandlerFn(cliCtx context.CLIContext, target string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + tokenID := vars["token_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + supply, height, err := retriever.GetTotal(cliCtx, contractID, tokenID, target) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, supply) + } +} + +func QueryTokensWithTokenTypeRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + tokenType := vars["token_type"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + tokens, height, err := retriever.GetTokensWithTokenType(cliCtx, contractID, tokenType) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, tokens) + } +} + +func QueryCountRequestHandlerFn(cliCtx context.CLIContext, target string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + tokenID := vars["token_type"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + nftcount, height, err := retriever.GetCollectionNFTCount(cliCtx, contractID, tokenID, target) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, nftcount) + } +} + +func QueryPermRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + addr, err := sdk.AccAddressFromBech32(vars["address"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("address cannot parsed: %s", err)) + return + } + contractID := vars["contract_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + pms, height, err := retriever.GetAccountPermission(cliCtx, contractID, addr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, pms) + } +} + +// nolint:dupl +func QueryParentRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + tokenID := vars["token_id"] + + if len(contractID) == 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, "contract_id absent") + return + } + + if len(tokenID) == 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, "token_id absent") + return + } + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + tokenGetter := clienttypes.NewRetriever(cliCtx) + + if err := tokenGetter.EnsureExists(cliCtx, contractID, tokenID); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + token, height, err := tokenGetter.GetParent(cliCtx, contractID, tokenID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, token) + } +} + +// nolint:dupl +func QueryRootRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + tokenID := vars["token_id"] + + if len(contractID) == 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, "contract_id absent") + return + } + + if len(tokenID) == 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, "token_id absent") + return + } + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + tokenGetter := clienttypes.NewRetriever(cliCtx) + + if err := tokenGetter.EnsureExists(cliCtx, contractID, tokenID); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + token, height, err := tokenGetter.GetRoot(cliCtx, contractID, tokenID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, token) + } +} + +// nolint:dupl +func QueryChildrenRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + tokenID := vars["token_id"] + + if len(contractID) == 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, "contract_id absent") + return + } + + if len(tokenID) == 0 { + rest.WriteErrorResponse(w, http.StatusBadRequest, "token_id absent") + return + } + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + tokenGetter := clienttypes.NewRetriever(cliCtx) + + if err := tokenGetter.EnsureExists(cliCtx, contractID, tokenID); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + tokens, height, err := tokenGetter.GetChildren(cliCtx, contractID, tokenID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, tokens) + } +} + +func QueryApproversRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + + proxy, err := sdk.AccAddressFromBech32(vars["address"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("approver[%s] cannot parsed: %s", proxy.String(), err)) + return + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + approvers, height, err := retriever.GetApprovers(cliCtx, contractID, proxy) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, approvers) + } +} + +func QueryIsApprovedRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + + proxy, err := sdk.AccAddressFromBech32(vars["address"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("proxy[%s] cannot parsed: %s", proxy.String(), err)) + return + } + + approver, err := sdk.AccAddressFromBech32(vars["approver"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("approver[%s] cannot parsed: %s", approver.String(), err)) + return + } + + contractID := vars["contract_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + approved, height, err := retriever.IsApproved(cliCtx, contractID, proxy, approver) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, approved) + } +} diff --git a/x/collection/genesis.go b/x/collection/genesis.go new file mode 100644 index 0000000000..499bd99852 --- /dev/null +++ b/x/collection/genesis.go @@ -0,0 +1,42 @@ +package collection + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type GenesisState struct { + Params types.Params `json:"params"` + Tokens []Token `json:"tokens"` + // TODO: approvals +} + +func NewGenesisState(params types.Params, tokens []Token) GenesisState { + return GenesisState{ + Params: params, + Tokens: tokens, + } +} + +func DefaultGenesisState() GenesisState { + return NewGenesisState(types.DefaultParams(), nil) +} + +func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { + // TODO: fill it with permission + keeper.SetParams(ctx, data.Params) +} + +func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + params := keeper.GetParams(ctx) + return NewGenesisState(params, nil) +} + +func ValidateGenesis(data GenesisState) error { + if err := data.Params.Validate(); err != nil { + return err + } + + return nil +} diff --git a/x/collection/internal/handler/burn.go b/x/collection/internal/handler/burn.go new file mode 100644 index 0000000000..6a664b3136 --- /dev/null +++ b/x/collection/internal/handler/burn.go @@ -0,0 +1,71 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func handleMsgBurnNFT(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgBurnNFT) (*sdk.Result, error) { + err := keeper.BurnNFT(ctx, msg.From, msg.TokenIDs...) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgBurnNFTFrom(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgBurnNFTFrom) (*sdk.Result, error) { + err := keeper.BurnNFTFrom(ctx, msg.Proxy, msg.From, msg.TokenIDs...) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Proxy.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgBurnFT(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgBurnFT) (*sdk.Result, error) { + err := keeper.BurnFT(ctx, msg.From, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgBurnFTFrom(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgBurnFTFrom) (*sdk.Result, error) { + err := keeper.BurnFTFrom(ctx, msg.Proxy, msg.From, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Proxy.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/collection/internal/handler/burn_test.go b/x/collection/internal/handler/burn_test.go new file mode 100644 index 0000000000..9aed6f9ada --- /dev/null +++ b/x/collection/internal/handler/burn_test.go @@ -0,0 +1,209 @@ +package handler + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" +) + +func TestHandleMsgBurnFT(t *testing.T) { + ctx, h, contractID := prepareFT(t) + + { + // invalid user + burnMsg := types.NewMsgBurnFT(addr2, contractID, types.NewCoin("0000000100000000", sdk.NewInt(100))) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn non-exist token + burnMsg := types.NewMsgBurnFT(addr1, contractID, types.NewCoin("0000000200000000", sdk.NewInt(100))) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn tokens over the being supplied + burnMsg := types.NewMsgBurnFT(addr1, contractID, types.NewCoin("0000000100000000", sdk.NewInt(1001))) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn tokens with invalid contractID + burnMsg := types.NewMsgBurnFT(addr1, "abcd11234", types.NewCoin("0000000100000000", sdk.NewInt(1000))) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // success case + burnMsg := types.NewMsgBurnFT(addr1, contractID, types.NewCoin("0000000100000000", sdk.NewInt(100))) + res, err := h(ctx, burnMsg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("burn_ft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("burn_ft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("burn_ft", sdk.NewAttribute("amount", types.NewCoins(types.NewCoin("0000000100000000", sdk.NewInt(100))).String())), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleMsgBurnFTFrom(t *testing.T) { + ctx, h, contractID := prepareFT(t) + + sendMsg := types.NewMsgTransferFT(addr1, contractID, addr2, types.NewCoin("0000000100000000", sdk.NewInt(1000))) + _, err := h(ctx, sendMsg) + require.NoError(t, err) + + { + // not approved + burnMsg := types.NewMsgBurnFTFrom(addr1, contractID, addr2, types.NewCoin("0000000100000000", sdk.NewInt(100))) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + + approve(t, addr2, addr1, contractID, ctx, h) + { + // invalid user + burnMsg := types.NewMsgBurnFTFrom(addr2, contractID, addr2, types.NewCoin("0000000100000000", sdk.NewInt(100))) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn non-exist token + burnMsg := types.NewMsgBurnFTFrom(addr1, contractID, addr2, types.NewCoin("0000000200000000", sdk.NewInt(100))) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn tokens over the being supplied + burnMsg := types.NewMsgBurnFTFrom(addr1, contractID, addr2, types.NewCoin("0000000100000000", sdk.NewInt(1001))) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn tokens with invalid contractID + burnMsg := types.NewMsgBurnFTFrom(addr1, "abcd11234", addr2, types.NewCoin("0000000100000000", sdk.NewInt(1000))) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // success case + burnMsg := types.NewMsgBurnFTFrom(addr1, contractID, addr2, types.NewCoin("0000000100000000", sdk.NewInt(100))) + res, err := h(ctx, burnMsg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("burn_ft_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("burn_ft_from", sdk.NewAttribute("proxy", addr1.String())), + sdk.NewEvent("burn_ft_from", sdk.NewAttribute("from", addr2.String())), + sdk.NewEvent("burn_ft_from", sdk.NewAttribute("amount", types.NewCoins(types.NewCoin("0000000100000000", sdk.NewInt(100))).String())), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleMsgBurnNFT(t *testing.T) { + ctx, h, contractID := prepareNFT(t, addr1) + + { + // invalid user + burnMsg := types.NewMsgBurnNFT(addr2, contractID, "1000000100000001") + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn non-exist token + burnMsg := types.NewMsgBurnNFT(addr1, contractID, "1000000200000001") + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn tokens with invalid contractID + burnMsg := types.NewMsgBurnNFT(addr1, "abcd11234", "1000000100000001") + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // success case + burnMsg := types.NewMsgBurnNFT(addr1, contractID, "1000000100000001") + res, err := h(ctx, burnMsg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("burn_nft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("burn_nft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("burn_nft", sdk.NewAttribute("token_id", "1000000100000001")), + sdk.NewEvent("operation_burn_nft", sdk.NewAttribute("token_id", "1000000100000001")), + } + verifyEventFunc(t, e, res.Events) + } + { + // burn already burned + burnMsg := types.NewMsgBurnNFT(addr1, contractID, "1000000100000001") + _, err := h(ctx, burnMsg) + require.Error(t, err) + } +} + +func TestHandleMsgBurnNFTFrom(t *testing.T) { + ctx, h, contractID := prepareNFT(t, addr2) + + { + // not approved + burnMsg := types.NewMsgBurnNFTFrom(addr1, contractID, addr2, "1000000100000001") + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + + approve(t, addr2, addr1, contractID, ctx, h) + { + // invalid user + burnMsg := types.NewMsgBurnNFTFrom(addr2, contractID, addr2, "1000000100000001") + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn non-exist token + burnMsg := types.NewMsgBurnNFTFrom(addr1, contractID, addr2, "1000000200000001") + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // burn tokens with invalid contractID + burnMsg := types.NewMsgBurnNFTFrom(addr1, "abcd11234", addr2, "1000000100000001") + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + { + // success case + burnMsg := types.NewMsgBurnNFTFrom(addr1, contractID, addr2, "1000000100000001") + res, err := h(ctx, burnMsg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("burn_nft_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("burn_nft_from", sdk.NewAttribute("proxy", addr1.String())), + sdk.NewEvent("burn_nft_from", sdk.NewAttribute("from", addr2.String())), + sdk.NewEvent("burn_nft_from", sdk.NewAttribute("token_id", "1000000100000001")), + sdk.NewEvent("operation_burn_nft", sdk.NewAttribute("token_id", "1000000100000001")), + } + verifyEventFunc(t, e, res.Events) + } + { + // burn already burned + burnMsg := types.NewMsgBurnNFTFrom(addr1, contractID, addr2, "1000000100000001") + _, err := h(ctx, burnMsg) + require.Error(t, err) + } +} diff --git a/x/collection/internal/handler/composable.go b/x/collection/internal/handler/composable.go new file mode 100644 index 0000000000..43629f55cc --- /dev/null +++ b/x/collection/internal/handler/composable.go @@ -0,0 +1,75 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func handleMsgAttach(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgAttach) (*sdk.Result, error) { + err := keeper.Attach(ctx, msg.From, msg.ToTokenID, msg.TokenID) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgDetach(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgDetach) (*sdk.Result, error) { + err := keeper.Detach(ctx, msg.From, msg.TokenID) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgAttachFrom(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgAttachFrom) (*sdk.Result, error) { + err := keeper.AttachFrom(ctx, msg.Proxy, msg.From, msg.ToTokenID, msg.TokenID) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Proxy.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgDetachFrom(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgDetachFrom) (*sdk.Result, error) { + err := keeper.DetachFrom(ctx, msg.Proxy, msg.From, msg.TokenID) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Proxy.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/collection/internal/handler/composable_test.go b/x/collection/internal/handler/composable_test.go new file mode 100644 index 0000000000..84598e6131 --- /dev/null +++ b/x/collection/internal/handler/composable_test.go @@ -0,0 +1,251 @@ +package handler + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestHandleAttach(t *testing.T) { + ctx, h, contractID := prepareNFT(t, addr1) + + { + msg := types.NewMsgAttach(addr1, contractID, defaultTokenID1, defaultTokenID2) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("attach", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("attach", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("attach", sdk.NewAttribute("to_token_id", defaultTokenID1)), + sdk.NewEvent("attach", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("attach", sdk.NewAttribute("old_root_token_id", defaultTokenID2)), + sdk.NewEvent("attach", sdk.NewAttribute("new_root_token_id", defaultTokenID1)), + sdk.NewEvent("operation_root_changed", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleAttachFrom(t *testing.T) { + ctx, h, contractID := prepareNFT(t, addr2) + approve(t, addr2, addr1, contractID, ctx, h) + { + msg := types.NewMsgAttachFrom(addr1, contractID, addr2, defaultTokenID1, defaultTokenID2) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("attach_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("attach_from", sdk.NewAttribute("proxy", addr1.String())), + sdk.NewEvent("attach_from", sdk.NewAttribute("from", addr2.String())), + sdk.NewEvent("attach_from", sdk.NewAttribute("to_token_id", defaultTokenID1)), + sdk.NewEvent("attach_from", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("attach_from", sdk.NewAttribute("old_root_token_id", defaultTokenID2)), + sdk.NewEvent("attach_from", sdk.NewAttribute("new_root_token_id", defaultTokenID1)), + sdk.NewEvent("operation_root_changed", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res.Events) + } +} + +func prepareForDetaching(t *testing.T, mintTo sdk.AccAddress) (sdk.Context, sdk.Handler, string) { + ctx, h, contractID := prepareNFT(t, mintTo) + + msg := types.NewMsgAttach(mintTo, contractID, defaultTokenID1, defaultTokenID2) + _, err := h(ctx, msg) + require.NoError(t, err) + return ctx, h, contractID +} + +func TestHandleDetach(t *testing.T) { + ctx, h, contractID := prepareForDetaching(t, addr1) + + { + msg := types.NewMsgDetach(addr1, contractID, defaultTokenID2) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("detach", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("detach", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("detach", sdk.NewAttribute("from_token_id", defaultTokenID1)), + sdk.NewEvent("detach", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("detach", sdk.NewAttribute("old_root_token_id", defaultTokenID1)), + sdk.NewEvent("detach", sdk.NewAttribute("new_root_token_id", defaultTokenID2)), + sdk.NewEvent("operation_root_changed", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleDetachFrom(t *testing.T) { + ctx, h, contractID := prepareForDetaching(t, addr2) + approve(t, addr2, addr1, contractID, ctx, h) + { + msg := types.NewMsgDetachFrom(addr1, contractID, addr2, defaultTokenID2) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("detach_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("detach_from", sdk.NewAttribute("proxy", addr1.String())), + sdk.NewEvent("detach_from", sdk.NewAttribute("from", addr2.String())), + sdk.NewEvent("detach_from", sdk.NewAttribute("from_token_id", defaultTokenID1)), + sdk.NewEvent("detach_from", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("detach_from", sdk.NewAttribute("old_root_token_id", defaultTokenID1)), + sdk.NewEvent("detach_from", sdk.NewAttribute("new_root_token_id", defaultTokenID2)), + sdk.NewEvent("operation_root_changed", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res.Events) + } +} + +func attach(t *testing.T, ctx sdk.Context, h sdk.Handler, contractID string) { + msg := types.NewMsgAttach(addr1, contractID, defaultTokenID1, defaultTokenID2) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("attach", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("attach", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("attach", sdk.NewAttribute("to_token_id", defaultTokenID1)), + sdk.NewEvent("attach", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("attach", sdk.NewAttribute("old_root_token_id", defaultTokenID2)), + sdk.NewEvent("attach", sdk.NewAttribute("new_root_token_id", defaultTokenID1)), + sdk.NewEvent("operation_root_changed", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res.Events) +} + +func TestHandleAttachDetach(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + createMsg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, createMsg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err = h(ctx, msg) + require.NoError(t, err) + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType) + msg2 := types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + msg2 = types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + } + + attach(t, ctx, h, contractID) + + { + msg2 := types.NewMsgDetach(addr1, contractID, defaultTokenID2) + res2, err2 := h(ctx, msg2) + require.NoError(t, err2) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("detach", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("detach", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("detach", sdk.NewAttribute("from_token_id", defaultTokenID1)), + sdk.NewEvent("detach", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("detach", sdk.NewAttribute("old_root_token_id", defaultTokenID1)), + sdk.NewEvent("detach", sdk.NewAttribute("new_root_token_id", defaultTokenID2)), + sdk.NewEvent("operation_root_changed", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res2.Events) + } + + // Attach again + attach(t, ctx, h, contractID) + + // Burn token + { + msg := types.NewMsgBurnNFT(addr1, contractID, defaultTokenID1) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("burn_nft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("burn_nft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("burn_nft", sdk.NewAttribute("token_id", defaultTokenID1)), + sdk.NewEvent("operation_burn_nft", sdk.NewAttribute("token_id", defaultTokenID1)), + sdk.NewEvent("operation_burn_nft", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleAttachFromDetachFromScenario(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + createMsg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, createMsg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err = h(ctx, msg) + require.NoError(t, err) + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType) + msg2 := types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + msg2 = types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + msg3 := types.NewMsgApprove(addr1, contractID, addr2) + _, err = h(ctx, msg3) + require.NoError(t, err) + } + + msg := types.NewMsgAttachFrom(addr2, contractID, addr1, defaultTokenID1, defaultTokenID2) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("attach_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("attach_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("attach_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("attach_from", sdk.NewAttribute("to_token_id", defaultTokenID1)), + sdk.NewEvent("attach_from", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("attach_from", sdk.NewAttribute("old_root_token_id", defaultTokenID2)), + sdk.NewEvent("attach_from", sdk.NewAttribute("new_root_token_id", defaultTokenID1)), + sdk.NewEvent("operation_root_changed", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res.Events) + + msg2 := types.NewMsgDetachFrom(addr2, contractID, addr1, defaultTokenID2) + res2, err2 := h(ctx, msg2) + require.NoError(t, err2) + e = sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("detach_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("detach_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("detach_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("detach_from", sdk.NewAttribute("from_token_id", defaultTokenID1)), + sdk.NewEvent("detach_from", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("detach_from", sdk.NewAttribute("old_root_token_id", defaultTokenID1)), + sdk.NewEvent("detach_from", sdk.NewAttribute("new_root_token_id", defaultTokenID2)), + sdk.NewEvent("operation_root_changed", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res2.Events) +} diff --git a/x/collection/internal/handler/create.go b/x/collection/internal/handler/create.go new file mode 100644 index 0000000000..9575256fd0 --- /dev/null +++ b/x/collection/internal/handler/create.go @@ -0,0 +1,29 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" +) + +func handleMsgCreateCollection(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgCreateCollection) (*sdk.Result, error) { + contractI := ctx.Context().Value(contract.CtxKey{}) + if contractI == nil { + panic("contract id does not set") + } + collection := types.NewCollection(contractI.(string), msg.Name, msg.Meta, msg.BaseImgURI) + err := keeper.CreateCollection(ctx, collection, msg.Owner) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/collection/internal/handler/create_test.go b/x/collection/internal/handler/create_test.go new file mode 100644 index 0000000000..6645b033e0 --- /dev/null +++ b/x/collection/internal/handler/create_test.go @@ -0,0 +1,38 @@ +package handler + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + contractID = "9be17165" +) + +func TestHandleMsgCreateCollection(t *testing.T) { + ctx, h := cacheKeeper() + { + msg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("create_collection", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("create_collection", sdk.NewAttribute("name", defaultName)), + sdk.NewEvent("create_collection", sdk.NewAttribute("owner", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "issue")), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "mint")), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "burn")), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "modify")), + } + verifyEventFunc(t, e, res.Events) + } +} diff --git a/x/collection/internal/handler/handler.go b/x/collection/internal/handler/handler.go new file mode 100644 index 0000000000..b8b0f991cb --- /dev/null +++ b/x/collection/internal/handler/handler.go @@ -0,0 +1,86 @@ +package handler + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" +) + +func NewHandler(keeper keeper.Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + if msg, ok := msg.(contract.Msg); ok { + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, msg.GetContractID())) + err := handleMsgContract(ctx, keeper, msg) + if err != nil { + return nil, err + } + } + + if _, ok := msg.(types.MsgCreateCollection); ok { + contractID := keeper.NewContractID(ctx) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, contractID)) + } + if ctx.Context().Value(contract.CtxKey{}) == nil { + panic("contract id does not set") + } + switch msg := msg.(type) { + case types.MsgCreateCollection: + return handleMsgCreateCollection(ctx, keeper, msg) + case types.MsgIssueFT: + return handleMsgIssueFT(ctx, keeper, msg) + case types.MsgMintNFT: + return handleMsgMintNFT(ctx, keeper, msg) + case types.MsgBurnNFT: + return handleMsgBurnNFT(ctx, keeper, msg) + case types.MsgBurnNFTFrom: + return handleMsgBurnNFTFrom(ctx, keeper, msg) + case types.MsgIssueNFT: + return handleMsgIssueNFT(ctx, keeper, msg) + case types.MsgMintFT: + return handleMsgMintFT(ctx, keeper, msg) + case types.MsgBurnFT: + return handleMsgBurnFT(ctx, keeper, msg) + case types.MsgBurnFTFrom: + return handleMsgBurnFTFrom(ctx, keeper, msg) + case types.MsgGrantPermission: + return handleMsgGrant(ctx, keeper, msg) + case types.MsgRevokePermission: + return handleMsgRevoke(ctx, keeper, msg) + case types.MsgModify: + return handleMsgModify(ctx, keeper, msg) + case types.MsgTransferFT: + return handleMsgTransferFT(ctx, keeper, msg) + case types.MsgTransferNFT: + return handleMsgTransferNFT(ctx, keeper, msg) + case types.MsgTransferFTFrom: + return handleMsgTransferFTFrom(ctx, keeper, msg) + case types.MsgTransferNFTFrom: + return handleMsgTransferNFTFrom(ctx, keeper, msg) + case types.MsgAttach: + return handleMsgAttach(ctx, keeper, msg) + case types.MsgDetach: + return handleMsgDetach(ctx, keeper, msg) + case types.MsgAttachFrom: + return handleMsgAttachFrom(ctx, keeper, msg) + case types.MsgDetachFrom: + return handleMsgDetachFrom(ctx, keeper, msg) + case types.MsgApprove: + return handleMsgApprove(ctx, keeper, msg) + case types.MsgDisapprove: + return handleMsgDisapprove(ctx, keeper, msg) + default: + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized Msg type: %T", msg) + } + } +} +func handleMsgContract(ctx sdk.Context, keeper keeper.Keeper, msg contract.Msg) error { + if !keeper.HasContractID(ctx) { + return sdkerrors.Wrapf(contract.ErrContractNotExist, "contract id: %s", msg.GetContractID()) + } + return nil +} diff --git a/x/collection/internal/handler/handler_test.go b/x/collection/internal/handler/handler_test.go new file mode 100644 index 0000000000..512d169eca --- /dev/null +++ b/x/collection/internal/handler/handler_test.go @@ -0,0 +1,84 @@ +package handler + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + testCommon "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +var ( + ms store.CommitMultiStore + ctx sdk.Context + k testCommon.Keeper +) + +func setup() { + println("setup") + ctx, ms, k = testCommon.TestKeeper() +} + +func TestMain(m *testing.M) { + setup() + ret := m.Run() + os.Exit(ret) +} + +func cacheKeeper() (sdk.Context, sdk.Handler) { + msCache := ms.CacheMultiStore() + ctx = ctx.WithMultiStore(msCache) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID)) + return ctx, NewHandler(k) +} + +var verifyEventFunc = func(t *testing.T, expected sdk.Events, actual sdk.Events) { + require.Equal(t, sdk.StringifyEvents(expected.ToABCIEvents()).String(), sdk.StringifyEvents(actual.ToABCIEvents()).String()) +} + +const ( + defaultContractID = "9be17165" + defaultName = "name" + defaultMeta = "{}" + defaultImgURI = "img-uri" + defaultDecimals = 6 + defaultAmount = 1000 + defaultTokenType = "10000001" + defaultTokenType2 = "10000002" + defaultTokenType3 = "10000003" + defaultTokenIndex = "00000001" + defaultTokenID1 = defaultTokenType + defaultTokenIndex + defaultTokenID2 = defaultTokenType + "00000002" + defaultTokenID3 = defaultTokenType + "00000003" + defaultTokenIDFT = "0000000100000000" +) + +var ( + addr1 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) +) + +func GetMadeContractID(events sdk.Events) string { + for _, event := range events.ToABCIEvents() { + for _, attr := range event.Attributes { + if string(attr.Key) == types.AttributeKeyContractID { + return string(attr.Value) + } + } + } + return "" +} + +func TestHandlerUnrecognized(t *testing.T) { + ctx, h := cacheKeeper() + _, err := h(ctx, sdk.NewTestMsg()) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "unrecognized Msg type")) +} diff --git a/x/collection/internal/handler/issue.go b/x/collection/internal/handler/issue.go new file mode 100644 index 0000000000..5c52c9f7bc --- /dev/null +++ b/x/collection/internal/handler/issue.go @@ -0,0 +1,73 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func handleMsgIssueFT(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgIssueFT) (*sdk.Result, error) { + _, err := keeper.GetCollection(ctx) + if err != nil { + return nil, err + } + perm := types.NewIssuePermission() + if !keeper.HasPermission(ctx, msg.Owner, perm) { + return nil, sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", msg.Owner.String(), perm.String()) + } + + tokenID, err := keeper.GetNextTokenIDFT(ctx) + if err != nil { + return nil, err + } + + token := types.NewFT(msg.ContractID, tokenID, msg.Name, msg.Meta, msg.Decimals, msg.Mintable) + err = keeper.IssueFT(ctx, msg.Owner, msg.To, token, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String()), + ), + }) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgIssueNFT(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgIssueNFT) (*sdk.Result, error) { + _, err := keeper.GetCollection(ctx) + if err != nil { + return nil, err + } + + perm := types.NewIssuePermission() + if !keeper.HasPermission(ctx, msg.Owner, perm) { + return nil, sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", msg.Owner.String(), perm.String()) + } + + tokenTypeID, err := keeper.GetNextTokenType(ctx) + if err != nil { + return nil, err + } + + tokenType := types.NewBaseTokenType(msg.ContractID, tokenTypeID, msg.Name, msg.Meta) + err = keeper.IssueNFT(ctx, tokenType, msg.Owner) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String()), + ), + }) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/collection/internal/handler/issue_test.go b/x/collection/internal/handler/issue_test.go new file mode 100644 index 0000000000..8583c406a9 --- /dev/null +++ b/x/collection/internal/handler/issue_test.go @@ -0,0 +1,382 @@ +package handler + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func prepareCreateCollection(t *testing.T) (sdk.Context, sdk.Handler, string) { + ctx, h := cacheKeeper() + var contractID string + msg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, msg) + require.NoError(t, err) + + contractID = GetMadeContractID(res.Events) + + return ctx, h, contractID +} + +func prepareFT(t *testing.T) (sdk.Context, sdk.Handler, string) { + ctx, h, contractID := prepareCreateCollection(t) + + msg := types.NewMsgIssueFT(addr1, addr1, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err := h(ctx, msg) + require.NoError(t, err) + + return ctx, h, contractID +} + +func prepareNFT(t *testing.T, mintTo sdk.AccAddress) (sdk.Context, sdk.Handler, string) { + ctx, h, contractID := prepareCreateCollection(t) + + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err := h(ctx, msg) + require.NoError(t, err) + + param := types.NewMintNFTParam("sword1", defaultMeta, "10000001") + msg2 := types.NewMsgMintNFT(addr1, contractID, mintTo, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + + param = types.NewMintNFTParam("sword2", defaultMeta, "10000001") + types.NewMsgMintNFT(addr1, contractID, mintTo, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + + return ctx, h, contractID +} + +func TestHandleMsgIssueFT(t *testing.T) { + ctx, h, contractID := prepareCreateCollection(t) + + msg := types.NewMsgIssueFT(addr1, addr1, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("issue_ft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("issue_ft", sdk.NewAttribute("name", defaultName)), + sdk.NewEvent("issue_ft", sdk.NewAttribute("token_id", defaultTokenIDFT)), + sdk.NewEvent("issue_ft", sdk.NewAttribute("owner", addr1.String())), + sdk.NewEvent("issue_ft", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("issue_ft", sdk.NewAttribute("amount", "1000")), + sdk.NewEvent("issue_ft", sdk.NewAttribute("mintable", "true")), + sdk.NewEvent("issue_ft", sdk.NewAttribute("decimals", "6")), + } + verifyEventFunc(t, e, res.Events) +} + +func TestHandleMsgIssueNFT(t *testing.T) { + ctx, h, contractID := prepareCreateCollection(t) + + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("issue_nft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("issue_nft", sdk.NewAttribute("token_type", defaultTokenType)), + } + verifyEventFunc(t, e, res.Events) +} + +func TestHandlerIssueFT(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + msg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, msg) + contractID = GetMadeContractID(res.Events) + require.NoError(t, err) + } + + { + msg := types.NewMsgIssueFT(addr1, addr1, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + msg := types.NewMsgIssueFT(addr1, addr1, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + msg := types.NewMsgIssueFT(addr2, addr2, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err := h(ctx, msg) + require.Error(t, err) + } + + permission := types.NewIssuePermission() + + { + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, permission) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + msg := types.NewMsgIssueFT(addr2, addr2, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + msg := types.NewMsgRevokePermission(addr1, contractID, permission) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + msg := types.NewMsgIssueFT(addr1, addr2, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err := h(ctx, msg) + require.Error(t, err) + } +} + +func TestHandlerIssueNFT(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + msg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, msg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + } + + { + // Expect token type is 1001 + { + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err := h(ctx, msg) + require.NoError(t, err) + } + // Expect token type is 1002 + { + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType2) + msg := types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType2) + msg := types.NewMsgMintNFT(addr1, contractID, addr2, param) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + mintPermission := types.NewMintPermission() + { + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, mintPermission) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType2) + msg := types.NewMsgMintNFT(addr2, contractID, addr2, param) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + msg := types.NewMsgRevokePermission(addr1, contractID, mintPermission) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType2) + msg := types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err := h(ctx, msg) + require.Error(t, err) + } + } + } + + permission := types.NewIssuePermission() + + { + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, permission) + _, err := h(ctx, msg) + require.NoError(t, err) + } + + // Expect token type is 1003 + { + msg := types.NewMsgIssueNFT(addr2, contractID, defaultName, defaultMeta) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType3) + msg := types.NewMsgMintNFT(addr2, contractID, addr2, param) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + msg := types.NewMsgRevokePermission(addr1, contractID, permission) + _, err := h(ctx, msg) + require.NoError(t, err) + } + { + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err := h(ctx, msg) + require.Error(t, err) + } +} + +func TestEvents(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + msg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("create_collection", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("create_collection", sdk.NewAttribute("name", defaultName)), + sdk.NewEvent("create_collection", sdk.NewAttribute("owner", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "issue")), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "mint")), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "burn")), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "modify")), + } + verifyEventFunc(t, e, res.Events) + } + + { + msg := types.NewMsgIssueFT(addr1, addr1, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("issue_ft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("issue_ft", sdk.NewAttribute("name", defaultName)), + sdk.NewEvent("issue_ft", sdk.NewAttribute("token_id", defaultTokenIDFT)), + sdk.NewEvent("issue_ft", sdk.NewAttribute("owner", addr1.String())), + sdk.NewEvent("issue_ft", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("issue_ft", sdk.NewAttribute("amount", sdk.NewInt(defaultAmount).String())), + sdk.NewEvent("issue_ft", sdk.NewAttribute("mintable", "true")), + sdk.NewEvent("issue_ft", sdk.NewAttribute("decimals", sdk.NewInt(defaultDecimals).String())), + } + verifyEventFunc(t, e, res.Events) + } + + { + msg := types.NewMsgMintFT(addr1, contractID, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("mint_ft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("mint_ft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("mint_ft", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("mint_ft", sdk.NewAttribute("amount", types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount)).String())), + } + verifyEventFunc(t, e, res.Events) + } + + { + msg := types.NewMsgBurnFT(addr1, contractID, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("burn_ft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("burn_ft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("burn_ft", sdk.NewAttribute("amount", types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount)).String())), + } + verifyEventFunc(t, e, res.Events) + } + + { + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("issue_nft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("issue_nft", sdk.NewAttribute("token_type", defaultTokenType)), + } + verifyEventFunc(t, e, res.Events) + } + + { + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType) + msg := types.NewMsgMintNFT(addr1, contractID, addr1, param) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("mint_nft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("mint_nft", sdk.NewAttribute("name", defaultName)), + sdk.NewEvent("mint_nft", sdk.NewAttribute("token_id", defaultTokenID1)), + sdk.NewEvent("mint_nft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("mint_nft", sdk.NewAttribute("to", addr1.String())), + } + verifyEventFunc(t, e, res.Events) + } + + permission := types.NewIssuePermission() + + { + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, permission) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", permission.String())), + } + verifyEventFunc(t, e, res.Events) + } + { + msg := types.NewMsgRevokePermission(addr1, contractID, permission) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("perm", permission.String())), + } + verifyEventFunc(t, e, res.Events) + } +} diff --git a/x/collection/internal/handler/mint.go b/x/collection/internal/handler/mint.go new file mode 100644 index 0000000000..3f53065a58 --- /dev/null +++ b/x/collection/internal/handler/mint.go @@ -0,0 +1,52 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func handleMsgMintNFT(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgMintNFT) (*sdk.Result, error) { + _, err := keeper.GetCollection(ctx) + if err != nil { + return nil, err + } + + for _, mintNFTParam := range msg.MintNFTParams { + tokenID, err := keeper.GetNextTokenIDNFT(ctx, mintNFTParam.TokenType) + if err != nil { + return nil, err + } + + token := types.NewNFT(msg.ContractID, tokenID, mintNFTParam.Name, mintNFTParam.Meta, msg.To) + err = keeper.MintNFT(ctx, msg.From, token) + if err != nil { + return nil, err + } + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgMintFT(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgMintFT) (*sdk.Result, error) { + err := keeper.MintFT(ctx, msg.From, msg.To, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/collection/internal/handler/mint_test.go b/x/collection/internal/handler/mint_test.go new file mode 100644 index 0000000000..569bd630ab --- /dev/null +++ b/x/collection/internal/handler/mint_test.go @@ -0,0 +1,89 @@ +package handler + +import ( + "fmt" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" +) + +func TestHandleMsgMintFT(t *testing.T) { + ctx, h, contractID := prepareFT(t) + + { + burnMsg := types.NewMsgMintFT(addr1, contractID, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(100))) + res, err := h(ctx, burnMsg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("mint_ft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("mint_ft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("mint_ft", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("mint_ft", sdk.NewAttribute("amount", types.NewCoins(types.NewCoin("0000000100000000", sdk.NewInt(100))).String())), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleMsgMintNFT(t *testing.T) { + ctx, h, contractID := prepareNFT(t, addr1) + + { + param := types.NewMintNFTParam("shield", "", "10000001") + msg := types.NewMsgMintNFT(addr1, contractID, addr1, param) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("mint_nft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("mint_nft", sdk.NewAttribute("name", "shield")), + sdk.NewEvent("mint_nft", sdk.NewAttribute("token_id", defaultTokenID3)), + sdk.NewEvent("mint_nft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("mint_nft", sdk.NewAttribute("to", addr1.String())), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleMsgMintNFTPerformance(t *testing.T) { + ctx, h, contractID := prepareNFT(t, addr1) + var mean int64 + var sum int64 = 0 + { + param := types.NewMintNFTParam("shield", "", "10000001") + msg := types.NewMsgMintNFT(addr1, contractID, addr1, param) + for jdx := 0; jdx < 10; jdx++ { + startTime := time.Now() + for idx := 0; idx < 1000; idx++ { + _, err := h(ctx, msg) + require.NoError(t, err) + } + duration := time.Since(startTime) + sum += duration.Nanoseconds() + } + mean = sum / 10 + } + + ctx, h, contractID = prepareNFT(t, addr1) + { + param := types.NewMintNFTParam("shield", "", "10000001") + msg := types.NewMsgMintNFT(addr1, contractID, addr1, param) + for jdx := 0; jdx < 10; jdx++ { + startTime := time.Now() + for idx := 0; idx < 1000; idx++ { + _, err := h(ctx, msg) + require.NoError(t, err) + } + duration := time.Since(startTime) + t.Log(fmt.Sprintf("MintNFT %s", duration.String())) + require.Less(t, duration.Nanoseconds(), mean*2) + } + } +} diff --git a/x/collection/internal/handler/modify.go b/x/collection/internal/handler/modify.go new file mode 100644 index 0000000000..0b8aec7265 --- /dev/null +++ b/x/collection/internal/handler/modify.go @@ -0,0 +1,22 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func handleMsgModify(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgModify) (*sdk.Result, error) { + if err := keeper.Modify(ctx, msg.Owner, msg.TokenType, msg.TokenIndex, msg.Changes); err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/collection/internal/handler/modify_test.go b/x/collection/internal/handler/modify_test.go new file mode 100644 index 0000000000..face4fd7e3 --- /dev/null +++ b/x/collection/internal/handler/modify_test.go @@ -0,0 +1,172 @@ +package handler + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestHandleMsgModifyForCollection(t *testing.T) { + ctx, h := cacheKeeper() + const ( + modifiedName = "modifiedName" + modifiedImgURI = "modifiedImgURI" + modifiedMeta = "modifiedMeta" + ) + + var contractID string + + // Given MsgModify + msg := types.NewMsgModify(addr1, defaultContractID, "", "", types.NewChanges( + types.NewChange("name", modifiedName), + types.NewChange("base_img_uri", modifiedImgURI), + types.NewChange("meta", modifiedMeta), + )) + + t.Log("Test with nonexistent token") + { + // When handle MsgModify + _, err := h(ctx, msg) + + // Then response is error + require.Error(t, err) + } + + t.Log("Test modify token") + { + // Given created collection + res, err := h(ctx, types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI)) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + // When handle MsgModify + msg = types.NewMsgModify(addr1, contractID, "", "", types.NewChanges( + types.NewChange("name", modifiedName), + types.NewChange("base_img_uri", modifiedImgURI), + types.NewChange("meta", modifiedMeta))) + res, err = h(ctx, msg) + + // Then response is success + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + // And events are returned + expectedEvents := sdk.Events{ + sdk.NewEvent(types.EventTypeModifyCollection, sdk.NewAttribute(types.AttributeKeyContractID, contractID)), + sdk.NewEvent(types.EventTypeModifyCollection, sdk.NewAttribute("name", modifiedName)), + sdk.NewEvent(types.EventTypeModifyCollection, sdk.NewAttribute("base_img_uri", modifiedImgURI)), + sdk.NewEvent(types.EventTypeModifyCollection, sdk.NewAttribute("meta", modifiedMeta)), + sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory)), + sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String())), + } + verifyEventFunc(t, expectedEvents, res.Events) + } +} + +func TestHandleMsgModifyForToken(t *testing.T) { + ctx, h := cacheKeeper() + const ( + modifiedTokenName = "modifiedTokenName" + modifiedMeta = "modifiedMeta" + ) + + // created collection + res, err := h(ctx, types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI)) + require.NoError(t, err) + contractID := GetMadeContractID(res.Events) + + // Given MsgModify + msg := types.NewMsgModify(addr1, contractID, defaultTokenType, defaultTokenIndex, types.NewChanges( + types.NewChange("name", modifiedTokenName), + types.NewChange("meta", modifiedMeta), + )) + + t.Log("Test with nonexistent token") + { + // When handle MsgModify + _, err := h(ctx, msg) + + // Then response is error + require.Error(t, err) + } + + t.Log("Test modify token") + { + // Given token + _, err = h(ctx, types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta)) + require.NoError(t, err) + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType) + _, err = h(ctx, types.NewMsgMintNFT(addr1, contractID, addr1, param)) + require.NoError(t, err) + + // When handle MsgModify + res, err = h(ctx, msg) + + // Then response is success + require.NoError(t, err) + // And events are returned + expectedEvents := sdk.Events{ + sdk.NewEvent(types.EventTypeModifyToken, sdk.NewAttribute(types.AttributeKeyContractID, contractID)), + sdk.NewEvent(types.EventTypeModifyToken, sdk.NewAttribute(types.AttributeKeyTokenID, defaultTokenID1)), + sdk.NewEvent(types.EventTypeModifyToken, sdk.NewAttribute("name", modifiedTokenName)), + sdk.NewEvent(types.EventTypeModifyToken, sdk.NewAttribute("meta", modifiedMeta)), + sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory)), + sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String())), + } + verifyEventFunc(t, expectedEvents, res.Events) + } +} + +func TestHandleMsgModifyForTokenType(t *testing.T) { + ctx, h := cacheKeeper() + const ( + modifiedTokenName = "modifiedTokenName" + modifiedMeta = "modifiedMeta" + ) + + // created collection + res, err := h(ctx, types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI)) + require.NoError(t, err) + contractID := GetMadeContractID(res.Events) + + // Given MsgModify + msg := types.NewMsgModify(addr1, contractID, defaultTokenType, "", types.NewChanges( + types.NewChange("name", modifiedTokenName), + types.NewChange("meta", modifiedMeta), + )) + + t.Log("Test with nonexistent token type") + { + // When handle MsgModify + _, err := h(ctx, msg) + + // Then response is error + require.Error(t, err) + } + + t.Log("Test modify token type") + { + // Given token type + _, err = h(ctx, types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta)) + require.NoError(t, err) + + // When handle MsgModify + res, err = h(ctx, msg) + + // Then response is success + require.NoError(t, err) + // And events are returned + expectedEvents := sdk.Events{ + sdk.NewEvent(types.EventTypeModifyTokenType, sdk.NewAttribute(types.AttributeKeyContractID, contractID)), + sdk.NewEvent(types.EventTypeModifyTokenType, sdk.NewAttribute(types.AttributeKeyTokenType, defaultTokenType)), + sdk.NewEvent(types.EventTypeModifyTokenType, sdk.NewAttribute("name", modifiedTokenName)), + sdk.NewEvent(types.EventTypeModifyTokenType, sdk.NewAttribute("meta", modifiedMeta)), + sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory)), + sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String())), + } + verifyEventFunc(t, expectedEvents, res.Events) + } +} diff --git a/x/collection/internal/handler/perm.go b/x/collection/internal/handler/perm.go new file mode 100644 index 0000000000..fc139a8dc6 --- /dev/null +++ b/x/collection/internal/handler/perm.go @@ -0,0 +1,39 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func handleMsgGrant(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgGrantPermission) (*sdk.Result, error) { + err := keeper.GrantPermission(ctx, msg.From, msg.To, msg.Permission) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgRevoke(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgRevokePermission) (*sdk.Result, error) { + err := keeper.RevokePermission(ctx, msg.From, msg.Permission) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/collection/internal/handler/perm_test.go b/x/collection/internal/handler/perm_test.go new file mode 100644 index 0000000000..f1c62dcca4 --- /dev/null +++ b/x/collection/internal/handler/perm_test.go @@ -0,0 +1,160 @@ +package handler + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestHandleMsgGrant(t *testing.T) { + ctx, h, contractID := prepareCreateCollection(t) + { + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, types.NewIssuePermission()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "issue")), + } + verifyEventFunc(t, e, res.Events) + } + { + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, types.NewMintPermission()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "mint")), + } + verifyEventFunc(t, e, res.Events) + } + { + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, types.NewBurnPermission()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "burn")), + } + verifyEventFunc(t, e, res.Events) + } + { + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, types.NewModifyPermission()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "modify")), + } + verifyEventFunc(t, e, res.Events) + } + t.Log("Invalid contract id") + { + msg := types.NewMsgGrantPermission(addr1, "1234567890", addr2, types.NewModifyPermission()) + require.Error(t, msg.ValidateBasic()) + } +} + +func TestHandleMsgRevoke(t *testing.T) { + ctx, h, contractID := prepareCreateCollection(t) + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, types.NewIssuePermission()) + _, err := h(ctx, msg) + require.NoError(t, err) + + msg = types.NewMsgGrantPermission(addr1, contractID, addr2, types.NewMintPermission()) + _, err = h(ctx, msg) + require.NoError(t, err) + + msg = types.NewMsgGrantPermission(addr1, contractID, addr2, types.NewBurnPermission()) + _, err = h(ctx, msg) + require.NoError(t, err) + + msg = types.NewMsgGrantPermission(addr1, contractID, addr2, types.NewModifyPermission()) + _, err = h(ctx, msg) + require.NoError(t, err) + + { + msg := types.NewMsgRevokePermission(addr2, contractID, types.NewIssuePermission()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("from", addr2.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("perm", "issue")), + } + verifyEventFunc(t, e, res.Events) + } + { + msg := types.NewMsgRevokePermission(addr2, contractID, types.NewMintPermission()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("from", addr2.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("perm", "mint")), + } + verifyEventFunc(t, e, res.Events) + } + { + msg := types.NewMsgRevokePermission(addr2, contractID, types.NewBurnPermission()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("from", addr2.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("perm", "burn")), + } + verifyEventFunc(t, e, res.Events) + } + { + msg := types.NewMsgRevokePermission(addr2, contractID, types.NewModifyPermission()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("from", addr2.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("perm", "modify")), + } + verifyEventFunc(t, e, res.Events) + } + t.Log("Invalid contract id") + { + msg := types.NewMsgRevokePermission(addr1, "1234567890", types.NewModifyPermission()) + require.Error(t, msg.ValidateBasic()) + } +} diff --git a/x/collection/internal/handler/proxy.go b/x/collection/internal/handler/proxy.go new file mode 100644 index 0000000000..4e288cc90e --- /dev/null +++ b/x/collection/internal/handler/proxy.go @@ -0,0 +1,41 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func handleMsgApprove(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgApprove) (*sdk.Result, error) { + err := keeper.SetApproved(ctx, msg.Proxy, msg.Approver) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Approver.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgDisapprove(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgDisapprove) (*sdk.Result, error) { + err := keeper.DeleteApproved(ctx, msg.Proxy, msg.Approver) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Approver.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/collection/internal/handler/proxy_test.go b/x/collection/internal/handler/proxy_test.go new file mode 100644 index 0000000000..a8b84a3466 --- /dev/null +++ b/x/collection/internal/handler/proxy_test.go @@ -0,0 +1,148 @@ +package handler + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" +) + +func approve(t *testing.T, approver, proxy sdk.AccAddress, contractID string, ctx sdk.Context, h sdk.Handler) { + approveMsg := types.NewMsgApprove(approver, contractID, proxy) + _, err := h(ctx, approveMsg) + require.NoError(t, err) +} + +func TestHandleMsgApprove(t *testing.T) { + t.Log("implement me - ", t.Name()) +} +func TestHandleMsgDisapprove(t *testing.T) { + t.Log("implement me - ", t.Name()) +} + +func TestHandleApproveDisapprove(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + createMsg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, createMsg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err = h(ctx, msg) + require.NoError(t, err) + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType) + msg2 := types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + msg2 = types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + msg3 := types.NewMsgIssueFT(addr1, addr1, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err = h(ctx, msg3) + require.NoError(t, err) + msg4 := types.NewMsgMintFT(addr1, contractID, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + _, err = h(ctx, msg4) + require.NoError(t, err) + } + + msg := types.NewMsgTransferNFTFrom(addr2, contractID, addr1, addr2, defaultTokenID1) + _, err := h(ctx, msg) + require.Error(t, err) + + { + msg3 := types.NewMsgApprove(addr1, contractID, addr2) + _, err = h(ctx, msg3) + require.NoError(t, err) + } + + msg = types.NewMsgTransferNFTFrom(addr2, contractID, addr1, addr2, defaultTokenID1) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("token_id", defaultTokenID1)), + sdk.NewEvent("operation_transfer_nft", sdk.NewAttribute("token_id", defaultTokenID1)), + } + verifyEventFunc(t, e, res.Events) + + msg2 := types.NewMsgBurnNFTFrom(addr2, contractID, addr1, defaultTokenID2) + _, err = h(ctx, msg2) + require.Error(t, err) // addr2 does not have the burn permission + msg3 := types.NewMsgBurnFTFrom(addr2, contractID, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + _, err = h(ctx, msg3) + require.Error(t, err) // addr2 does not have the burn permission + + { + permission := types.NewBurnPermission() + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, permission) + _, err := h(ctx, msg) + require.NoError(t, err) + } + + msg2 = types.NewMsgBurnNFTFrom(addr2, contractID, addr1, defaultTokenID2) + res, err = h(ctx, msg2) + require.NoError(t, err) // addr2 does not have the burn permission + + e = sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("burn_nft_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("burn_nft_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("burn_nft_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("burn_nft_from", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("operation_burn_nft", sdk.NewAttribute("token_id", defaultTokenID2)), + } + verifyEventFunc(t, e, res.Events) + + msg3 = types.NewMsgBurnFTFrom(addr2, contractID, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + _, err = h(ctx, msg3) + require.NoError(t, err) + + { + permission := types.NewBurnPermission() + msg := types.NewMsgGrantPermission(addr1, contractID, addr2, permission) + _, err := h(ctx, msg) + require.NoError(t, err) + } + + msg3 = types.NewMsgBurnFTFrom(addr2, contractID, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + res, err = h(ctx, msg3) + require.NoError(t, err) + e = sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("burn_ft_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("burn_ft_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("burn_ft_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("burn_ft_from", sdk.NewAttribute("amount", types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))).String())), + } + verifyEventFunc(t, e, res.Events) + + { + msg3 := types.NewMsgDisapprove(addr1, contractID, addr2) + _, err = h(ctx, msg3) + require.NoError(t, err) + } + + msg = types.NewMsgTransferNFTFrom(addr2, contractID, addr1, addr2, defaultTokenID1) + _, err = h(ctx, msg) + require.Error(t, err) + + msg2 = types.NewMsgBurnNFTFrom(addr2, contractID, addr1, defaultTokenID1) + _, err = h(ctx, msg2) + require.Error(t, err) + + msg3 = types.NewMsgBurnFTFrom(addr2, contractID, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + _, err = h(ctx, msg3) + require.Error(t, err) +} diff --git a/x/collection/internal/handler/transfer.go b/x/collection/internal/handler/transfer.go new file mode 100644 index 0000000000..51558fdd8b --- /dev/null +++ b/x/collection/internal/handler/transfer.go @@ -0,0 +1,75 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func handleMsgTransferFT(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgTransferFT) (*sdk.Result, error) { + err := keeper.TransferFT(ctx, msg.From, msg.To, msg.Amount...) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgTransferNFT(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgTransferNFT) (*sdk.Result, error) { + err := keeper.TransferNFT(ctx, msg.From, msg.To, msg.TokenIDs...) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgTransferFTFrom(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgTransferFTFrom) (*sdk.Result, error) { + err := keeper.TransferFTFrom(ctx, msg.Proxy, msg.From, msg.To, msg.Amount...) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Proxy.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgTransferNFTFrom(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgTransferNFTFrom) (*sdk.Result, error) { + err := keeper.TransferNFTFrom(ctx, msg.Proxy, msg.From, msg.To, msg.TokenIDs...) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Proxy.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/collection/internal/handler/transfer_test.go b/x/collection/internal/handler/transfer_test.go new file mode 100644 index 0000000000..579203887e --- /dev/null +++ b/x/collection/internal/handler/transfer_test.go @@ -0,0 +1,192 @@ +package handler + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestHandleTransferFT(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + createMsg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, createMsg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + msg := types.NewMsgIssueFT(addr1, addr1, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err = h(ctx, msg) + require.NoError(t, err) + } + + msg := types.NewMsgTransferFT(addr1, contractID, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("transfer_ft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("transfer_ft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("transfer_ft", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("transfer_ft", sdk.NewAttribute("amount", types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount)).String())), + } + verifyEventFunc(t, e, res.Events) +} + +func TestHandleTransferFTFrom(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + createMsg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, createMsg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + msg := types.NewMsgIssueFT(addr1, addr1, contractID, defaultName, defaultMeta, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err = h(ctx, msg) + require.NoError(t, err) + msg2 := types.NewMsgApprove(addr1, contractID, addr2) + _, err = h(ctx, msg2) + require.NoError(t, err) + } + + msg := types.NewMsgTransferFTFrom(addr2, contractID, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("transfer_ft_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("transfer_ft_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("transfer_ft_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("transfer_ft_from", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("transfer_ft_from", sdk.NewAttribute("amount", types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount)).String())), + } + verifyEventFunc(t, e, res.Events) +} + +func TestHandleTransferNFT(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + createMsg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, createMsg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err = h(ctx, msg) + require.NoError(t, err) + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType) + msg2 := types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + } + + msg := types.NewMsgTransferNFT(addr1, contractID, addr2, defaultTokenID1) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("transfer_nft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("transfer_nft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("transfer_nft", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("transfer_nft", sdk.NewAttribute("token_id", defaultTokenID1)), + sdk.NewEvent("operation_transfer_nft", sdk.NewAttribute("token_id", defaultTokenID1)), + } + verifyEventFunc(t, e, res.Events) +} + +func TestHandleTransferNFTFrom(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + createMsg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, createMsg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err = h(ctx, msg) + require.NoError(t, err) + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType) + msg2 := types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + msg3 := types.NewMsgApprove(addr1, contractID, addr2) + _, err = h(ctx, msg3) + require.NoError(t, err) + } + + msg := types.NewMsgTransferNFTFrom(addr2, contractID, addr1, addr2, defaultTokenID1) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("transfer_nft_from", sdk.NewAttribute("token_id", defaultTokenID1)), + sdk.NewEvent("operation_transfer_nft", sdk.NewAttribute("token_id", defaultTokenID1)), + } + verifyEventFunc(t, e, res.Events) +} + +func TestHandleTransferNFTChild(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + createMsg := types.NewMsgCreateCollection(addr1, defaultName, defaultMeta, defaultImgURI) + res, err := h(ctx, createMsg) + require.NoError(t, err) + contractID = GetMadeContractID(res.Events) + + msg := types.NewMsgIssueNFT(addr1, contractID, defaultName, defaultMeta) + _, err = h(ctx, msg) + require.NoError(t, err) + param := types.NewMintNFTParam(defaultName, defaultMeta, defaultTokenType) + msg2 := types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + msg2 = types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + msg2 = types.NewMsgMintNFT(addr1, contractID, addr1, param) + _, err = h(ctx, msg2) + require.NoError(t, err) + msg3 := types.NewMsgAttach(addr1, contractID, defaultTokenID1, defaultTokenID2) + _, err = h(ctx, msg3) + require.NoError(t, err) + msg3 = types.NewMsgAttach(addr1, contractID, defaultTokenID2, defaultTokenID3) + _, err = h(ctx, msg3) + require.NoError(t, err) + } + + msg := types.NewMsgTransferNFT(addr1, contractID, addr2, defaultTokenID1) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "collection")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("transfer_nft", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("transfer_nft", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("transfer_nft", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("transfer_nft", sdk.NewAttribute("token_id", defaultTokenID1)), + sdk.NewEvent("operation_transfer_nft", sdk.NewAttribute("token_id", defaultTokenID1)), + sdk.NewEvent("operation_transfer_nft", sdk.NewAttribute("token_id", defaultTokenID2)), + sdk.NewEvent("operation_transfer_nft", sdk.NewAttribute("token_id", defaultTokenID3)), + } + verifyEventFunc(t, e, res.Events) +} diff --git a/x/collection/internal/keeper/account.go b/x/collection/internal/keeper/account.go new file mode 100644 index 0000000000..f7064f7a37 --- /dev/null +++ b/x/collection/internal/keeper/account.go @@ -0,0 +1,91 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type AccountKeeper interface { + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) + GetOrNewAccount(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) + GetAccount(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) + SetAccount(ctx sdk.Context, acc types.Account) error + UpdateAccount(ctx sdk.Context, acc types.Account) error + GetBalance(ctx sdk.Context, tokenID string, addr sdk.AccAddress) (sdk.Int, error) +} + +var _ AccountKeeper = (*Keeper)(nil) + +func (k Keeper) GetBalance(ctx sdk.Context, tokenID string, addr sdk.AccAddress) (sdk.Int, error) { + acc, err := k.GetAccount(ctx, addr) + if err != nil { + return sdk.ZeroInt(), err + } + return acc.GetCoins().AmountOf(tokenID), nil +} + +func (k Keeper) NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) { + acc = types.NewBaseAccountWithAddress(k.getContractID(ctx), addr) + if err = k.SetAccount(ctx, acc); err != nil { + return nil, err + } + return acc, nil +} + +func (k Keeper) SetAccount(ctx sdk.Context, acc types.Account) error { + store := ctx.KVStore(k.storeKey) + accKey := types.AccountKey(acc.GetContractID(), acc.GetAddress()) + if store.Has(accKey) { + return sdkerrors.Wrap(types.ErrAccountExist, acc.GetAddress().String()) + } + store.Set(accKey, k.cdc.MustMarshalBinaryBare(acc)) + + // Set Account if not exists yet + account := k.accountKeeper.GetAccount(ctx, acc.GetAddress()) + if account == nil { + account = k.accountKeeper.NewAccountWithAddress(ctx, acc.GetAddress()) + k.accountKeeper.SetAccount(ctx, account) + } + + return nil +} + +func (k Keeper) UpdateAccount(ctx sdk.Context, acc types.Account) error { + store := ctx.KVStore(k.storeKey) + accKey := types.AccountKey(acc.GetContractID(), acc.GetAddress()) + if !store.Has(accKey) { + return sdkerrors.Wrap(types.ErrAccountNotExist, acc.GetAddress().String()) + } + store.Set(accKey, k.cdc.MustMarshalBinaryBare(acc)) + return nil +} + +func (k Keeper) GetOrNewAccount(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) { + acc, err = k.GetAccount(ctx, addr) + if err != nil { + acc, err = k.NewAccountWithAddress(ctx, addr) + if err != nil { + return nil, err + } + } + return acc, nil +} + +func (k Keeper) GetAccount(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) { + store := ctx.KVStore(k.storeKey) + accKey := types.AccountKey(k.getContractID(ctx), addr) + if !store.Has(accKey) { + return nil, sdkerrors.Wrap(types.ErrAccountNotExist, addr.String()) + } + bz := store.Get(accKey) + return k.mustDecodeAccount(bz), nil +} + +func (k Keeper) mustDecodeAccount(bz []byte) (acc types.Account) { + err := k.cdc.UnmarshalBinaryBare(bz, &acc) + if err != nil { + panic(err) + } + return +} diff --git a/x/collection/internal/keeper/account_test.go b/x/collection/internal/keeper/account_test.go new file mode 100644 index 0000000000..39736de1c7 --- /dev/null +++ b/x/collection/internal/keeper/account_test.go @@ -0,0 +1,114 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper_GetAccountSupply(t *testing.T) { + ctx := cacheKeeper() + t.Log("Balance of addr1. Expect 0") + { + _, err := keeper.GetBalance(ctx, defaultTokenIDFT, addr1) + require.Error(t, err) + } + t.Log("Set tokens to addr1") + { + _, err := keeper.AddCoins(ctx, addr1, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount)))) + require.NoError(t, err) + } + t.Log("Balance of addr1.") + { + balance, err := keeper.GetBalance(ctx, defaultTokenIDFT, addr1) + require.NoError(t, err) + require.Equal(t, int64(defaultAmount), balance.Int64()) + } +} + +func verifyAccountFunc(t *testing.T, expected types.Account, actual types.Account) { + require.Equal(t, expected.GetContractID(), actual.GetContractID()) + require.Equal(t, expected.GetAddress(), actual.GetAddress()) + require.Equal(t, expected.GetCoins().String(), actual.GetCoins().String()) +} + +func TestKeeper_SetAccount(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + expected := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, expected)) + } + t.Log("Compare Account") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.AccountKey(expected.GetContractID(), addr1)) + actual := keeper.mustDecodeAccount(bz) + verifyAccountFunc(t, expected, actual) + } +} + +func TestKeeper_GetAccount(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Account") + expected := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.AccountKey(expected.GetContractID(), addr1), keeper.cdc.MustMarshalBinaryBare(expected)) + } + t.Log("Get Account") + { + actual, err := keeper.GetAccount(ctx, addr1) + require.NoError(t, err) + verifyAccountFunc(t, expected, actual) + } +} + +func TestKeeper_UpdateAccount(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + acc := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Update Account") + var expected types.Account + expected = types.NewBaseAccountWithAddress(defaultContractID, addr1) + expected = expected.SetCoins(types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.OneInt()))) + { + require.NoError(t, keeper.UpdateAccount(ctx, expected)) + } + t.Log("Compare Account") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.AccountKey(acc.GetContractID(), addr1)) + actual := keeper.mustDecodeAccount(bz) + verifyAccountFunc(t, expected, actual) + } +} + +func TestKeeper_GetOrNewAccount(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Account") + expected := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.AccountKey(expected.GetContractID(), addr1), keeper.cdc.MustMarshalBinaryBare(expected)) + } + t.Log("Get Account addr1") + { + actual, err := keeper.GetOrNewAccount(ctx, addr1) + require.NoError(t, err) + verifyAccountFunc(t, expected, actual) + } + + expected = types.NewBaseAccountWithAddress(defaultContractID, addr2) + t.Log("Get Account addr2") + { + actual, err := keeper.GetOrNewAccount(ctx, addr2) + require.NoError(t, err) + verifyAccountFunc(t, expected, actual) + } +} diff --git a/x/collection/internal/keeper/bank.go b/x/collection/internal/keeper/bank.go new file mode 100644 index 0000000000..0a5221be91 --- /dev/null +++ b/x/collection/internal/keeper/bank.go @@ -0,0 +1,99 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +// For the Token module +type BankKeeper interface { + GetCoins(ctx sdk.Context, addr sdk.AccAddress) types.Coins + HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt types.Coins) bool + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt types.Coins) error + SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt types.Coins) (types.Coins, error) + AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt types.Coins) (types.Coins, error) + SetCoins(ctx sdk.Context, addr sdk.AccAddress, amt types.Coins) error +} + +var _ BankKeeper = (*Keeper)(nil) + +func (k Keeper) GetCoins(ctx sdk.Context, addr sdk.AccAddress) types.Coins { + acc, err := k.GetAccount(ctx, addr) + if err != nil { + return types.NewCoins() + } + return acc.GetCoins() +} + +func (k Keeper) HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt types.Coins) bool { + return k.GetCoins(ctx, addr).IsAllGTE(amt) +} + +func (k Keeper) SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt types.Coins) error { + if !amt.IsValid() { + return sdkerrors.Wrap(types.ErrInvalidCoin, "send amount must be positive") + } + + _, err := k.SubtractCoins(ctx, fromAddr, amt) + if err != nil { + return err + } + + _, err = k.AddCoins(ctx, toAddr, amt) + if err != nil { + return err + } + return nil +} + +func (k Keeper) SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt types.Coins) (types.Coins, error) { + if !amt.IsValid() { + return nil, sdkerrors.Wrap(types.ErrInvalidCoin, "amount must be positive") + } + + acc, err := k.GetAccount(ctx, addr) + if err != nil { + return nil, sdkerrors.Wrapf(types.ErrInsufficientToken, "insufficient account funds[%s]; account has no coin", k.getContractID(ctx)) + } + oldCoins := acc.GetCoins() + + newCoins, hasNeg := oldCoins.SafeSub(amt) + if hasNeg { + return amt, sdkerrors.Wrapf(types.ErrInsufficientToken, "insufficient account funds[%s]; %s < %s", k.getContractID(ctx), oldCoins, amt) + } + + err = k.SetCoins(ctx, addr, newCoins) + + return newCoins, err +} + +func (k Keeper) AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt types.Coins) (types.Coins, error) { + if !amt.IsValid() { + return nil, sdkerrors.Wrap(types.ErrInvalidCoin, "amount must be positive") + } + + oldCoins := k.GetCoins(ctx, addr) + newCoins := oldCoins.Add(amt...) + + err := k.SetCoins(ctx, addr, newCoins) + return newCoins, err +} + +func (k Keeper) SetCoins(ctx sdk.Context, addr sdk.AccAddress, amt types.Coins) error { + if !amt.IsValid() { + return sdkerrors.Wrapf(types.ErrInvalidCoin, "invalid amount: %s", amt.String()) + } + + acc, err := k.GetOrNewAccount(ctx, addr) + if err != nil { + return err + } + + acc = acc.SetCoins(amt) + err = k.UpdateAccount(ctx, acc) + if err != nil { + return err + } + return nil +} diff --git a/x/collection/internal/keeper/bank_test.go b/x/collection/internal/keeper/bank_test.go new file mode 100644 index 0000000000..98ba9256f4 --- /dev/null +++ b/x/collection/internal/keeper/bank_test.go @@ -0,0 +1,25 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper_SendCoins(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.EqualError(t, keeper.SendCoins(ctx, addr3, addr1, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrap(types.ErrInsufficientToken, "insufficient account funds[abcdef01]; account has no coin").Error()) +} + +func TestKeeper_SetCoins(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + coins := types.Coins{types.Coin{Denom: defaultTokenIDFT, Amount: sdk.NewInt(-1)}} + require.EqualError(t, keeper.SetCoins(ctx, addr1, coins), sdkerrors.Wrapf(types.ErrInvalidCoin, "invalid amount: %s", coins.String()).Error()) +} diff --git a/x/collection/internal/keeper/burn.go b/x/collection/internal/keeper/burn.go new file mode 100644 index 0000000000..00832bb634 --- /dev/null +++ b/x/collection/internal/keeper/burn.go @@ -0,0 +1,235 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type BurnKeeper interface { + BurnFT(ctx sdk.Context, from sdk.AccAddress, amount types.Coins) error + BurnNFT(ctx sdk.Context, from sdk.AccAddress, tokenIDs ...string) error + BurnFTFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, amount types.Coins) error + BurnNFTFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, tokenIDs ...string) error +} + +var _ BurnKeeper = (*Keeper)(nil) + +func (k Keeper) BurnFT(ctx sdk.Context, from sdk.AccAddress, amount types.Coins) error { + if err := k.burnFT(ctx, from, from, amount); err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeBurnFT, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + ), + }) + + return nil +} + +func (k Keeper) BurnFTFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, amount types.Coins) error { + if !k.IsApproved(ctx, proxy, from) { + return sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), from.String(), k.getContractID(ctx)) + } + + if err := k.burnFT(ctx, proxy, from, amount); err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeBurnFTFrom, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + ), + }) + return nil +} + +func (k Keeper) burnFT(ctx sdk.Context, permissionOwner, tokenOwner sdk.AccAddress, amount types.Coins) error { + if err := k.isBurnable(ctx, permissionOwner, tokenOwner, amount); err != nil { + return err + } + + if err := k.BurnSupply(ctx, tokenOwner, amount); err != nil { + return err + } + return nil +} + +func (k Keeper) isBurnable(ctx sdk.Context, permissionOwner, tokenOwner sdk.AccAddress, amount types.Coins) error { + perm := types.NewBurnPermission() + if !k.HasPermission(ctx, permissionOwner, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", permissionOwner.String(), perm.String()) + } + + if !k.HasCoins(ctx, tokenOwner, amount) { + return sdkerrors.Wrapf(types.ErrInsufficientToken, "%v has not enough coins for %v", tokenOwner.String(), amount) + } + return nil +} + +func (k Keeper) BurnNFT(ctx sdk.Context, from sdk.AccAddress, tokenIDs ...string) error { + for _, tokenID := range tokenIDs { + if err := k.burnNFT(ctx, from, from, tokenID); err != nil { + return err + } + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeBurnNFT, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + ), + }) + + for _, tokenID := range tokenIDs { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeBurnNFT, + sdk.NewAttribute(types.AttributeKeyTokenID, tokenID), + ), + }) + } + + return nil +} + +func (k Keeper) BurnNFTFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, tokenIDs ...string) error { + if !k.IsApproved(ctx, proxy, from) { + return sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), from.String(), k.getContractID(ctx)) + } + + for _, tokenID := range tokenIDs { + if err := k.burnNFT(ctx, proxy, from, tokenID); err != nil { + return err + } + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeBurnNFTFrom, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + ), + }) + for _, tokenID := range tokenIDs { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeBurnNFTFrom, + sdk.NewAttribute(types.AttributeKeyTokenID, tokenID), + ), + }) + } + return nil +} + +func (k Keeper) burnNFT(ctx sdk.Context, permissionOwner, tokenOwner sdk.AccAddress, tokenID string) error { + token, err := k.GetNFT(ctx, tokenID) + if err != nil { + return err + } + + perm := types.NewBurnPermission() + if !k.HasPermission(ctx, permissionOwner, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", permissionOwner.String(), perm.String()) + } + + if !token.GetOwner().Equals(tokenOwner) { + return sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", tokenID, tokenOwner.String()) + } + + if parent, err := k.ParentOf(ctx, token.GetTokenID()); parent != nil || err != nil { + if err != nil { + return err + } + return sdkerrors.Wrapf(types.ErrBurnNonRootNFT, "TokenID(%s) has a parent", tokenID) + } + + err = k.burnNFTRecursive(ctx, token, tokenOwner) + if err != nil { + return err + } + + return nil +} + +func (k Keeper) burnNFTRecursive(ctx sdk.Context, token types.NFT, from sdk.AccAddress) error { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeOperationBurnNFT, + sdk.NewAttribute(types.AttributeKeyTokenID, token.GetTokenID()), + ), + }) + + children, err := k.ChildrenOf(ctx, token.GetTokenID()) + if err != nil { + return err + } + + for _, child := range children { + err = k.burnNFTRecursive(ctx, child.(types.NFT), from) + if err != nil { + return err + } + } + + parent, err := k.ParentOf(ctx, token.GetTokenID()) + if err != nil { + return err + } + if parent != nil { + _, err = k.detach(ctx, from, token.GetTokenID()) + if err != nil { + return err + } + } + + err = k.burnNFTInternal(ctx, token) + if err != nil { + return nil + } + + return nil +} + +func (k Keeper) burnNFTInternal(ctx sdk.Context, token types.NFT) error { + err := k.DeleteToken(ctx, token.GetTokenID()) + if err != nil { + return err + } + + if !k.HasNFTOwner(ctx, token.GetOwner(), token.GetTokenID()) { + return sdkerrors.Wrapf(types.ErrInsufficientSupply, "insufficient supply for token [%s]", k.getContractID(ctx)) + } + k.DeleteNFTOwner(ctx, token.GetOwner(), token.GetTokenID()) + k.increaseTokenTypeBurnCount(ctx, token.GetTokenType()) + return nil +} + +func (k Keeper) increaseTokenTypeBurnCount(ctx sdk.Context, tokenType string) { + store := ctx.KVStore(k.storeKey) + count := k.getTokenTypeBurnCount(ctx, tokenType) + count = count.Add(sdk.NewInt(1)) + + store.Set(types.TokenTypeBurnCount(k.getContractID(ctx), tokenType), k.cdc.MustMarshalBinaryBare(count)) +} + +func (k Keeper) getTokenTypeBurnCount(ctx sdk.Context, tokenType string) (count sdk.Int) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.TokenTypeBurnCount(k.getContractID(ctx), tokenType)) + if bz == nil { + return sdk.ZeroInt() + } + k.cdc.MustUnmarshalBinaryBare(bz, &count) + return count +} diff --git a/x/collection/internal/keeper/burn_test.go b/x/collection/internal/keeper/burn_test.go new file mode 100644 index 0000000000..12923eb23a --- /dev/null +++ b/x/collection/internal/keeper/burn_test.go @@ -0,0 +1,277 @@ +package keeper + +import ( + "context" + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func TestKeeper_BurnFT(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.NoError(t, keeper.BurnFT(ctx, addr1, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1))))) + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.BurnFT(ctx2, addr1, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr1.String(), types.NewBurnPermission().String()).Error()) + require.EqualError(t, keeper.BurnFT(ctx, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr2.String(), types.NewBurnPermission().String()).Error()) + require.EqualError(t, keeper.BurnFT(ctx, addr3, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr3.String(), types.NewBurnPermission().String()).Error()) + require.EqualError(t, keeper.BurnFT(ctx, addr1, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount)))), sdkerrors.Wrapf(types.ErrInsufficientToken, "%v has not enough coins for %v", addr1.String(), types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount)).String()).Error()) + + require.NoError(t, keeper.MintFT(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1))))) + require.EqualError(t, keeper.BurnFT(ctx, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr2, types.NewBurnPermission()).Error()) + require.EqualError(t, keeper.BurnFT(ctx, addr1, types.NewCoins(types.NewCoin("0000000200000000", sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrInsufficientToken, "%v has not enough coins for %v", addr1.String(), types.NewCoin("0000000200000000", sdk.NewInt(1)).String()).Error()) +} + +func TestKeeper_BurnFTFrom(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + prepareProxy(ctx, t) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.BurnFTFrom(ctx2, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr1.String(), addr2.String(), wrongContractID).Error()) + require.NoError(t, keeper.BurnFTFrom(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))))) + require.EqualError(t, keeper.BurnFTFrom(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrInsufficientToken, "%v has not enough coins for %v", addr2.String(), types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)).String()).Error()) + + require.NoError(t, keeper.MintFT(ctx, addr1, addr1, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1))))) + require.EqualError(t, keeper.BurnFTFrom(ctx, addr2, addr1, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr2, types.NewBurnPermission()).Error()) + require.EqualError(t, keeper.BurnFTFrom(ctx, addr1, addr2, types.NewCoins(types.NewCoin("0000000200000000", sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrInsufficientToken, "%v has not enough coins for %v", addr2.String(), types.NewCoin("0000000200000000", sdk.NewInt(1)).String()).Error()) +} + +func TestKeeper_BurnNFT(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + i, err := keeper.GetNFTCountInt(ctx, defaultTokenType, types.QueryNFTCount) + require.NoError(t, err) + require.Equal(t, int64(5), i.Int64()) + i, err = keeper.GetNFTCountInt(ctx, defaultTokenType, types.QueryNFTMint) + require.NoError(t, err) + require.Equal(t, int64(5), i.Int64()) + i, err = keeper.GetNFTCountInt(ctx, defaultTokenType, types.QueryNFTBurn) + require.NoError(t, err) + require.Equal(t, int64(0), i.Int64()) + + require.NoError(t, keeper.BurnNFT(ctx, addr1, defaultTokenID4)) + require.EqualError(t, keeper.BurnNFT(ctx, addr1, defaultTokenID4), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID4).Error()) + require.EqualError(t, keeper.BurnNFT(ctx, addr2, defaultTokenID4), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID4).Error()) + require.EqualError(t, keeper.BurnNFT(ctx, addr3, defaultTokenID4), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID4).Error()) + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.BurnNFT(ctx2, addr1, defaultTokenID4), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", wrongContractID, defaultTokenID4).Error()) + + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + require.NoError(t, keeper.BurnNFT(ctx, addr1, defaultTokenID1)) + + i, err = keeper.GetNFTCountInt(ctx, defaultTokenType, types.QueryNFTCount) + require.NoError(t, err) + require.Equal(t, int64(1), i.Int64()) + i, err = keeper.GetNFTCountInt(ctx, defaultTokenType, types.QueryNFTMint) + require.NoError(t, err) + require.Equal(t, int64(5), i.Int64()) + i, err = keeper.GetNFTCountInt(ctx, defaultTokenType, types.QueryNFTBurn) + require.NoError(t, err) + require.Equal(t, int64(4), i.Int64()) +} + +func TestKeeper_BurnNFTFrom(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + prepareProxy(ctx, t) + + require.NoError(t, keeper.BurnNFTFrom(ctx, addr1, addr2, defaultTokenID4)) + require.EqualError(t, keeper.BurnNFTFrom(ctx, addr1, addr2, defaultTokenID4), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID4).Error()) + require.EqualError(t, keeper.BurnNFTFrom(ctx, addr3, addr2, defaultTokenID4), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr3.String(), addr2.String(), defaultContractID).Error()) + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.BurnNFTFrom(ctx2, addr1, addr2, defaultTokenID4), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr1.String(), addr2.String(), wrongContractID).Error()) + + require.NoError(t, keeper.Attach(ctx, addr2, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr2, defaultTokenID2, defaultTokenID3)) + require.NoError(t, keeper.BurnNFTFrom(ctx, addr1, addr2, defaultTokenID1)) +} + +func TestMintBurn(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + const ( + wrongTokenID = "12345678" + ) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID2)) + require.EqualError(t, keeper.MintNFT(ctx2, addr1, types.NewNFT(defaultContractID2, wrongTokenID, defaultName, defaultMeta, addr1)), sdkerrors.Wrapf(types.ErrTokenTypeNotExist, "ContractID: %s, TokenType: %s", defaultContractID2, wrongTokenID[:types.TokenTypeLength]).Error()) + require.EqualError(t, keeper.MintNFT(ctx, addr3, types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1)), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr3.String(), types.NewMintPermission().String()).Error()) + + require.NoError(t, keeper.BurnFT(ctx, addr1, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))))) + require.EqualError(t, keeper.BurnNFT(ctx, addr1, wrongTokenID), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, wrongTokenID).Error()) + require.EqualError(t, keeper.BurnNFT(ctx, addr3, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr3.String(), types.NewBurnPermission().String()).Error()) +} + +func composeNFTs(t *testing.T, ctx sdk.Context) { + prepareCollectionTokens(ctx, t) + + // attach token1 <- token2 (basic case) : success + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + // attach token2 <- token3 (attach to a child): success + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + // attach token1 <- token4 (attach to a root): success + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID4)) +} + +func TestBurnRootComposedNFT(t *testing.T) { + ctx := cacheKeeper() + composeNFTs(t, ctx) + // token1 -+- token2 --- token3 + // +- token4 + + require.NoError(t, keeper.BurnNFT(ctx, addr1, defaultTokenID1)) + + _, err := keeper.GetNFT(ctx, defaultTokenID1) + require.Error(t, err) + _, err = keeper.GetNFT(ctx, defaultTokenID2) + require.Error(t, err) + _, err = keeper.GetNFT(ctx, defaultTokenID3) + require.Error(t, err) + _, err = keeper.GetNFT(ctx, defaultTokenID4) + require.Error(t, err) + + require.False(t, keeper.HasToken(ctx, defaultTokenID1)) + require.False(t, keeper.HasToken(ctx, defaultTokenID2)) + require.False(t, keeper.HasToken(ctx, defaultTokenID3)) + require.False(t, keeper.HasToken(ctx, defaultTokenID4)) +} + +func TestBurnNonRootComposedNFT(t *testing.T) { + // only the root tokens can be burned + ctx := cacheKeeper() + + composeNFTs(t, ctx) + // token1 -+- token2 --- token3 + // +- token4 + + require.Error(t, keeper.BurnNFT(ctx, addr1, defaultTokenID2)) + require.Error(t, keeper.BurnNFT(ctx, addr1, defaultTokenID3)) + require.Error(t, keeper.BurnNFT(ctx, addr1, defaultTokenID4)) + + _, err := keeper.GetNFT(ctx, defaultTokenID1) + require.NoError(t, err) + _, err = keeper.GetNFT(ctx, defaultTokenID2) + require.NoError(t, err) + _, err = keeper.GetNFT(ctx, defaultTokenID3) + require.NoError(t, err) + _, err = keeper.GetNFT(ctx, defaultTokenID4) + require.NoError(t, err) + + require.True(t, keeper.HasToken(ctx, defaultTokenID1)) + require.True(t, keeper.HasToken(ctx, defaultTokenID2)) + require.True(t, keeper.HasToken(ctx, defaultTokenID3)) + require.True(t, keeper.HasToken(ctx, defaultTokenID4)) +} + +func TestBurnNFTFromSuccess(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + // success case + // addr1 has: burn permission, approved + // addr2 has: token + + // attach token1 <- token2 (basic case) : success + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + // attach token2 <- token3 (attach to a child): success + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + // attach token1 <- token4 (attach to a root): success + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID4)) + + // transfer tokens to addr2 + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID1)) + require.NoError(t, keeper.TransferFT(ctx, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount)))) + + // addr2 approves addr1 for the contractID + require.NoError(t, keeper.SetApproved(ctx, addr1, addr2)) + + // test burnNFTFrom + require.NoError(t, keeper.BurnNFTFrom(ctx, addr1, addr2, defaultTokenID1)) + require.NoError(t, keeper.BurnFTFrom(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))))) + + _, err := keeper.GetNFT(ctx, defaultTokenID1) + require.Error(t, err) + _, err = keeper.GetNFT(ctx, defaultTokenID2) + require.Error(t, err) + _, err = keeper.GetNFT(ctx, defaultTokenID3) + require.Error(t, err) + _, err = keeper.GetNFT(ctx, defaultTokenID4) + require.Error(t, err) + + balance, err := keeper.GetBalance(ctx, defaultTokenID1, addr1) + require.NoError(t, err) + require.Equal(t, int64(0), balance.Int64()) + balance, err = keeper.GetBalance(ctx, defaultTokenID2, addr1) + require.NoError(t, err) + require.Equal(t, int64(0), balance.Int64()) + balance, err = keeper.GetBalance(ctx, defaultTokenID3, addr1) + require.NoError(t, err) + require.Equal(t, int64(0), balance.Int64()) + balance, err = keeper.GetBalance(ctx, defaultTokenID4, addr1) + require.NoError(t, err) + require.Equal(t, int64(0), balance.Int64()) + balance, err = keeper.GetBalance(ctx, defaultTokenIDFT, addr1) + require.NoError(t, err) + require.Equal(t, int64(0), balance.Int64()) +} + +func TestBurnNFTFromFailure1(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + // failure case1 + // addr1 has: burn permission, approved, token + // addr2 has: nothing + + // addr2 approves addr1 for the contractID + require.NoError(t, keeper.SetApproved(ctx, addr1, addr2)) + + // test burnNFTFrom, burnFTFrom fail + require.EqualError(t, keeper.BurnNFTFrom(ctx, addr1, addr2, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", defaultTokenID1, addr2.String()).Error()) + require.EqualError(t, keeper.BurnFTFrom(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrInsufficientToken, "%v has not enough coins for %v", addr2.String(), types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)).String()).Error()) +} + +func TestBurnNFTFromFailure2(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + // failure case2 + // addr1 has: burn permission (not approved) + // addr2 has: token + + // transfer tokens to addr2 + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID1)) + require.NoError(t, keeper.TransferFT(ctx, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))) + + // test burnNFTFrom fail + require.EqualError(t, keeper.BurnNFTFrom(ctx, addr1, addr2, defaultTokenID1), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr1.String(), addr2.String(), defaultContractID).Error()) + require.EqualError(t, keeper.BurnFTFrom(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr1.String(), addr2.String(), defaultContractID).Error()) +} + +func TestBurnNFTFromFailure3(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + // failure case3 + // addr2 has: approved (no permission) + // addr3 has: token + + // transfer tokens to addr2 + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr3, defaultTokenID1)) + require.NoError(t, keeper.TransferFT(ctx, addr1, addr3, types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))) + // addr3 approves addr2 for the contractID + require.NoError(t, keeper.SetApproved(ctx, addr2, addr3)) + + // test burnNFTFrom fail + require.EqualError(t, keeper.BurnNFTFrom(ctx, addr2, addr3, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr2.String(), types.NewBurnPermission().String()).Error()) + require.EqualError(t, keeper.BurnFTFrom(ctx, addr2, addr3, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(1)))), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr2.String(), types.NewBurnPermission().String()).Error()) +} diff --git a/x/collection/internal/keeper/collection.go b/x/collection/internal/keeper/collection.go new file mode 100644 index 0000000000..64074ca3eb --- /dev/null +++ b/x/collection/internal/keeper/collection.go @@ -0,0 +1,133 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type CollectionKeeper interface { + CreateCollection(ctx sdk.Context, collection types.Collection, owner sdk.AccAddress) error + ExistCollection(ctx sdk.Context) bool + GetCollection(ctx sdk.Context) (collection types.Collection, err error) + SetCollection(ctx sdk.Context, collection types.Collection) error + UpdateCollection(ctx sdk.Context, collection types.Collection) error +} + +var _ CollectionKeeper = (*Keeper)(nil) + +func (k Keeper) CreateCollection(ctx sdk.Context, collection types.Collection, owner sdk.AccAddress) error { + err := k.SetCollection(ctx, collection) + if err != nil { + return err + } + k.SetSupply(ctx, types.DefaultSupply(collection.GetContractID())) + + perms := types.NewPermissions( + types.NewIssuePermission(), + types.NewMintPermission(), + types.NewBurnPermission(), + types.NewModifyPermission(), + ) + for _, perm := range perms { + k.AddPermission(ctx, owner, perm) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeCreateCollection, + sdk.NewAttribute(types.AttributeKeyContractID, collection.GetContractID()), + sdk.NewAttribute(types.AttributeKeyName, collection.GetName()), + sdk.NewAttribute(types.AttributeKeyOwner, owner.String()), + ), + sdk.NewEvent( + types.EventTypeGrantPermToken, + sdk.NewAttribute(types.AttributeKeyTo, owner.String()), + sdk.NewAttribute(types.AttributeKeyContractID, collection.GetContractID()), + ), + }) + for _, perm := range perms { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeGrantPermToken, + sdk.NewAttribute(types.AttributeKeyPerm, perm.String()), + ), + }) + } + + return nil +} + +func (k Keeper) ExistCollection(ctx sdk.Context) bool { + store := ctx.KVStore(k.storeKey) + return store.Has(types.CollectionKey(k.getContractID(ctx))) +} + +func (k Keeper) GetCollection(ctx sdk.Context) (collection types.Collection, err error) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.CollectionKey(k.getContractID(ctx))) + if bz == nil { + return collection, sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", k.getContractID(ctx)) + } + + collection = k.mustDecodeCollection(bz) + return collection, nil +} + +func (k Keeper) SetCollection(ctx sdk.Context, collection types.Collection) error { + store := ctx.KVStore(k.storeKey) + if store.Has(types.CollectionKey(collection.GetContractID())) { + return sdkerrors.Wrapf(types.ErrCollectionExist, "ContractID: %s", collection.GetContractID()) + } + + store.Set(types.CollectionKey(collection.GetContractID()), k.cdc.MustMarshalBinaryBare(collection)) + k.setNextTokenTypeFT(ctx, types.ReservedEmpty) + k.setNextTokenTypeNFT(ctx, types.ReservedEmptyNFT) + return nil +} + +func (k Keeper) UpdateCollection(ctx sdk.Context, collection types.Collection) error { + store := ctx.KVStore(k.storeKey) + if !store.Has(types.CollectionKey(collection.GetContractID())) { + return sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", collection.GetContractID()) + } + + store.Set(types.CollectionKey(collection.GetContractID()), k.cdc.MustMarshalBinaryBare(collection)) + return nil +} + +func (k Keeper) GetAllCollections(ctx sdk.Context) types.Collections { + var collections types.Collections + appendCollection := func(collection types.Collection) (stop bool) { + collections = append(collections, collection) + return false + } + k.iterateCollections(ctx, appendCollection) + return collections +} + +func (k Keeper) iterateCollections(ctx sdk.Context, process func(types.Collection) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, types.CollectionKey("")) + defer iter.Close() + for { + if !iter.Valid() { + return + } + val := iter.Value() + collection := k.mustDecodeCollection(val) + if process(collection) { + return + } + iter.Next() + } +} + +func (k Keeper) mustDecodeCollection(collectionByte []byte) types.Collection { + var collection types.Collection + err := k.cdc.UnmarshalBinaryBare(collectionByte, &collection) + if err != nil { + panic(err) + } + return collection +} diff --git a/x/collection/internal/keeper/collection_test.go b/x/collection/internal/keeper/collection_test.go new file mode 100644 index 0000000000..8f1e8812ea --- /dev/null +++ b/x/collection/internal/keeper/collection_test.go @@ -0,0 +1,108 @@ +package keeper + +import ( + "context" + "testing" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" +) + +func TestKeeper_NewContractID(t *testing.T) { + ctx := cacheKeeper() + contractID := keeper.NewContractID(ctx) + require.Equal(t, len(contractID), 8) + contractID2 := keeper.NewContractID(ctx) + require.NotEqual(t, contractID, contractID2) +} + +func TestKeeper_CreateCollection(t *testing.T) { + ctx := cacheKeeper() + contractID := keeper.NewContractID(ctx) + + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(contractID, "MyCollection", "meta", "base url"), addr1)) + require.EqualError(t, keeper.CreateCollection(ctx, types.NewCollection(contractID, "MyCollection", "meta", "base url"), addr1), sdkerrors.Wrapf(types.ErrCollectionExist, "ContractID: %s", contractID).Error()) +} + +func TestKeeper_ExistCollection(t *testing.T) { + ctx := cacheKeeper() + contractID := keeper.NewContractID(ctx) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, contractID)) + + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(contractID, "MyCollection", "meta", "base url"), addr1)) + require.True(t, keeper.ExistCollection(ctx)) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, "abcd1234")) + require.False(t, keeper.ExistCollection(ctx)) +} + +func TestKeeper_GetCollection(t *testing.T) { + ctx := cacheKeeper() + contractID := keeper.NewContractID(ctx) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, contractID)) + + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(contractID, "MyCollection", "meta", "base url"), addr1)) + collection, err := keeper.GetCollection(ctx) + require.NoError(t, err) + require.Equal(t, collection.GetContractID(), contractID) + require.Equal(t, collection.GetName(), "MyCollection") + require.Equal(t, collection.GetBaseImgURI(), "base url") + + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, "abcd1234")) + _, err = keeper.GetCollection(ctx) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: abcd1234").Error()) +} + +func TestKeeper_SetCollection(t *testing.T) { + ctx := cacheKeeper() + contractID := keeper.NewContractID(ctx) + + require.NoError(t, keeper.SetCollection(ctx, types.NewCollection(contractID, "MyCollection", "meta", "base url"))) + require.EqualError(t, keeper.SetCollection(ctx, types.NewCollection(contractID, "MyCollection", "meta", "base url")), sdkerrors.Wrapf(types.ErrCollectionExist, "ContractID: %s", contractID).Error()) +} + +func TestKeeper_UpdateCollection(t *testing.T) { + ctx := cacheKeeper() + contractID := keeper.NewContractID(ctx) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, contractID)) + + require.EqualError(t, keeper.UpdateCollection(ctx, types.NewCollection(contractID, "MyCollection", "meta", "base url")), sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", contractID).Error()) + require.NoError(t, keeper.SetCollection(ctx, types.NewCollection(contractID, "MyCollection", "meta", "base url"))) + require.NoError(t, keeper.UpdateCollection(ctx, types.NewCollection(contractID, "MyCollection2", "meta", "base url2"))) + + collection, err := keeper.GetCollection(ctx) + require.NoError(t, err) + require.Equal(t, collection.GetContractID(), contractID) + require.Equal(t, collection.GetName(), "MyCollection2") + require.Equal(t, collection.GetBaseImgURI(), "base url2") +} + +func TestKeeper_GetAllCollections(t *testing.T) { + ctx := cacheKeeper() + contractID1 := keeper.NewContractID(ctx) + contractID2 := keeper.NewContractID(ctx) + contractID3 := keeper.NewContractID(ctx) + + collections := keeper.GetAllCollections(ctx) + require.Equal(t, len(collections), 0) + + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(contractID1, "MyCollection1", "meta1", "base url1"), addr1)) + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(contractID2, "MyCollection2", "meta2", "base url2"), addr1)) + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(contractID3, "MyCollection3", "meta3", "base url3"), addr1)) + + collections = keeper.GetAllCollections(ctx) + require.Equal(t, len(collections), 3) + require.Equal(t, collections[2].GetContractID(), contractID1) + require.Equal(t, collections[2].GetName(), "MyCollection1") + require.Equal(t, collections[2].GetMeta(), "meta1") + require.Equal(t, collections[2].GetBaseImgURI(), "base url1") + require.Equal(t, collections[1].GetContractID(), contractID2) + require.Equal(t, collections[1].GetName(), "MyCollection2") + require.Equal(t, collections[1].GetMeta(), "meta2") + require.Equal(t, collections[1].GetBaseImgURI(), "base url2") + require.Equal(t, collections[0].GetContractID(), contractID3) + require.Equal(t, collections[0].GetName(), "MyCollection3") + require.Equal(t, collections[0].GetMeta(), "meta3") + require.Equal(t, collections[0].GetBaseImgURI(), "base url3") +} diff --git a/x/collection/internal/keeper/composable.go b/x/collection/internal/keeper/composable.go new file mode 100644 index 0000000000..db7be93fb4 --- /dev/null +++ b/x/collection/internal/keeper/composable.go @@ -0,0 +1,417 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +var ChildExists = []byte{1} + +type ComposeKeeper interface { + Attach(ctx sdk.Context, from sdk.AccAddress, toTokenID string, tokenID string) error + AttachFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, toTokenID string, tokenID string) error + Detach(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, tokenID string) error + DetachFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, to sdk.AccAddress, tokenID string) error + RootOf(ctx sdk.Context, tokenID string) (types.NFT, error) + ParentOf(ctx sdk.Context, tokenID string) (types.NFT, error) + ChildrenOf(ctx sdk.Context, tokenID string) (types.Tokens, error) +} + +func (k Keeper) eventRootChanged(ctx sdk.Context, tokenID string) { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeOperationRootChanged, + sdk.NewAttribute(types.AttributeKeyTokenID, tokenID), + ), + }) + + children := k.getChildren(ctx, tokenID) + for _, child := range children { + k.eventRootChanged(ctx, child.GetTokenID()) + } +} + +func (k Keeper) Attach(ctx sdk.Context, from sdk.AccAddress, toTokenID string, tokenID string) error { + if err := k.attach(ctx, from, toTokenID, tokenID); err != nil { + return err + } + + newRoot, err := k.RootOf(ctx, toTokenID) + if err != nil { + return err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeAttachToken, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyToTokenID, toTokenID), + sdk.NewAttribute(types.AttributeKeyTokenID, tokenID), + sdk.NewAttribute(types.AttributeKeyOldRoot, tokenID), + sdk.NewAttribute(types.AttributeKeyNewRoot, newRoot.GetTokenID()), + ), + }) + + k.eventRootChanged(ctx, tokenID) + + return nil +} + +func (k Keeper) AttachFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, toTokenID string, tokenID string) error { + if !k.IsApproved(ctx, proxy, from) { + return sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), from.String(), k.getContractID(ctx)) + } + + if err := k.attach(ctx, from, toTokenID, tokenID); err != nil { + return err + } + + newRoot, err := k.RootOf(ctx, toTokenID) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeAttachFrom, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyToTokenID, toTokenID), + sdk.NewAttribute(types.AttributeKeyTokenID, tokenID), + sdk.NewAttribute(types.AttributeKeyOldRoot, tokenID), + sdk.NewAttribute(types.AttributeKeyNewRoot, newRoot.GetTokenID()), + ), + }) + + k.eventRootChanged(ctx, tokenID) + + return nil +} + +func (k Keeper) attach(ctx sdk.Context, from sdk.AccAddress, parentID string, childID string) error { + store := ctx.KVStore(k.storeKey) + + if parentID == childID { + return sdkerrors.Wrapf(types.ErrCannotAttachToItself, "TokenID: %s", childID) + } + + childToken, err := k.GetNFT(ctx, childID) + if err != nil { + return err + } + + if !from.Equals(childToken.GetOwner()) { + return sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", childID, from.String()) + } + + toToken, err := k.GetNFT(ctx, parentID) + if err != nil { + return err + } + + if !from.Equals(toToken.GetOwner()) { + return sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", parentID, from.String()) + } + + // verify token should be a root + childToParentKey := types.TokenChildToParentKey(k.getContractID(ctx), childID) + if store.Has(childToParentKey) { + return sdkerrors.Wrapf(types.ErrTokenAlreadyAChild, "TokenID: %s", childID) + } + + // verify no circulation(toToken must not be a descendant of token) + rootOfToToken, err := k.RootOf(ctx, parentID) + if err != nil { + return err + } + parentToken, err := k.GetNFT(ctx, parentID) + if err != nil { + return err + } + if rootOfToToken.GetTokenID() == childID { + return sdkerrors.Wrapf(types.ErrCannotAttachToADescendant, "TokenID: %s, ToTokenID: %s", childID, parentID) + } + + if err := k.checkDepthAndWidth(ctx, rootOfToToken.GetTokenID(), parentID, childID); err != nil { + return err + } + + parentToChildKey := types.TokenParentToChildKey(k.getContractID(ctx), parentID, childID) + if store.Has(parentToChildKey) { + panic("token is already a child of some other") + } + + if !from.Equals(parentToken.GetOwner()) { + if err := k.moveNFToken(ctx, from, parentToken.GetOwner(), childToken); err != nil { + return err + } + } + + store.Set(childToParentKey, k.mustEncodeString(parentID)) + store.Set(parentToChildKey, k.mustEncodeString(childID)) + + return nil +} + +func (k Keeper) checkDepthAndWidth(ctx sdk.Context, rootID, parentID, childID string) error { + rootTable := k.GetDepthWidthTable(ctx, rootID) + childTable := k.GetDepthWidthTable(ctx, childID) + + parentDepth := k.GetDepthFromRoot(ctx, parentID) + + // root: token1 - token2 - token3 => depth of token3 is 2 + // child: token4 - token5 => length 2 + // attach result: token1 - token2 - token3 - token4 - token5 => depth 4 + // [depth of token3] + [len([token4,token5])] should be result depth + resultDepth := uint64(parentDepth + len(childTable)) + if resultDepth > k.GetParams(ctx).MaxComposableDepth { + return sdkerrors.Wrapf(types.ErrCompositionTooDeep, "Depth: %d", resultDepth) + } + + // root table: [1, 2, 3, 4, 5, 6, 7, 8] + // child table: [1, 3, 5, 7, 9] + // if the child is attached after depth 3, + // then the merged width table should be [1, 2, 3, 4, 5+1, 6+3, 7+5, 8+7, 9] + maxComposableWidth := k.GetParams(ctx).MaxComposableWidth + for curDepth, idx := parentDepth+1, 0; curDepth < len(rootTable) && idx < len(childTable); { + totalWidth := uint64(rootTable[curDepth] + childTable[idx]) + if totalWidth > maxComposableWidth { + return sdkerrors.Wrapf(types.ErrCompositionTooWide, "Width: %d (at depth %d)", totalWidth, curDepth) + } + curDepth++ + idx++ + } + + return nil +} + +// Gets the depth-width(count) table (array) +// +// lv0(1) lv1(2) lv2(3) lv3(2) lv4(1) +// token1 -+- token2 --- token4 --- token7 +// +- token3 -+- token5 --- token8 --- token9 +// +- token6 +// +// then returns [1, 2, 3, 2, 1] +// and len([1, 2, 3, 2, 1]) - 1 represents the depth of token1's children +func (k Keeper) GetDepthWidthTable(ctx sdk.Context, tokenID string) []int { + table := make([]int, 1) + table[0] = 1 + k.fillDepthWidthTable(ctx, tokenID, &table, 1) + return table +} + +func (k Keeper) fillDepthWidthTable(ctx sdk.Context, tokenID string, table *[]int, index int) { + count := 0 + + k.iterateChildren(ctx, tokenID, func(tokenID string) bool { + k.fillDepthWidthTable(ctx, tokenID, table, index+1) + count++ + return false + }) + + // if count = 0, the current is leaf, so doesn't need to insert a row + if count > 0 { + for len(*table) <= index { + *table = append(*table, 0) + } + // fills the children count of current depth + (*table)[index] += count + } +} + +func (k Keeper) GetDepthFromRoot(ctx sdk.Context, tokenID string) int { + store := ctx.KVStore(k.storeKey) + + depth := 0 + for nextID := tokenID; ; depth++ { + childToParentKey := types.TokenChildToParentKey(k.getContractID(ctx), nextID) + bz := store.Get(childToParentKey) + if bz == nil { + break + } + nextID = k.mustDecodeString(bz) + } + return depth +} + +func (k Keeper) Detach(ctx sdk.Context, from sdk.AccAddress, tokenID string) error { + oldRoot, err := k.RootOf(ctx, tokenID) + if err != nil { + return err + } + + parentTokenID, err := k.detach(ctx, from, tokenID) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeDetachToken, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyFromTokenID, parentTokenID), + sdk.NewAttribute(types.AttributeKeyTokenID, tokenID), + sdk.NewAttribute(types.AttributeKeyOldRoot, oldRoot.GetTokenID()), + sdk.NewAttribute(types.AttributeKeyNewRoot, tokenID), + ), + }) + + k.eventRootChanged(ctx, tokenID) + + return nil +} + +// nolint:dupl +func (k Keeper) DetachFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, tokenID string) error { + oldRoot, err := k.RootOf(ctx, tokenID) + if err != nil { + return err + } + + if !k.IsApproved(ctx, proxy, from) { + return sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), from.String(), k.getContractID(ctx)) + } + + parentTokenID, err := k.detach(ctx, from, tokenID) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeDetachFrom, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyFromTokenID, parentTokenID), + sdk.NewAttribute(types.AttributeKeyTokenID, tokenID), + sdk.NewAttribute(types.AttributeKeyOldRoot, oldRoot.GetTokenID()), + sdk.NewAttribute(types.AttributeKeyNewRoot, tokenID), + ), + }) + + k.eventRootChanged(ctx, tokenID) + + return nil +} + +func (k Keeper) detach(ctx sdk.Context, from sdk.AccAddress, childID string) (string, error) { + store := ctx.KVStore(k.storeKey) + + childToken, err := k.GetNFT(ctx, childID) + if err != nil { + return "", err + } + + if !from.Equals(childToken.GetOwner()) { + return "", sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", childID, from.String()) + } + + childToParentKey := types.TokenChildToParentKey(k.getContractID(ctx), childID) + if !store.Has(childToParentKey) { + return "", sdkerrors.Wrapf(types.ErrTokenNotAChild, "TokenID: %s", childID) + } + + bz := store.Get(childToParentKey) + parentID := k.mustDecodeString(bz) + + _, err = k.GetNFT(ctx, parentID) + if err != nil { + return "", err + } + + parentToChildKey := types.TokenParentToChildKey(k.getContractID(ctx), parentID, childID) + if !store.Has(parentToChildKey) { + panic("token is not a child of some other") + } + + store.Delete(childToParentKey) + store.Delete(parentToChildKey) + + return parentID, nil +} + +func (k Keeper) RootOf(ctx sdk.Context, tokenID string) (types.NFT, error) { + store := ctx.KVStore(k.storeKey) + + token, err := k.GetNFT(ctx, tokenID) + if err != nil { + return nil, err + } + + for childToParentKey := types.TokenChildToParentKey(k.getContractID(ctx), token.GetTokenID()); store.Has(childToParentKey); { + bz := store.Get(childToParentKey) + parentTokenID := k.mustDecodeString(bz) + + token, err = k.GetNFT(ctx, parentTokenID) + if err != nil { + return nil, err + } + childToParentKey = types.TokenChildToParentKey(k.getContractID(ctx), token.GetTokenID()) + } + + return token, nil +} + +func (k Keeper) ParentOf(ctx sdk.Context, tokenID string) (types.NFT, error) { + store := ctx.KVStore(k.storeKey) + + token, err := k.GetNFT(ctx, tokenID) + if err != nil { + return nil, err + } + childToParentKey := types.TokenChildToParentKey(k.getContractID(ctx), token.GetTokenID()) + if store.Has(childToParentKey) { + bz := store.Get(childToParentKey) + parentTokenID := k.mustDecodeString(bz) + + return k.GetNFT(ctx, parentTokenID) + } + return nil, nil +} + +func (k Keeper) ChildrenOf(ctx sdk.Context, tokenID string) (types.Tokens, error) { + _, err := k.GetNFT(ctx, tokenID) + if err != nil { + return nil, err + } + tokens := k.getChildren(ctx, tokenID) + + return tokens, nil +} + +func (k Keeper) getChildren(ctx sdk.Context, parentID string) (tokens types.Tokens) { + getToken := func(tokenID string) bool { + token, err := k.GetNFT(ctx, tokenID) + if err != nil { + panic(err) + } + tokens = append(tokens, token) + return false + } + + k.iterateChildren(ctx, parentID, getToken) + + return tokens +} + +func (k Keeper) iterateChildren(ctx sdk.Context, parentID string, process func(string) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, types.TokenParentToChildSubKey(k.getContractID(ctx), parentID)) + defer iter.Close() + for { + if !iter.Valid() { + return + } + val := iter.Value() + tokenID := k.mustDecodeString(val) + if process(tokenID) { + return + } + iter.Next() + } +} diff --git a/x/collection/internal/keeper/composable_test.go b/x/collection/internal/keeper/composable_test.go new file mode 100644 index 0000000000..d5cd57ebd1 --- /dev/null +++ b/x/collection/internal/keeper/composable_test.go @@ -0,0 +1,491 @@ +package keeper + +import ( + "context" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func TestKeeper_Attach(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID1), sdkerrors.Wrapf(types.ErrCannotAttachToItself, "TokenID: %s", defaultTokenID1).Error()) + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID6), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID6).Error()) + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.Attach(ctx2, addr1, defaultTokenID1, defaultTokenID2), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", wrongContractID, defaultTokenID2).Error()) + require.EqualError(t, keeper.Attach(ctx, addr2, defaultTokenID1, defaultTokenID2), sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %v", defaultTokenID2, addr2.String()).Error()) + + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID6, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID6).Error()) + require.EqualError(t, keeper.Attach(ctx, addr2, defaultTokenID1, defaultTokenID5), sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", defaultTokenID1, addr2.String()).Error()) + + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID3, defaultTokenID2), sdkerrors.Wrapf(types.ErrTokenAlreadyAChild, "TokenID: %s", defaultTokenID2).Error()) + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID1), sdkerrors.Wrapf(types.ErrCannotAttachToADescendant, "TokenID: %s, ToTokenID: %s", defaultTokenID1, defaultTokenID2).Error()) +} + +func TestKeeper_AttachFrom(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.EqualError(t, keeper.AttachFrom(ctx, addr1, addr2, defaultTokenID1, defaultTokenID2), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr1.String(), addr2.String(), defaultContractID).Error()) + prepareProxy(ctx, t) + require.NoError(t, keeper.AttachFrom(ctx, addr1, addr2, defaultTokenID1, defaultTokenID2)) +} + +func TestKeeper_Detach(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.EqualError(t, keeper.Detach(ctx, addr1, defaultTokenID6), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID6).Error()) + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.Detach(ctx2, addr1, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", wrongContractID, defaultTokenID1).Error()) + require.EqualError(t, keeper.Detach(ctx, addr2, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", defaultTokenID1, addr2.String()).Error()) + require.EqualError(t, keeper.Detach(ctx, addr1, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotAChild, "TokenID: %s", defaultTokenID1).Error()) + require.EqualError(t, keeper.Detach(ctx, addr1, defaultTokenIDFT), sdkerrors.Wrapf(types.ErrTokenNotNFT, "TokenID: %s", defaultTokenIDFT).Error()) + + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Detach(ctx, addr1, defaultTokenID2)) +} + +func TestKeeper_DetachFrom(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.EqualError(t, keeper.DetachFrom(ctx, addr1, addr2, defaultTokenID2), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr1.String(), addr2.String(), defaultContractID).Error()) + prepareProxy(ctx, t) + require.NoError(t, keeper.Attach(ctx, addr2, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.DetachFrom(ctx, addr1, addr2, defaultTokenID2)) +} + +func TestKeeper_RootOf(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + _, err := keeper.RootOf(ctx, defaultTokenID6) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID6).Error()) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + _, err = keeper.RootOf(ctx2, defaultTokenID1) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", wrongContractID, defaultTokenID1).Error()) + + _, err = keeper.RootOf(ctx, defaultTokenIDFT) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotNFT, "TokenID: %s", defaultTokenIDFT).Error()) + + nft, err := keeper.RootOf(ctx, defaultTokenID1) + require.NoError(t, err) + require.Equal(t, nft.GetContractID(), defaultContractID) + require.Equal(t, nft.GetTokenID(), defaultTokenID1) + + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + + nft, err = keeper.RootOf(ctx, defaultTokenID3) + require.NoError(t, err) + require.Equal(t, nft.GetContractID(), defaultContractID) + require.Equal(t, nft.GetTokenID(), defaultTokenID1) +} + +func TestKeeper_ParentOf(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + _, err := keeper.ParentOf(ctx, defaultTokenID6) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID6).Error()) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + _, err = keeper.ParentOf(ctx2, defaultTokenID1) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", wrongContractID, defaultTokenID1).Error()) + + _, err = keeper.ParentOf(ctx, defaultTokenIDFT) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotNFT, "TokenID: %s", defaultTokenIDFT).Error()) + + nft, err := keeper.ParentOf(ctx, defaultTokenID1) + require.NoError(t, err) + require.Equal(t, nft, nil) + + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + + nft, err = keeper.ParentOf(ctx, defaultTokenID3) + require.NoError(t, err) + require.Equal(t, nft.GetContractID(), defaultContractID) + require.Equal(t, nft.GetTokenID(), defaultTokenID2) +} + +func TestKeeper_ChildrenOf(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + _, err := keeper.ChildrenOf(ctx, defaultTokenID6) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID6).Error()) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + _, err = keeper.ChildrenOf(ctx2, defaultTokenID1) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", wrongContractID, defaultTokenID1).Error()) + + _, err = keeper.ChildrenOf(ctx, defaultTokenIDFT) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotNFT, "TokenID: %s", defaultTokenIDFT).Error()) + + tokens, err := keeper.ChildrenOf(ctx, defaultTokenID1) + require.NoError(t, err) + require.Equal(t, len(tokens), 0) + + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID4)) + + tokens, err = keeper.ChildrenOf(ctx, defaultTokenID1) + require.NoError(t, err) + require.Equal(t, len(tokens), 2) + require.Equal(t, tokens[0].GetTokenID(), defaultTokenID2) + require.Equal(t, tokens[1].GetTokenID(), defaultTokenID4) + + tokens, err = keeper.ChildrenOf(ctx, defaultTokenID2) + require.NoError(t, err) + require.Equal(t, len(tokens), 1) + require.Equal(t, tokens[0].GetTokenID(), defaultTokenID3) +} + +func TestAttachDetachScenario(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + // + // attach success cases + // + + // attach token1 <- token2 (basic case) : success + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + + // attach token2 <- token3 (attach to a child): success + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + + // attach token1 <- token4 (attach to a root): success + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID4)) + + // verify the relations + + // root of token1 is token1 + rootOfToken1, err1 := keeper.RootOf(ctx, defaultTokenID1) + require.NoError(t, err1) + require.Equal(t, rootOfToken1.GetTokenID(), defaultTokenID1) + + // root of token2 is token1 + rootOfToken2, err2 := keeper.RootOf(ctx, defaultTokenID2) + require.NoError(t, err2) + require.Equal(t, rootOfToken2.GetTokenID(), defaultTokenID1) + + // root of token3 is token1 + rootOfToken3, err3 := keeper.RootOf(ctx, defaultTokenID3) + require.NoError(t, err3) + require.Equal(t, rootOfToken3.GetTokenID(), defaultTokenID1) + + // root of token4 is token1 + rootOfToken4, err4 := keeper.RootOf(ctx, defaultTokenID4) + require.NoError(t, err4) + require.Equal(t, rootOfToken4.GetTokenID(), defaultTokenID1) + + // parent of token1 is nil + parentOfToken1, err5 := keeper.ParentOf(ctx, defaultTokenID1) + require.NoError(t, err5) + require.Nil(t, parentOfToken1) + + // parent of token2 is token1 + parentOfToken2, err6 := keeper.ParentOf(ctx, defaultTokenID2) + require.NoError(t, err6) + require.Equal(t, parentOfToken2.GetTokenID(), defaultTokenID1) + + // parent of token3 is token2 + parentOfToken3, err7 := keeper.ParentOf(ctx, defaultTokenID3) + require.NoError(t, err7) + require.Equal(t, parentOfToken3.GetTokenID(), defaultTokenID2) + + // parent of token4 is token1 + parentOfToken4, err8 := keeper.ParentOf(ctx, defaultTokenID4) + require.NoError(t, err8) + require.Equal(t, parentOfToken4.GetTokenID(), defaultTokenID1) + + // children of token1 are token2, token4 + childrenOfToken1, err9 := keeper.ChildrenOf(ctx, defaultTokenID1) + require.NoError(t, err9) + require.Equal(t, len(childrenOfToken1), 2) + require.True(t, (childrenOfToken1[0].GetTokenID() == defaultTokenID2 && childrenOfToken1[1].GetTokenID() == defaultTokenID4) || (childrenOfToken1[0].GetTokenID() == defaultTokenID4 && childrenOfToken1[1].GetTokenID() == defaultTokenID2)) + + // child of token2 is token3 + childrenOfToken2, err10 := keeper.ChildrenOf(ctx, defaultTokenID2) + require.NoError(t, err10) + require.Equal(t, len(childrenOfToken2), 1) + require.True(t, childrenOfToken2[0].GetTokenID() == defaultTokenID3) + + // child of token3 is empty + childrenOfToken3, err11 := keeper.ChildrenOf(ctx, defaultTokenID3) + require.NoError(t, err11) + require.Equal(t, len(childrenOfToken3), 0) + + // child of token4 is empty + childrenOfToken4, err12 := keeper.ChildrenOf(ctx, defaultTokenID4) + require.NoError(t, err12) + require.Equal(t, len(childrenOfToken4), 0) + + // query failure cases + _, err := keeper.ParentOf(ctx, defaultTokenIDFT) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNotNFT, "TokenID: %s", defaultTokenIDFT).Error()) + + // + // attach error cases + // + + // attach non-root token : failure + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2), sdkerrors.Wrapf(types.ErrTokenAlreadyAChild, "TokenID: %s", defaultTokenID2).Error()) + + // attach non-exist token : failure + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID8), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID8).Error()) + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID8, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID8).Error()) + + // attach non-mine token : failure + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID5), sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", defaultTokenID5, addr1.String()).Error()) + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID5, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", defaultTokenID5, addr1.String()).Error()) + + // attach to itself : failure + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID1), sdkerrors.Wrapf(types.ErrCannotAttachToItself, "TokenID: %s", defaultTokenID1).Error()) + + // attach to a descendant : failure + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID1), sdkerrors.Wrapf(types.ErrCannotAttachToADescendant, "TokenID: %s, ToTokenID: %s", defaultTokenID1, defaultTokenID2).Error()) + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID3, defaultTokenID1), sdkerrors.Wrapf(types.ErrCannotAttachToADescendant, "TokenID: %s, ToTokenID: %s", defaultTokenID1, defaultTokenID3).Error()) + require.EqualError(t, keeper.Attach(ctx, addr1, defaultTokenID4, defaultTokenID1), sdkerrors.Wrapf(types.ErrCannotAttachToADescendant, "TokenID: %s, ToTokenID: %s", defaultTokenID1, defaultTokenID4).Error()) + + // + // detach error cases + // + + // detach not a child : failure + require.EqualError(t, keeper.Detach(ctx, addr1, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotAChild, "TokenID: %s", defaultTokenID1).Error()) + + // detach non-mine token : failure + require.EqualError(t, keeper.Detach(ctx, addr1, defaultTokenID5), sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", defaultTokenID5, addr1.String()).Error()) + + // detach non-exist token : failure + require.EqualError(t, keeper.Detach(ctx, addr1, defaultTokenID8), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID8).Error()) + + // + // detach success cases + // + + // detach single child + require.NoError(t, keeper.Detach(ctx, addr1, defaultTokenID4)) + + // detach a child having child + require.NoError(t, keeper.Detach(ctx, addr1, defaultTokenID2)) + + // detach child + require.NoError(t, keeper.Detach(ctx, addr1, defaultTokenID3)) + + // + // verify the relations + // + // parent of token2 is nil + parentOfToken2, err6 = keeper.ParentOf(ctx, defaultTokenID2) + require.NoError(t, err6) + require.Nil(t, parentOfToken2) + + // parent of token3 is nil + parentOfToken3, err7 = keeper.ParentOf(ctx, defaultTokenID3) + require.NoError(t, err7) + require.Nil(t, parentOfToken3) + + // parent of token4 is nil + parentOfToken4, err8 = keeper.ParentOf(ctx, defaultTokenID4) + require.NoError(t, err8) + require.Nil(t, parentOfToken4) + + // children of token1 is empty + childrenOfToken1, err1 = keeper.ChildrenOf(ctx, defaultTokenID1) + require.NoError(t, err1) + require.Equal(t, len(childrenOfToken1), 0) + + // owner of token3 is addr1 + token3, err13 := keeper.GetToken(ctx, defaultTokenID3) + require.NoError(t, err13) + + require.Equal(t, (token3.(types.NFT)).GetOwner(), addr1) +} + +func setupNFTs(t *testing.T, ctx sdk.Context) { + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, "name", "{}", defaultImgURI), addr1)) + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1)) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID2, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID3, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID4, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID5, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID6, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID7, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID8, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID9, defaultName, defaultMeta, addr1))) +} + +func TestGetDepthWidthTable(t *testing.T) { + ctx := cacheKeeper() + + setupNFTs(t, ctx) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID3)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID4)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID5)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID6)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID3, defaultTokenID7)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID3, defaultTokenID8)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID7, defaultTokenID9)) + + // +- token4 + // +- token5 + // token1 -+- token2 -+- token6 + // | + // +- token3 -+- token7 --- token9 + // +- token8 + + var table []int + table = keeper.GetDepthWidthTable(ctx, defaultTokenID1) + require.Equal(t, []int{1, 2, 5, 1}, table) + + table = keeper.GetDepthWidthTable(ctx, defaultTokenID2) + require.Equal(t, []int{1, 3}, table) + + table = keeper.GetDepthWidthTable(ctx, defaultTokenID3) + require.Equal(t, []int{1, 2, 1}, table) +} + +func TestGetCurrentDepthFromRoot(t *testing.T) { + ctx := cacheKeeper() + + setupNFTs(t, ctx) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID3)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID4)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID3, defaultTokenID5)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID3, defaultTokenID6)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID4, defaultTokenID7)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID5, defaultTokenID8)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID8, defaultTokenID9)) + + // token1 -+- token2 --- token4 --- token7 + // +- token3 -+- token5 --- token8 --- token9 + // +- token6 + + // the depth of token1 should be 0 + require.Equal(t, 0, keeper.GetDepthFromRoot(ctx, defaultTokenID1)) + + // the depth of token2, token3 should be 1 + require.Equal(t, 1, keeper.GetDepthFromRoot(ctx, defaultTokenID2)) + require.Equal(t, 1, keeper.GetDepthFromRoot(ctx, defaultTokenID3)) + + // the depth of token4, token5, token6 should be + require.Equal(t, 2, keeper.GetDepthFromRoot(ctx, defaultTokenID4)) + require.Equal(t, 2, keeper.GetDepthFromRoot(ctx, defaultTokenID5)) + require.Equal(t, 2, keeper.GetDepthFromRoot(ctx, defaultTokenID6)) + + // the depth of token7, token8 should be 3 + require.Equal(t, 3, keeper.GetDepthFromRoot(ctx, defaultTokenID7)) + require.Equal(t, 3, keeper.GetDepthFromRoot(ctx, defaultTokenID8)) + + // the depth of token9 should be 4 + require.Equal(t, 4, keeper.GetDepthFromRoot(ctx, defaultTokenID9)) +} + +// nolint:dupl +func TestCheckDepth(t *testing.T) { + ctx := cacheKeeper() + keeper.SetParams(ctx, types.NewParams(4, 4)) // Sets the max composable width/depth to 4 + + setupNFTs(t, ctx) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID3, defaultTokenID4)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID5, defaultTokenID6)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID5, defaultTokenID7)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID6, defaultTokenID8)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID8, defaultTokenID9)) + + // Given two composed tokens + // token1 --- token2 --- token3 --- token4 + // + // token5 -+- token6 --- token8 --- token9 + // +- token7 + // + // Sets the max composable width/depth to 4 + + // if token5 is attached to token2 then, + // + // token1 --- token2 -+- token3 --- token4 + // +- token5 -+- token6 --- token8 --- token9 + // +- token7 + // deepest depth is 5 (path: token1-token2-token5-token6-token8-token9) + err := keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID5) + require.Error(t, err) + + // if token5 is attached to token1 then, + // + // token1 -+- token2 --- token3 --- token4 + // +- token5 -+- token6 --- token8 --- token9 + // +- token7 + // deepest depth is 4 (path: token1-token5-token6-token8-token9) + err = keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID5) + require.NoError(t, err) +} + +// nolint:dupl +func TestCheckWidth(t *testing.T) { + ctx := cacheKeeper() + keeper.SetParams(ctx, types.NewParams(4, 4)) // Sets the max composable width/depth to 4 + + setupNFTs(t, ctx) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID4)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID5)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID6, defaultTokenID7)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID6, defaultTokenID8)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID7, defaultTokenID9)) + + // Given two composed tokens + // +- token3 + // +- token4 + // token1 -+- token2 -+- token5 + // + // token6 -+- token7 --- token9 + // +- token8 + // + // Sets the max composable width/depth to 4 + + // if token6 is attached to token1 then, + // + // +- token3 + // +- token4 + // token1 -+- token2 -+- token5 + // | + // +- token6 -+- token7 --- token9 + // +- token8 + // + // widest width is 5 + err := keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID6) + require.Error(t, err) + + // if token6 is attached to token2 then, + // + // +- token3 + // +- token4 + // token1 -+- token2 -+- token5 + // +- token6 -+- token7 --- token9 + // +- token8 + // + // widest width is 4 + err = keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID6) + require.NoError(t, err) +} diff --git a/x/collection/internal/keeper/issue.go b/x/collection/internal/keeper/issue.go new file mode 100644 index 0000000000..671c24b606 --- /dev/null +++ b/x/collection/internal/keeper/issue.go @@ -0,0 +1,71 @@ +package keeper + +import ( + "strconv" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type IssueKeeper interface { + IssueFT(ctx sdk.Context, owner sdk.AccAddress, token types.FT, amount sdk.Int) error + IssueNFT(ctx sdk.Context, owner sdk.AccAddress, tokenType string) error +} + +func (k Keeper) IssueFT(ctx sdk.Context, owner, to sdk.AccAddress, token types.FT, amount sdk.Int) error { + if !k.ExistCollection(ctx) { + return sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", k.getContractID(ctx)) + } + err := k.SetToken(ctx, token) + if err != nil { + return err + } + + err = k.MintSupply(ctx, to, types.NewCoins(types.NewCoin(token.GetTokenID(), amount))) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeIssueFT, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyName, token.GetName()), + sdk.NewAttribute(types.AttributeKeyTokenID, token.GetTokenID()), + sdk.NewAttribute(types.AttributeKeyOwner, owner.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + sdk.NewAttribute(types.AttributeKeyMintable, strconv.FormatBool(token.GetMintable())), + sdk.NewAttribute(types.AttributeKeyDecimals, token.GetDecimals().String()), + ), + }) + + return nil +} + +func (k Keeper) IssueNFT(ctx sdk.Context, tokenType types.TokenType, owner sdk.AccAddress) error { + if !k.ExistCollection(ctx) { + return sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", k.getContractID(ctx)) + } + + err := k.SetTokenType(ctx, tokenType) + if err != nil { + return err + } + + mintPerm := types.NewMintPermission() + k.AddPermission(ctx, owner, mintPerm) + burnPerm := types.NewBurnPermission() + k.AddPermission(ctx, owner, burnPerm) + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeIssueNFT, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyTokenType, tokenType.GetTokenType()), + ), + }) + + return nil +} diff --git a/x/collection/internal/keeper/issue_test.go b/x/collection/internal/keeper/issue_test.go new file mode 100644 index 0000000000..4aaa0d1fb3 --- /dev/null +++ b/x/collection/internal/keeper/issue_test.go @@ -0,0 +1,34 @@ +package keeper + +import ( + "context" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" +) + +func TestKeeper_IssueFT(t *testing.T) { + ctx := cacheKeeper() + + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, "name", defaultMeta, defaultImgURI), addr1)) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.IssueFT(ctx2, addr1, addr1, types.NewFT(wrongContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(1), true), sdk.NewInt(defaultAmount)), sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", wrongContractID).Error()) + require.NoError(t, keeper.IssueFT(ctx, addr1, addr1, types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(1), true), sdk.NewInt(defaultAmount))) + require.EqualError(t, keeper.IssueFT(ctx, addr1, addr1, types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(1), true), sdk.NewInt(defaultAmount)), sdkerrors.Wrapf(types.ErrTokenExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenIDFT).Error()) +} + +func TestKeeper_IssueNFT(t *testing.T) { + ctx := cacheKeeper() + + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, "name", defaultMeta, defaultImgURI), addr1)) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.IssueNFT(ctx2, types.NewBaseTokenType(wrongContractID, defaultTokenType, defaultName, defaultMeta), addr1), sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", wrongContractID).Error()) + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1)) + require.EqualError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1), sdkerrors.Wrapf(types.ErrTokenTypeExist, "ContractID: %s, TokenType: %s", defaultContractID, defaultTokenType).Error()) +} diff --git a/x/collection/internal/keeper/keeper.go b/x/collection/internal/keeper/keeper.go new file mode 100644 index 0000000000..cc5255907d --- /dev/null +++ b/x/collection/internal/keeper/keeper.go @@ -0,0 +1,78 @@ +package keeper + +import ( + "fmt" + + "github.com/tendermint/tendermint/libs/log" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params/subspace" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" +) + +type Keeper struct { + accountKeeper types.AccountKeeper + contractKeeper contract.Keeper + storeKey sdk.StoreKey + paramsSpace subspace.Subspace + cdc *codec.Codec +} + +func NewKeeper( + cdc *codec.Codec, + accountKeeper types.AccountKeeper, + contractKeeper contract.Keeper, + paramsSpace subspace.Subspace, + storeKey sdk.StoreKey, +) Keeper { + return Keeper{ + accountKeeper: accountKeeper, + contractKeeper: contractKeeper, + storeKey: storeKey, + paramsSpace: paramsSpace.WithKeyTable(types.ParamKeyTable()), + cdc: cdc, + } +} +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +func (k Keeper) NewContractID(ctx sdk.Context) string { + return k.contractKeeper.NewContractID(ctx) +} + +func (k Keeper) HasContractID(ctx sdk.Context) bool { + return k.contractKeeper.HasContractID(ctx, k.getContractID(ctx)) +} + +func (k Keeper) getContractID(ctx sdk.Context) string { + contractI := ctx.Context().Value(contract.CtxKey{}) + if contractI == nil { + panic("contract id does not set on the context") + } + return contractI.(string) +} + +func (k Keeper) UnmarshalJSON(bz []byte, ptr interface{}) error { + return k.cdc.UnmarshalJSON(bz, ptr) +} + +func (k Keeper) MarshalJSON(o interface{}) ([]byte, error) { + return k.cdc.MarshalJSON(o) +} + +func (k Keeper) MarshalJSONIndent(o interface{}) ([]byte, error) { + return k.cdc.MarshalJSONIndent(o, "", " ") +} + +func (k Keeper) mustEncodeString(str string) []byte { + return k.cdc.MustMarshalBinaryBare(str) +} + +func (k Keeper) mustDecodeString(bz []byte) (str string) { + k.cdc.MustUnmarshalBinaryBare(bz, &str) + return str +} diff --git a/x/collection/internal/keeper/keeper_test.go b/x/collection/internal/keeper/keeper_test.go new file mode 100644 index 0000000000..26b9036e84 --- /dev/null +++ b/x/collection/internal/keeper/keeper_test.go @@ -0,0 +1,169 @@ +package keeper + +import ( + "context" + "os" + "testing" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +const ( + defaultName = "name" + defaultMeta = "{}" + defaultContractID = "abcdef01" + defaultContractID2 = "abcdef02" + wrongContractID = "abcd1234" + defaultImgURI = "img-uri" + defaultDecimals = 6 + defaultAmount = 1000 + defaultTokenType = "10000001" + defaultTokenType2 = "10000002" + defaultTokenType3 = "10000003" + defaultTokenType4 = "10000004" + defaultTokenIndex = "00000001" + defaultTokenID1 = defaultTokenType + defaultTokenIndex + defaultTokenID2 = defaultTokenType + "00000002" + defaultTokenID3 = defaultTokenType + "00000003" + defaultTokenID4 = defaultTokenType + "00000004" + defaultTokenID5 = defaultTokenType + "00000005" + defaultTokenID6 = defaultTokenType + "00000006" + defaultTokenID7 = defaultTokenType + "00000007" + defaultTokenID8 = defaultTokenType + "00000008" + defaultTokenID9 = defaultTokenType + "00000009" + wrongTokenID = defaultTokenType2 + "00000001" + defaultTokenIDFT = "0000000100000000" + defaultTokenIDFT2 = "0000000200000000" + defaultTokenIDFT3 = "0000000300000000" + defaultTokenIDFT4 = "0000000400000000" +) + +var ( + addr1 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr3 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) +) + +var ( + ms store.CommitMultiStore + ctx sdk.Context + keeper Keeper +) + +func setup() { + println("setup") + ctx, ms, keeper = TestKeeper() +} + +func TestMain(m *testing.M) { + setup() + ret := m.Run() + os.Exit(ret) +} + +func cacheKeeper() sdk.Context { + msCache := ms.CacheMultiStore() + ctx = ctx.WithMultiStore(msCache) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID)) + return ctx +} + +func TestKeeper_MarshalJSONLogger(t *testing.T) { + ctx := cacheKeeper() + dummy := struct { + Key string + Value string + }{ + Key: "key", + Value: "value", + } + bz, err := keeper.MarshalJSON(dummy) + require.NoError(t, err) + + dummy2 := struct { + Key string + Value string + }{} + + err = keeper.UnmarshalJSON(bz, &dummy2) + require.NoError(t, err) + require.Equal(t, dummy.Key, dummy2.Key) + require.Equal(t, dummy.Value, dummy2.Value) + logger := keeper.Logger(ctx) + logger.Info("test", dummy, dummy2) +} + +func prepareCollectionTokens(ctx sdk.Context, t *testing.T) { + // prepare collection + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, "name", "{}", + defaultImgURI), addr1)) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID2)) + require.NoError(t, keeper.CreateCollection(ctx2, types.NewCollection(defaultContractID2, "name", "{}", + defaultImgURI), addr1)) + + // issue 6 tokens + // token1 = contract1id1 by addr1 + // token2 = contract1id2 by addr1 + // token3 = contract1id3 by addr1 + // token4 = contract1id4 by addr1 + // token5 = contract2id5 by addr1 + // token6 = contract1id6 by addr2 + // token7 = contract1 by addr1 + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1)) + require.NoError(t, keeper.IssueNFT(ctx2, types.NewBaseTokenType(defaultContractID2, defaultTokenType, defaultName, defaultMeta), addr1)) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID2, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID3, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID4, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx2, addr1, types.NewNFT(defaultContractID2, defaultTokenID1, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.GrantPermission(ctx, addr1, addr2, types.NewMintPermission())) + require.NoError(t, keeper.MintNFT(ctx, addr2, types.NewNFT(defaultContractID, defaultTokenID5, defaultName, defaultMeta, addr2))) + require.NoError(t, keeper.IssueFT(ctx, addr1, addr1, types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(1), true), sdk.NewInt(defaultAmount))) +} + +func prepareProxy(ctx sdk.Context, t *testing.T) { + require.NoError(t, keeper.SetApproved(ctx, addr1, addr2)) + require.NoError(t, keeper.SetApproved(ctx, addr2, addr1)) + require.NoError(t, keeper.TransferFT(ctx, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount)))) + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID1)) + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID2)) + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID3)) + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID4)) +} + +func verifyTokenFunc(t *testing.T, expected types.Token, actual types.Token) { + switch e := expected.(type) { + case types.FT: + a, ok := actual.(types.FT) + require.True(t, ok) + require.Equal(t, e.GetContractID(), a.GetContractID()) + require.Equal(t, e.GetName(), a.GetName()) + require.Equal(t, e.GetTokenID(), a.GetTokenID()) + require.Equal(t, e.GetTokenType(), a.GetTokenType()) + require.Equal(t, e.GetTokenIndex(), a.GetTokenIndex()) + require.Equal(t, e.GetDecimals(), a.GetDecimals()) + require.Equal(t, e.GetMintable(), a.GetMintable()) + case types.NFT: + a, ok := actual.(types.NFT) + require.True(t, ok) + require.Equal(t, e.GetContractID(), a.GetContractID()) + require.Equal(t, e.GetName(), a.GetName()) + require.Equal(t, e.GetTokenID(), a.GetTokenID()) + require.Equal(t, e.GetTokenType(), a.GetTokenType()) + require.Equal(t, e.GetTokenIndex(), a.GetTokenIndex()) + require.Equal(t, e.GetOwner(), a.GetOwner()) + default: + panic("never happen") + } +} + +func verifyTokenTypeFunc(t *testing.T, expected types.TokenType, actual types.TokenType) { + require.Equal(t, expected.GetName(), actual.GetName()) + require.Equal(t, expected.GetTokenType(), actual.GetTokenType()) +} diff --git a/x/collection/internal/keeper/mint.go b/x/collection/internal/keeper/mint.go new file mode 100644 index 0000000000..5ccc71a1f7 --- /dev/null +++ b/x/collection/internal/keeper/mint.go @@ -0,0 +1,115 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type MintKeeper interface { + MintFT(ctx sdk.Context, from, to sdk.AccAddress, amount types.Coins) error + MintNFT(ctx sdk.Context, from sdk.AccAddress, token types.NFT) error +} + +func (k Keeper) MintFT(ctx sdk.Context, from, to sdk.AccAddress, amount types.Coins) error { + for _, coin := range amount { + token, err := k.GetToken(ctx, coin.Denom) + if err != nil { + return err + } + if err := k.isMintable(ctx, token, from); err != nil { + return err + } + } + err := k.MintSupply(ctx, to, amount) + if err != nil { + return err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeMintFT, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + ), + }) + return nil +} + +func (k Keeper) MintNFT(ctx sdk.Context, from sdk.AccAddress, token types.NFT) error { + if !k.HasTokenType(ctx, token.GetTokenType()) { + return sdkerrors.Wrapf(types.ErrTokenTypeNotExist, "ContractID: %s, TokenType: %s", k.getContractID(ctx), token.GetTokenType()) + } + + perm := types.NewMintPermission() + if !k.HasPermission(ctx, from, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", from.String(), perm.String()) + } + + err := k.mintNFTInternal(ctx, token) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeMintNFT, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyName, token.GetName()), + sdk.NewAttribute(types.AttributeKeyTokenID, token.GetTokenID()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, token.GetOwner().String()), + ), + }) + + return nil +} + +func (k Keeper) mintNFTInternal(ctx sdk.Context, token types.NFT) error { + err := k.SetToken(ctx, token) + if err != nil { + return err + } + + if k.HasNFTOwner(ctx, token.GetOwner(), token.GetTokenID()) { + return sdkerrors.Wrapf(types.ErrTokenExist, "ContractID: %s, TokenID: %s", k.getContractID(ctx), token.GetTokenID()) + } + k.AddNFTOwner(ctx, token.GetOwner(), token.GetTokenID()) + k.increaseTokenTypeMintCount(ctx, token.GetTokenType()) + return nil +} + +func (k Keeper) increaseTokenTypeMintCount(ctx sdk.Context, tokenType string) { + store := ctx.KVStore(k.storeKey) + count := k.getTokenTypeMintCount(ctx, tokenType) + count = count.Add(sdk.NewInt(1)) + + store.Set(types.TokenTypeMintCount(k.getContractID(ctx), tokenType), k.cdc.MustMarshalBinaryBare(count)) +} + +func (k Keeper) getTokenTypeMintCount(ctx sdk.Context, tokenType string) (count sdk.Int) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.TokenTypeMintCount(k.getContractID(ctx), tokenType)) + if bz == nil { + return sdk.ZeroInt() + } + k.cdc.MustUnmarshalBinaryBare(bz, &count) + return count +} + +func (k Keeper) isMintable(ctx sdk.Context, token types.Token, from sdk.AccAddress) error { + ft, ok := token.(types.FT) + if !ok { + return sdkerrors.Wrapf(types.ErrTokenNotMintable, "ContractID: %s, TokenID: %s", k.getContractID(ctx), token.GetTokenID()) + } + + if !ft.GetMintable() { + return sdkerrors.Wrapf(types.ErrTokenNotMintable, "ContractID: %s, TokenID: %s", k.getContractID(ctx), token.GetTokenID()) + } + perm := types.NewMintPermission() + if !k.HasPermission(ctx, from, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", from.String(), perm.String()) + } + return nil +} diff --git a/x/collection/internal/keeper/mint_test.go b/x/collection/internal/keeper/mint_test.go new file mode 100644 index 0000000000..a5ffc676d2 --- /dev/null +++ b/x/collection/internal/keeper/mint_test.go @@ -0,0 +1,45 @@ +package keeper + +import ( + "context" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" +) + +func TestKeeper_MintFT(t *testing.T) { + ctx := cacheKeeper() + + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, "name", defaultMeta, defaultImgURI), addr1)) + require.NoError(t, keeper.IssueFT(ctx, addr1, addr1, types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(1), true), sdk.NewInt(defaultAmount))) + require.NoError(t, keeper.IssueFT(ctx, addr1, addr1, types.NewFT(defaultContractID, defaultTokenIDFT2, defaultName, defaultMeta, sdk.NewInt(1), true), sdk.NewInt(defaultAmount))) + require.NoError(t, keeper.IssueFT(ctx, addr1, addr1, types.NewFT(defaultContractID, defaultTokenIDFT3, defaultName, defaultMeta, sdk.NewInt(1), false), sdk.NewInt(defaultAmount))) + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1)) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.MintFT(ctx2, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(10)))), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", wrongContractID, defaultTokenIDFT).Error()) + require.EqualError(t, keeper.MintFT(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT4, sdk.NewInt(10)))), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenIDFT4).Error()) + require.EqualError(t, keeper.MintFT(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT3, sdk.NewInt(10)))), sdkerrors.Wrapf(types.ErrTokenNotMintable, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenIDFT3).Error()) + require.EqualError(t, keeper.MintFT(ctx, addr2, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(10)))), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr2.String(), types.NewMintPermission()).Error()) + require.EqualError(t, keeper.MintFT(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenID1, sdk.NewInt(10)))), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID1).Error()) + + require.NoError(t, keeper.MintFT(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(10))))) + require.NoError(t, keeper.MintFT(ctx, addr1, addr2, types.NewCoins(types.NewCoin(defaultTokenIDFT, sdk.NewInt(10)), types.NewCoin(defaultTokenIDFT2, sdk.NewInt(20))))) +} + +func TestKeeper_MintNFT(t *testing.T) { + ctx := cacheKeeper() + + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, "name", defaultMeta, defaultImgURI), addr1)) + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1)) + + require.EqualError(t, keeper.MintNFT(ctx, addr2, types.NewNFT(defaultContractID, defaultTokenID1, "sword", defaultMeta, addr1)), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr2.String(), types.NewMintPermission()).Error()) + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.MintNFT(ctx2, addr1, types.NewNFT(wrongContractID, defaultTokenID1, "sword", defaultMeta, addr1)), sdkerrors.Wrapf(types.ErrTokenTypeNotExist, "ContractID: %s, TokenType: %s", wrongContractID, defaultTokenType).Error()) + require.EqualError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, wrongTokenID, "sword", defaultMeta, addr1)), sdkerrors.Wrapf(types.ErrTokenTypeNotExist, "ContractID: %s, TokenType: %s", defaultContractID, defaultTokenType2).Error()) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID1, "sword", defaultMeta, addr1))) +} diff --git a/x/collection/internal/keeper/modify.go b/x/collection/internal/keeper/modify.go new file mode 100644 index 0000000000..712507d148 --- /dev/null +++ b/x/collection/internal/keeper/modify.go @@ -0,0 +1,152 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func (k Keeper) Modify(ctx sdk.Context, owner sdk.AccAddress, tokenType, tokenIndex string, + changes types.Changes) error { + if tokenType != "" { + if tokenIndex != "" { + return k.modifyToken(ctx, owner, tokenType+tokenIndex, changes) + } + return k.modifyTokenType(ctx, owner, tokenType, changes) + } + if tokenIndex == "" { + return k.modifyCollection(ctx, owner, changes) + } + return types.ErrTokenIndexWithoutType +} + +// nolint:dupl +func (k Keeper) modifyCollection(ctx sdk.Context, owner sdk.AccAddress, changes types.Changes) error { + collection, err := k.GetCollection(ctx) + if err != nil { + return err + } + modifyPerm := types.NewModifyPermission() + if !k.HasPermission(ctx, owner, modifyPerm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", owner.String(), modifyPerm.String()) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeModifyCollection, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + ), + }) + + for _, change := range changes { + switch change.Field { + case types.AttributeKeyName: + collection.SetName(change.Value) + case types.AttributeKeyMeta: + collection.SetMeta(change.Value) + case types.AttributeKeyBaseImgURI: + collection.SetBaseImgURI(change.Value) + default: + return sdkerrors.Wrapf(types.ErrInvalidChangesField, "Field: %s", change.Field) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeModifyCollection, + sdk.NewAttribute(change.Field, change.Value), + ), + }) + } + err = k.UpdateCollection(ctx, collection) + if err != nil { + return err + } + return nil +} + +// nolint:dupl +func (k Keeper) modifyTokenType(ctx sdk.Context, owner sdk.AccAddress, tokenTypeID string, + changes types.Changes) error { + tokenType, err := k.GetTokenType(ctx, tokenTypeID) + if err != nil { + return err + } + modifyPerm := types.NewModifyPermission() + if !k.HasPermission(ctx, owner, modifyPerm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", owner.String(), modifyPerm.String()) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeModifyTokenType, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyTokenType, tokenType.GetTokenType()), + ), + }) + + for _, change := range changes { + switch change.Field { + case types.AttributeKeyName: + tokenType.SetName(change.Value) + case types.AttributeKeyMeta: + tokenType.SetMeta(change.Value) + default: + return sdkerrors.Wrapf(types.ErrInvalidChangesField, "Field: %s", change.Field) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeModifyTokenType, + sdk.NewAttribute(change.Field, change.Value), + ), + }) + } + err = k.UpdateTokenType(ctx, tokenType) + if err != nil { + return err + } + return nil +} + +// nolint:dupl +func (k Keeper) modifyToken(ctx sdk.Context, owner sdk.AccAddress, tokenID string, + changes types.Changes) error { + token, err := k.GetToken(ctx, tokenID) + if err != nil { + return err + } + modifyPerm := types.NewModifyPermission() + if !k.HasPermission(ctx, owner, modifyPerm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", owner.String(), modifyPerm.String()) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeModifyToken, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyTokenID, token.GetTokenID()), + ), + }) + + for _, change := range changes { + switch change.Field { + case types.AttributeKeyName: + token.SetName(change.Value) + case types.AttributeKeyMeta: + token.SetMeta(change.Value) + default: + return sdkerrors.Wrapf(types.ErrInvalidChangesField, "Field: %s", change.Field) + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeModifyToken, + sdk.NewAttribute(change.Field, change.Value), + ), + }) + } + err = k.UpdateToken(ctx, token) + if err != nil { + return err + } + return nil +} diff --git a/x/collection/internal/keeper/modify_test.go b/x/collection/internal/keeper/modify_test.go new file mode 100644 index 0000000000..5d984ed072 --- /dev/null +++ b/x/collection/internal/keeper/modify_test.go @@ -0,0 +1,218 @@ +package keeper + +import ( + "context" + "testing" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" +) + +const nonExistentID = "1234abcd" + +func TestModifyCollection(t *testing.T) { + const ( + modifiedName = "modifiedName" + modifiedURI = "modifiedURI" + modifiedMeta = "modifiedMeta" + ) + changes := types.NewChanges( + types.NewChange("name", modifiedName), + types.NewChange("base_img_uri", modifiedURI), + types.NewChange("meta", modifiedMeta), + ) + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + // Given collection and permission + collection, err := keeper.GetCollection(ctx) + require.NoError(t, err) + modifyPermission := types.NewModifyPermission() + keeper.AddPermission(ctx, addr1, modifyPermission) + + t.Log("Test to modify collection") + { + // When modify collection + require.NoError(t, keeper.modifyCollection(ctx, addr1, changes)) + + // Then collection is modified + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.CollectionKey(collection.GetContractID())) + actual := keeper.mustDecodeCollection(bz) + require.Equal(t, modifiedName, actual.GetName()) + require.Equal(t, modifiedURI, actual.GetBaseImgURI()) + require.Equal(t, modifiedMeta, actual.GetMeta()) + } + t.Log("Test with nonexistent contract") + { + // Given nonexistent contract, When modify collection name with invalid contract, Then error is occurred + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, nonExistentID)) + require.EqualError(t, keeper.modifyCollection(ctx2, addr1, changes), + sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", nonExistentID).Error()) + } + t.Log("Test without permission") + { + // Given user does not have permission + invalidUser := addr2 + + // When modify collection name with invalid permission, Then error is occurred + require.EqualError(t, keeper.modifyCollection(ctx, invalidUser, changes), + sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", invalidUser.String(), modifyPermission.String()).Error()) + } +} + +func TestModifyTokenType(t *testing.T) { + const modifiedName = "modifiedName" + const modifiedURI = "modifiedURI" + const modifiedMeta = "modifiedMeta" + + validChanges := types.NewChanges( + types.NewChange("name", modifiedName), + types.NewChange("meta", modifiedMeta), + ) + invalidChanges := types.NewChanges( + types.NewChange("base_img_uri", modifiedURI), + ) + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + // Given collection and permission + modifyPermission := types.NewModifyPermission() + keeper.AddPermission(ctx, addr1, modifyPermission) + + t.Log("Test to modify token type with valid fields") + { + // When modify token type name + require.NoError(t, keeper.modifyTokenType(ctx, addr1, defaultTokenType, validChanges)) + + // Then collection name is modified + actual, err := keeper.GetTokenType(ctx, defaultTokenType) + require.NoError(t, err) + require.Equal(t, modifiedName, actual.GetName()) + } + t.Log("Test to modify token type with invalid fields") + { + require.EqualError(t, keeper.modifyTokenType(ctx, addr1, defaultTokenType, invalidChanges), + sdkerrors.Wrap(types.ErrInvalidChangesField, "Field: base_img_uri").Error()) + } + t.Log("Test with nonexistent contract") + { + // Given nonexistent token type, When modify token type name with invalid contract, Then error is occurred + require.EqualError(t, keeper.modifyTokenType(ctx, addr1, nonExistentID, validChanges), + sdkerrors.Wrapf(types.ErrTokenTypeNotExist, "ContractID: %s, TokenType: %s", defaultContractID, nonExistentID).Error()) + } + t.Log("Test without permission") + { + // Given user does not have permission + invalidUser := addr2 + + // When modify token type name with invalid permission, Then error is occurred + require.EqualError(t, keeper.modifyTokenType(ctx, invalidUser, defaultTokenType, validChanges), + sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", invalidUser.String(), modifyPermission.String()).Error()) + } +} + +func TestModifyToken(t *testing.T) { + const modifiedName = "modifiedName" + const modifiedURI = "modifiedURI" + const modifiedMeta = "modifiedMeta" + + validChanges := types.NewChanges( + types.NewChange("name", modifiedName), + types.NewChange("meta", modifiedMeta), + ) + invalidChanges := types.NewChanges( + types.NewChange("base_img_uri", modifiedURI), + ) + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + // Given collection and permission + modifyPermission := types.NewModifyPermission() + keeper.AddPermission(ctx, addr1, modifyPermission) + // And token + token, err := keeper.GetToken(ctx, defaultTokenID1) + require.NoError(t, err) + + t.Log("Test to modify token with valid changes") + { + // When modify token name + require.NoError(t, keeper.modifyToken(ctx, addr1, token.GetTokenID(), validChanges)) + + // Then token name is modified + actual, err := keeper.GetToken(ctx, token.GetTokenID()) + require.NoError(t, err) + require.Equal(t, modifiedName, actual.GetName()) + } + t.Log("Test to modify token with invalid changes") + { + require.EqualError(t, keeper.modifyToken(ctx, addr1, token.GetTokenID(), invalidChanges), + sdkerrors.Wrap(types.ErrInvalidChangesField, "Field: base_img_uri").Error()) + } + t.Log("Test with nonexistent contract") + { + // Given nonexistent token id, When modify token name with invalid contract, Then error is occurred + require.EqualError(t, keeper.modifyToken(ctx, addr1, nonExistentID, validChanges), + sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", token.GetContractID(), nonExistentID).Error()) + } + t.Log("Test without permission") + { + // Given user does not have permission + invalidUser := addr2 + + // When modify token name with invalid permission, Then error is occurred + require.EqualError(t, keeper.modifyToken(ctx, invalidUser, token.GetTokenID(), validChanges), + sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", invalidUser.String(), modifyPermission.String()).Error()) + } +} + +func TestModify(t *testing.T) { + const modifiedName = "modifiedName" + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + changes := types.NewChanges( + types.NewChange("name", modifiedName), + ) + // Given permission + modifyPermission := types.NewModifyPermission() + keeper.AddPermission(ctx, addr1, modifyPermission) + + t.Logf("Test to modify name of collection to %s", modifiedName) + { + // When modify collection name + require.NoError(t, keeper.Modify(ctx, addr1, "", "", changes)) + + // Then collection name is modified + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.CollectionKey(defaultContractID)) + actual := keeper.mustDecodeCollection(bz) + require.Equal(t, modifiedName, actual.GetName()) + } + t.Logf("Test to modify name of token type to %s", modifiedName) + { + // When modify token type name + require.NoError(t, keeper.Modify(ctx, addr1, defaultTokenType, "", changes)) + + // Then token type name is modified + actual, err := keeper.GetTokenType(ctx, defaultTokenType) + require.NoError(t, err) + require.Equal(t, modifiedName, actual.GetName()) + } + t.Logf("Test to modify name of token to %s", modifiedName) + { + // When modify token name + require.NoError(t, keeper.Modify(ctx, addr1, defaultTokenType, defaultTokenIndex, changes)) + + // Then token name is modified + actual, err := keeper.GetToken(ctx, defaultTokenID1) + require.NoError(t, err) + require.Equal(t, modifiedName, actual.GetName()) + } + t.Log("Test with only token index not token type") + { + // When modify token name, Then error is occurred + require.EqualError(t, keeper.Modify(ctx, addr1, "", defaultTokenIndex, changes), types.ErrTokenIndexWithoutType.Error()) + } +} diff --git a/x/collection/internal/keeper/msg_encoder.go b/x/collection/internal/keeper/msg_encoder.go new file mode 100644 index 0000000000..992834c97f --- /dev/null +++ b/x/collection/internal/keeper/msg_encoder.go @@ -0,0 +1,291 @@ +package keeper + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/wasm" +) + +func NewMsgEncodeHandler(collectionKeeper Keeper) wasm.EncodeHandler { + return func(jsonMsg json.RawMessage) ([]sdk.Msg, error) { + var wasmCustomMsg types.WasmCustomMsg + err := json.Unmarshal(jsonMsg, &wasmCustomMsg) + if err != nil { + return nil, err + } + + switch types.MsgRoute(wasmCustomMsg.Route) { + case types.RCreateCollection: + return handleMsgCreateCollection(wasmCustomMsg.Data) + case types.RIssueNFT: + return handleMsgIssueNFT(wasmCustomMsg.Data) + case types.RIssueFT: + return handleMsgIssueFT(wasmCustomMsg.Data) + case types.RMintNFT: + return handleMsgMintNFT(wasmCustomMsg.Data) + case types.RMintFT: + return handleMsgMintFT(wasmCustomMsg.Data) + case types.RBurnNFT: + return handleMsgBurnNFT(wasmCustomMsg.Data) + case types.RBurnNFTFrom: + return handleMsgBurnNFTFrom(wasmCustomMsg.Data) + case types.RBurnFT: + return handleMsgBurnFT(wasmCustomMsg.Data) + case types.RBurnFTFrom: + return handleMsgBurnFTFrom(wasmCustomMsg.Data) + case types.RTransferNFT: + return handleMsgTransferNFT(wasmCustomMsg.Data) + case types.RTransferNFTFrom: + return handleMsgTransferNFTFrom(wasmCustomMsg.Data) + case types.RTransferFT: + return handleMsgTransferFT(wasmCustomMsg.Data) + case types.RTransferFTFrom: + return handleMsgTransferFTFrom(wasmCustomMsg.Data) + case types.RModify: + return handleMsgModify(wasmCustomMsg.Data) + case types.RApprove: + return handleMsgApprove(wasmCustomMsg.Data) + case types.RDisapprove: + return handleMsgDisapprove(wasmCustomMsg.Data) + case types.RGrantPerm: + return handleMsgGrantPerm(wasmCustomMsg.Data) + case types.RRevokePerm: + return handleMsgRevokePerm(wasmCustomMsg.Data) + case types.RAttach: + return handleMsgAttach(wasmCustomMsg.Data) + case types.RDetach: + return handleMsgDetach(wasmCustomMsg.Data) + case types.RAttachFrom: + return handleMsgAttachFrom(wasmCustomMsg.Data) + case types.RDetachFrom: + return handleMsgDetachFrom(wasmCustomMsg.Data) + default: + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized Msg route: %T", wasmCustomMsg.Route) + } + } +} + +func handleMsgCreateCollection(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgCreateCollection + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgIssueNFT(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgIssueNFT + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgIssueFT(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgIssueFT + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + if msg.Decimals.Int64() < 0 || msg.Decimals.Int64() > 18 { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "invalid decimals. 0 <= decimals <= 18") + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgMintNFT(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgMintNFT + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgMintFT(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgMintFT + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgBurnNFT(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgBurnNFT + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgBurnNFTFrom(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgBurnNFTFrom + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgBurnFT(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgBurnFT + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgBurnFTFrom(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgBurnFTFrom + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgTransferNFT(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgTransferNFT + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgTransferNFTFrom(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgTransferNFTFrom + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgTransferFT(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgTransferFT + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgTransferFTFrom(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgTransferFTFrom + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgModify(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgModify + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgApprove(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgApprove + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgDisapprove(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgDisapprove + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgGrantPerm(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgGrantPermission + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgRevokePerm(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgRevokePermission + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgAttach(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgAttach + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgDetach(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgDetach + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgAttachFrom(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgAttachFrom + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} + +func handleMsgDetachFrom(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgDetachFrom + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + return []sdk.Msg{msg}, nil +} diff --git a/x/collection/internal/keeper/msg_encoder_test.go b/x/collection/internal/keeper/msg_encoder_test.go new file mode 100644 index 0000000000..f354503a2c --- /dev/null +++ b/x/collection/internal/keeper/msg_encoder_test.go @@ -0,0 +1,311 @@ +package keeper + +import ( + "encoding/json" + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Encode(t *testing.T) { + encodeHandler := NewMsgEncodeHandler(keeper) + + testContractName := "test_collection" + testContractID := "9be17165" + nft1 := "nft-1" + ft1 := "ft-1" + amount := int64(100) + mintNftParams := []types.MintNFTParam{types.NewMintNFTParam(nft1, "", defaultTokenType)} + coins := []types.Coin{types.NewCoin(defaultTokenIDFT, sdk.NewInt(amount))} + changes := []types.Change{types.NewChange("f", "t")} + + create := fmt.Sprintf(`{"route":"create","data":{"owner":"%s", "name":"%s","meta":"","base_img_uri":""}}`, addr1.String(), testContractName) + createMsg := json.RawMessage(create) + issueNft := fmt.Sprintf(`{"route":"issue_nft","data":{"owner":"%s", "contract_id":"%s", "name":"%s","meta":""}}`, addr1.String(), testContractID, nft1) + issueNftMsg := json.RawMessage(issueNft) + issueFt := fmt.Sprintf(`{"route":"issue_ft","data":{"owner":"%s", "contract_id":"%s", "to":"%s", "name":"%s","meta":"", "amount":"%d", "mintable":true, "decimals":"18"}}`, addr1.String(), testContractID, addr2.String(), ft1, amount) + issueFtMsg := json.RawMessage(issueFt) + mintNft := fmt.Sprintf(`{"route":"mint_nft","data":{"from":"%s", "contract_id":"%s", "to":"%s", "params":[{"name":"%s", "meta":"", "token_type":"%s"}]}}`, addr1.String(), testContractID, addr2.String(), nft1, defaultTokenType) + mintNftMsg := json.RawMessage(mintNft) + mintFt := fmt.Sprintf(`{"route":"mint_ft","data":{"from":"%s", "contract_id":"%s", "to":"%s", "amount":[{"token_id":"%s", "amount":"%d"}]}}`, addr1.String(), testContractID, addr2.String(), defaultTokenIDFT, amount) + mintFtMsg := json.RawMessage(mintFt) + burnNft := fmt.Sprintf(`{"route":"burn_nft","data":{"from":"%s", "contract_id":"%s", "token_ids":["%s"]}}`, addr1.String(), testContractID, defaultTokenID1) + burnNftMsg := json.RawMessage(burnNft) + burnNftFrom := fmt.Sprintf(`{"route":"burn_nft_from","data":{"proxy":"%s", "from":"%s","contract_id":"%s", "token_ids":["%s"]}}`, addr2.String(), addr1.String(), testContractID, defaultTokenID1) + burnNftFromMsg := json.RawMessage(burnNftFrom) + burnFt := fmt.Sprintf(`{"route":"burn_ft","data":{"from":"%s","contract_id":"%s", "amount":[{"token_id":"%s", "amount":"%d"}]}}`, addr1.String(), testContractID, defaultTokenIDFT, amount) + burnFtMsg := json.RawMessage(burnFt) + burnFtFrom := fmt.Sprintf(`{"route":"burn_ft_from","data":{"proxy":"%s", "from":"%s","contract_id":"%s", "amount":[{"token_id":"%s", "amount":"%d"}]}}`, addr2.String(), addr1.String(), testContractID, defaultTokenIDFT, amount) + burnFtFromMsg := json.RawMessage(burnFtFrom) + transferNft := fmt.Sprintf(`{"route":"transfer_nft","data":{"from":"%s", "contract_id":"%s", "to":"%s", "token_ids":["%s"]}}`, addr1.String(), testContractID, addr2.String(), defaultTokenID1) + transferNftMsg := json.RawMessage(transferNft) + transferNftFrom := fmt.Sprintf(`{"route":"transfer_nft_from","data":{"proxy":"%s", "from":"%s", "contract_id":"%s", "to":"%s", "token_ids":["%s"]}}`, addr2.String(), addr1.String(), testContractID, addr2.String(), defaultTokenID1) + transferNftFromMsg := json.RawMessage(transferNftFrom) + transferFt := fmt.Sprintf(`{"route":"transfer_ft","data":{"from":"%s", "contract_id":"%s", "to":"%s", "amount":[{"token_id":"%s", "amount":"%d"}]}}`, addr1.String(), testContractID, addr2.String(), defaultTokenIDFT, amount) + transferFtMsg := json.RawMessage(transferFt) + transferFtFrom := fmt.Sprintf(`{"route":"transfer_ft_from","data":{"proxy":"%s", "from":"%s", "contract_id":"%s", "to":"%s", "amount":[{"token_id":"%s", "amount":"%d"}]}}`, addr2.String(), addr1.String(), testContractID, addr2.String(), defaultTokenIDFT, amount) + transferFtFromMsg := json.RawMessage(transferFtFrom) + approve := fmt.Sprintf(`{"route":"approve","data":{"approver":"%s", "contract_id":"%s", "proxy":"%s"}}`, addr1.String(), testContractID, addr2.String()) + approveMsg := json.RawMessage(approve) + disapprove := fmt.Sprintf(`{"route":"disapprove","data":{"approver":"%s", "contract_id":"%s", "proxy":"%s"}}`, addr1.String(), testContractID, addr2.String()) + disapproveMsg := json.RawMessage(disapprove) + attach := fmt.Sprintf(`{"route":"attach","data":{"from":"%s", "contract_id":"%s", "to_token_id":"%s", "token_id":"%s"}}`, addr1.String(), testContractID, defaultTokenID1, defaultTokenID2) + attachMsg := json.RawMessage(attach) + detach := fmt.Sprintf(`{"route":"detach","data":{"from":"%s", "contract_id":"%s", "token_id":"%s"}}`, addr1.String(), testContractID, defaultTokenID1) + detachMsg := json.RawMessage(detach) + attachFrom := fmt.Sprintf(`{"route":"attach_from","data":{"proxy":"%s", "from":"%s", "contract_id":"%s", "to_token_id":"%s", "token_id":"%s"}}`, addr2.String(), addr1.String(), testContractID, defaultTokenID1, defaultTokenID2) + attachFromMsg := json.RawMessage(attachFrom) + detachFrom := fmt.Sprintf(`{"route":"detach_from","data":{"proxy":"%s", "from":"%s", "contract_id":"%s", "token_id":"%s"}}`, addr2.String(), addr1.String(), testContractID, defaultTokenID1) + detachFromMsg := json.RawMessage(detachFrom) + modify := fmt.Sprintf(`{"route":"modify","data":{"owner":"%s", "contract_id":"%s", "token_type":"%s", "token_index":"%s", "changes":[{"field":"f", "value":"t"}]}}`, addr1.String(), testContractID, defaultTokenType, defaultTokenIndex) + modifyMsg := json.RawMessage(modify) + + cases := map[string]struct { + input json.RawMessage + // set if valid + output []sdk.Msg + // set if invalid + isError bool + }{ + "create collection": { + input: createMsg, + output: []sdk.Msg{ + types.MsgCreateCollection{ + Owner: addr1, + Name: testContractName, + Meta: "", + BaseImgURI: "", + }, + }, + }, + "issue nft": { + input: issueNftMsg, + output: []sdk.Msg{ + types.MsgIssueNFT{ + Owner: addr1, + ContractID: testContractID, + Name: nft1, + Meta: "", + }, + }, + }, + "issue ft": { + input: issueFtMsg, + output: []sdk.Msg{ + types.MsgIssueFT{ + Owner: addr1, + ContractID: testContractID, + To: addr2, + Name: ft1, + Meta: "", + Amount: sdk.NewInt(amount), + Mintable: true, + Decimals: sdk.NewInt(18), + }, + }, + }, + "mint nft": { + input: mintNftMsg, + output: []sdk.Msg{ + types.MsgMintNFT{ + From: addr1, + ContractID: testContractID, + To: addr2, + MintNFTParams: mintNftParams, + }, + }, + }, + "mint ft": { + input: mintFtMsg, + output: []sdk.Msg{ + types.MsgMintFT{ + From: addr1, + ContractID: testContractID, + To: addr2, + Amount: coins, + }, + }, + }, + "burn nft": { + input: burnNftMsg, + output: []sdk.Msg{ + types.MsgBurnNFT{ + From: addr1, + ContractID: testContractID, + TokenIDs: []string{defaultTokenID1}, + }, + }, + }, + "burn nft from": { + input: burnNftFromMsg, + output: []sdk.Msg{ + types.MsgBurnNFTFrom{ + Proxy: addr2, + From: addr1, + ContractID: testContractID, + TokenIDs: []string{defaultTokenID1}, + }, + }, + }, + "burn ft": { + input: burnFtMsg, + output: []sdk.Msg{ + types.MsgBurnFT{ + From: addr1, + ContractID: testContractID, + Amount: coins, + }, + }, + }, + "burn ft from": { + input: burnFtFromMsg, + output: []sdk.Msg{ + types.MsgBurnFTFrom{ + Proxy: addr2, + From: addr1, + ContractID: testContractID, + Amount: coins, + }, + }, + }, + "transfer nft": { + input: transferNftMsg, + output: []sdk.Msg{ + types.MsgTransferNFT{ + From: addr1, + ContractID: testContractID, + To: addr2, + TokenIDs: []string{defaultTokenID1}, + }, + }, + }, + "transfer nft from": { + input: transferNftFromMsg, + output: []sdk.Msg{ + types.MsgTransferNFTFrom{ + Proxy: addr2, + From: addr1, + ContractID: testContractID, + To: addr2, + TokenIDs: []string{defaultTokenID1}, + }, + }, + }, + "transfer ft": { + input: transferFtMsg, + output: []sdk.Msg{ + types.MsgTransferFT{ + From: addr1, + ContractID: testContractID, + To: addr2, + Amount: coins, + }, + }, + }, + "transfer ft from": { + input: transferFtFromMsg, + output: []sdk.Msg{ + types.MsgTransferFTFrom{ + Proxy: addr2, + From: addr1, + ContractID: testContractID, + To: addr2, + Amount: coins, + }, + }, + }, + "approve": { + input: approveMsg, + output: []sdk.Msg{ + types.MsgApprove{ + Approver: addr1, + ContractID: testContractID, + Proxy: addr2, + }, + }, + }, + "disapprove": { + input: disapproveMsg, + output: []sdk.Msg{ + types.MsgDisapprove{ + Approver: addr1, + ContractID: testContractID, + Proxy: addr2, + }, + }, + }, + "attach": { + input: attachMsg, + output: []sdk.Msg{ + types.MsgAttach{ + From: addr1, + ContractID: testContractID, + ToTokenID: defaultTokenID1, + TokenID: defaultTokenID2, + }, + }, + }, + "detach": { + input: detachMsg, + output: []sdk.Msg{ + types.MsgDetach{ + From: addr1, + ContractID: testContractID, + TokenID: defaultTokenID1, + }, + }, + }, + "attach from": { + input: attachFromMsg, + output: []sdk.Msg{ + types.MsgAttachFrom{ + Proxy: addr2, + From: addr1, + ContractID: testContractID, + ToTokenID: defaultTokenID1, + TokenID: defaultTokenID2, + }, + }, + }, + "detach from": { + input: detachFromMsg, + output: []sdk.Msg{ + types.MsgDetachFrom{ + Proxy: addr2, + From: addr1, + ContractID: testContractID, + TokenID: defaultTokenID1, + }, + }, + }, + "modify": { + input: modifyMsg, + output: []sdk.Msg{ + types.MsgModify{ + Owner: addr1, + ContractID: testContractID, + TokenType: defaultTokenType, + TokenIndex: defaultTokenIndex, + Changes: changes, + }, + }, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + res, err := encodeHandler(tc.input) + if tc.isError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.output, res) + } + }) + } +} diff --git a/x/collection/internal/keeper/nftowner.go b/x/collection/internal/keeper/nftowner.go new file mode 100644 index 0000000000..78c02a3eb0 --- /dev/null +++ b/x/collection/internal/keeper/nftowner.go @@ -0,0 +1,61 @@ +package keeper + +import ( + "fmt" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func (k Keeper) AddNFTOwner(ctx sdk.Context, addr sdk.AccAddress, tokenID string) { + store := ctx.KVStore(k.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(k.getContractID(ctx), addr, tokenID) + if store.Has(tokenOwnerKey) { + panic(fmt.Sprintf("account: %s already has the token: %s", addr.String(), tokenID)) + } + store.Set(tokenOwnerKey, []byte(tokenID)) +} + +func (k Keeper) DeleteNFTOwner(ctx sdk.Context, addr sdk.AccAddress, tokenID string) { + store := ctx.KVStore(k.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(k.getContractID(ctx), addr, tokenID) + if !store.Has(tokenOwnerKey) { + panic(fmt.Sprintf("account: %s has not the token: %s", addr.String(), tokenID)) + } + store.Delete(tokenOwnerKey) +} + +func (k Keeper) HasNFTOwner(ctx sdk.Context, addr sdk.AccAddress, tokenID string) bool { + store := ctx.KVStore(k.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(k.getContractID(ctx), addr, tokenID) + return store.Has(tokenOwnerKey) +} + +func (k Keeper) ChangeNFTOwner(ctx sdk.Context, from, to sdk.AccAddress, tokenID string) error { + if !k.HasNFTOwner(ctx, from, tokenID) { + return sdkerrors.Wrapf(types.ErrInsufficientToken, "insufficient account funds[%s]; account has no coin", k.getContractID(ctx)) + } + + k.DeleteNFTOwner(ctx, from, tokenID) + k.AddNFTOwner(ctx, to, tokenID) + return nil +} + +func (k Keeper) GetNFTsOwner(ctx sdk.Context, addr sdk.AccAddress) (tokenIDs []string) { + store := ctx.KVStore(k.storeKey) + var iter = sdk.KVStorePrefixIterator(store, types.AccountOwnNFTKey(k.getContractID(ctx), addr, "")) + defer iter.Close() + for { + if !iter.Valid() { + break + } + + val := iter.Value() + tokenIDs = append(tokenIDs, string(val)) + iter.Next() + } + return tokenIDs +} diff --git a/x/collection/internal/keeper/nftowner_test.go b/x/collection/internal/keeper/nftowner_test.go new file mode 100644 index 0000000000..a232ab7436 --- /dev/null +++ b/x/collection/internal/keeper/nftowner_test.go @@ -0,0 +1,120 @@ +package keeper + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper_AddNFTOwner(t *testing.T) { + ctx := cacheKeeper() + t.Log("Add Owner") + { + keeper.AddNFTOwner(ctx, addr1, defaultTokenID1) + } + t.Log("Add Owner Again") + { + require.Panics(t, func() { keeper.AddNFTOwner(ctx, addr1, defaultTokenID1) }, "") + } + t.Log("Get The Data") + { + store := ctx.KVStore(keeper.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(defaultContractID, addr1, defaultTokenID1) + require.True(t, store.Has(tokenOwnerKey)) + } + t.Log("Get The Wrong Data") + { + store := ctx.KVStore(keeper.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(defaultContractID, addr1, defaultTokenID2) + require.False(t, store.Has(tokenOwnerKey)) + } +} + +func TestKeeper_DeleteNFTOwner(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Owner") + { + store := ctx.KVStore(keeper.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(defaultContractID, addr1, defaultTokenID1) + store.Set(tokenOwnerKey, []byte(defaultTokenID1)) + } + t.Log("Delete the Data") + { + keeper.DeleteNFTOwner(ctx, addr1, defaultTokenID1) + } + t.Log("Is deleted") + { + store := ctx.KVStore(keeper.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(defaultContractID, addr1, defaultTokenID1) + require.False(t, store.Has(tokenOwnerKey)) + } + t.Log("Delete Wrong Data") + { + require.Panics(t, func() { keeper.DeleteNFTOwner(ctx, addr1, defaultTokenID1) }, "") + } +} + +func TestKeeepr_HasNFTOwner(t *testing.T) { + ctx := cacheKeeper() + t.Log("Has Not Data") + { + require.False(t, keeper.HasNFTOwner(ctx, addr1, defaultTokenID1)) + } + t.Log("Prepare Owner") + { + store := ctx.KVStore(keeper.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(defaultContractID, addr1, defaultTokenID1) + store.Set(tokenOwnerKey, []byte(defaultTokenID1)) + } + t.Log("Has Data") + { + require.True(t, keeper.HasNFTOwner(ctx, addr1, defaultTokenID1)) + } +} + +func TestKeeper_ChangeNFTOwner(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Owner") + { + store := ctx.KVStore(keeper.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(defaultContractID, addr1, defaultTokenID1) + store.Set(tokenOwnerKey, []byte(defaultTokenID1)) + } + t.Log("transfer") + { + require.NoError(t, keeper.ChangeNFTOwner(ctx, addr1, addr2, defaultTokenID1)) + } + t.Log("transfer again") + { + require.EqualError(t, keeper.ChangeNFTOwner(ctx, addr1, addr2, defaultTokenID1), "insufficient token: insufficient account funds[abcdef01]; account has no coin") + } +} + +func TestKeeper_GetNFTsOwner(t *testing.T) { + ctx := cacheKeeper() + { + tokenIDs := keeper.GetNFTsOwner(ctx, addr1) + require.Empty(t, tokenIDs) + } + t.Log("Prepare Owner") + { + store := ctx.KVStore(keeper.storeKey) + tokenOwnerKey := types.AccountOwnNFTKey(defaultContractID, addr1, defaultTokenID1) + store.Set(tokenOwnerKey, []byte(defaultTokenID1)) + + tokenOwnerKey = types.AccountOwnNFTKey(defaultContractID, addr1, defaultTokenID2) + store.Set(tokenOwnerKey, []byte(defaultTokenID2)) + + tokenOwnerKey = types.AccountOwnNFTKey(defaultContractID, addr1, defaultTokenID3) + store.Set(tokenOwnerKey, []byte(defaultTokenID3)) + } + t.Log("Get the data") + { + tokenIDs := keeper.GetNFTsOwner(ctx, addr1) + require.NotEmpty(t, tokenIDs) + require.Equal(t, defaultTokenID1, tokenIDs[0]) + require.Equal(t, defaultTokenID2, tokenIDs[1]) + require.Equal(t, defaultTokenID3, tokenIDs[2]) + } +} diff --git a/x/collection/internal/keeper/params.go b/x/collection/internal/keeper/params.go new file mode 100644 index 0000000000..f547e8eef5 --- /dev/null +++ b/x/collection/internal/keeper/params.go @@ -0,0 +1,16 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramsSpace.SetParamSet(ctx, ¶ms) +} + +func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { + k.paramsSpace.GetParamSet(ctx, ¶ms) + return +} diff --git a/x/collection/internal/keeper/perm.go b/x/collection/internal/keeper/perm.go new file mode 100644 index 0000000000..787c44381a --- /dev/null +++ b/x/collection/internal/keeper/perm.go @@ -0,0 +1,91 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +func (k Keeper) AddPermission(ctx sdk.Context, addr sdk.AccAddress, perm types.Permission) { + accPerm := k.getAccountPermission(ctx, addr) + accPerm.AddPermission(perm) + k.setAccountPermission(ctx, accPerm) +} + +func (k Keeper) HasPermission(ctx sdk.Context, addr sdk.AccAddress, p types.Permission) bool { + accPerm := k.getAccountPermission(ctx, addr) + return accPerm.HasPermission(p) +} + +func (k Keeper) GetPermissions(ctx sdk.Context, addr sdk.AccAddress) types.Permissions { + accPerm := k.getAccountPermission(ctx, addr) + return accPerm.GetPermissions() +} + +func (k Keeper) RevokePermission(ctx sdk.Context, addr sdk.AccAddress, perm types.Permission) error { + if !k.HasPermission(ctx, addr, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr.String(), perm.String()) + } + accPerm := k.getAccountPermission(ctx, addr) + accPerm.RemovePermission(perm) + k.setAccountPermission(ctx, accPerm) + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeRevokePermToken, + sdk.NewAttribute(types.AttributeKeyFrom, addr.String()), + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyPerm, perm.String()), + ), + }) + return nil +} + +func (k Keeper) GrantPermission(ctx sdk.Context, from, to sdk.AccAddress, perm types.Permission) error { + if !k.HasPermission(ctx, from, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", from.String(), perm.String()) + } + k.AddPermission(ctx, to, perm) + + // Set Account if not exists yet + account := k.accountKeeper.GetAccount(ctx, to) + if account == nil { + account = k.accountKeeper.NewAccountWithAddress(ctx, to) + k.accountKeeper.SetAccount(ctx, account) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeGrantPermToken, + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyPerm, perm.String()), + ), + }) + + return nil +} + +func (k Keeper) getAccountPermission(ctx sdk.Context, addr sdk.AccAddress) (accPerm types.AccountPermissionI) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.PermKey(k.getContractID(ctx), addr)) + if bz != nil { + accPerm = k.mustDecodeAccountPermission(bz) + return accPerm + } + return types.NewAccountPermission(addr) +} + +func (k Keeper) setAccountPermission(ctx sdk.Context, accPerm types.AccountPermissionI) { + store := ctx.KVStore(k.storeKey) + store.Set(types.PermKey(k.getContractID(ctx), accPerm.GetAddress()), k.cdc.MustMarshalBinaryBare(accPerm)) +} + +func (k Keeper) mustDecodeAccountPermission(bz []byte) (accPerm types.AccountPermissionI) { + err := k.cdc.UnmarshalBinaryBare(bz, &accPerm) + if err != nil { + panic(err) + } + return +} diff --git a/x/collection/internal/keeper/perm_test.go b/x/collection/internal/keeper/perm_test.go new file mode 100644 index 0000000000..ae6fc7838b --- /dev/null +++ b/x/collection/internal/keeper/perm_test.go @@ -0,0 +1,96 @@ +package keeper + +import ( + "context" + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func TestCollectionAndPermission(t *testing.T) { + ctx := cacheKeeper() + + issuePerm := types.NewIssuePermission() + { + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, + defaultMeta, defaultImgURI), addr1)) + require.True(t, keeper.HasPermission(ctx, addr1, issuePerm)) + require.Error(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, + defaultMeta, defaultImgURI), addr1)) + collection, err := keeper.GetCollection(ctx) + require.NoError(t, err) + require.Equal(t, defaultContractID, collection.GetContractID()) + + { + require.NoError(t, keeper.IssueFT(ctx, addr1, addr1, types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true), sdk.NewInt(defaultAmount))) + token, err := keeper.GetToken(ctx, defaultTokenIDFT) + require.NoError(t, err) + require.Equal(t, defaultContractID, token.GetContractID()) + require.Equal(t, defaultTokenIDFT, token.GetTokenID()) + } + { + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1)) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1))) + + token, err := keeper.GetToken(ctx, defaultTokenID1) + require.NoError(t, err) + require.Equal(t, defaultContractID, token.GetContractID()) + require.Equal(t, defaultTokenID1, token.GetTokenID()) + + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID2, defaultName, defaultMeta, addr1))) + token, err = keeper.GetToken(ctx, defaultTokenID2) + require.NoError(t, err) + require.Equal(t, defaultContractID, token.GetContractID()) + require.Equal(t, defaultTokenID2, token.GetTokenID()) + + count, err := keeper.GetNFTCount(ctx, defaultTokenType) + require.NoError(t, err) + require.Equal(t, int64(2), count.Int64()) + + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType2, defaultName, defaultMeta), addr1)) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenType2+"00000001", defaultName, defaultMeta, addr1))) + token, err = keeper.GetToken(ctx, defaultTokenType2+"00000001") + require.NoError(t, err) + require.Equal(t, defaultContractID, token.GetContractID()) + require.Equal(t, defaultTokenType2+"00000001", token.GetTokenID()) + } + } + { + require.NoError(t, keeper.GrantPermission(ctx, addr1, addr2, issuePerm)) + require.True(t, keeper.HasPermission(ctx, addr1, issuePerm)) + require.True(t, keeper.HasPermission(ctx, addr2, issuePerm)) + } + + issuePerm2 := types.NewIssuePermission() + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID2)) + { + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID2, defaultName, + defaultMeta, defaultImgURI), addr1)) + require.True(t, keeper.HasPermission(ctx, addr1, issuePerm2)) + require.Error(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID2, defaultName, + defaultMeta, defaultImgURI), addr1)) + collection, err := keeper.GetCollection(ctx) + require.NoError(t, err) + require.Equal(t, defaultContractID2, collection.GetContractID()) + } + { + collections := keeper.GetAllCollections(ctx) + require.Equal(t, 2, len(collections)) + require.Equal(t, defaultContractID, collections[0].GetContractID()) + require.Equal(t, defaultContractID2, collections[1].GetContractID()) + } +} + +func TestPermission(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.EqualError(t, keeper.RevokePermission(ctx, addr3, types.NewMintPermission()), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr3.String(), types.NewMintPermission().String()).Error()) + require.NoError(t, keeper.RevokePermission(ctx, addr1, types.NewMintPermission())) + require.EqualError(t, keeper.GrantPermission(ctx, addr3, addr1, types.NewMintPermission()), sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr3.String(), types.NewMintPermission().String()).Error()) +} diff --git a/x/collection/internal/keeper/proxy.go b/x/collection/internal/keeper/proxy.go new file mode 100644 index 0000000000..249506102d --- /dev/null +++ b/x/collection/internal/keeper/proxy.go @@ -0,0 +1,111 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +var ( + ApprovedValue = []byte{0x01} +) + +type ProxyKeeper interface { + IsApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) bool + SetApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) error + DeleteApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) error +} + +func (k Keeper) IsApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) bool { + store := ctx.KVStore(k.storeKey) + approvedKey := types.CollectionApprovedKey(k.getContractID(ctx), proxy, approver) + return store.Has(approvedKey) +} + +func (k Keeper) GetApprovers(ctx sdk.Context, proxy sdk.AccAddress) (accAds []sdk.AccAddress, err error) { + _, err = k.GetCollection(ctx) + if err != nil { + return nil, err + } + k.iterateApprovers(ctx, proxy, false, func(ad sdk.AccAddress) bool { + accAds = append(accAds, ad) + return false + }) + return accAds, nil +} + +func (k Keeper) iterateApprovers(ctx sdk.Context, prefix sdk.AccAddress, reverse bool, process func(accAd sdk.AccAddress) bool) { + store := ctx.KVStore(k.storeKey) + prefixKey := types.CollectionApproversKey(k.getContractID(ctx), prefix) + var iter sdk.Iterator + if reverse { + iter = sdk.KVStoreReversePrefixIterator(store, prefixKey) + } else { + iter = sdk.KVStorePrefixIterator(store, prefixKey) + } + defer iter.Close() + for { + if !iter.Valid() { + return + } + bz := iter.Key() + approver := bz[len(prefixKey):] + if process(approver) { + return + } + iter.Next() + } +} + +func (k Keeper) SetApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) error { + store := ctx.KVStore(k.storeKey) + if !store.Has(types.CollectionKey(k.getContractID(ctx))) { + return sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", k.getContractID(ctx)) + } + approvedKey := types.CollectionApprovedKey(k.getContractID(ctx), proxy, approver) + if store.Has(approvedKey) { + return sdkerrors.Wrapf(types.ErrCollectionAlreadyApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), approver.String(), k.getContractID(ctx)) + } + store.Set(approvedKey, ApprovedValue) + + // Set Account if not exists yet + account := k.accountKeeper.GetAccount(ctx, proxy) + if account == nil { + account = k.accountKeeper.NewAccountWithAddress(ctx, proxy) + k.accountKeeper.SetAccount(ctx, account) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeApproveCollection, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyApprover, approver.String()), + ), + }) + + return nil +} + +func (k Keeper) DeleteApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) error { + store := ctx.KVStore(k.storeKey) + if !store.Has(types.CollectionKey(k.getContractID(ctx))) { + return sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", k.getContractID(ctx)) + } + approvedKey := types.CollectionApprovedKey(k.getContractID(ctx), proxy, approver) + if !store.Has(approvedKey) { + return sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), approver.String(), k.getContractID(ctx)) + } + store.Delete(approvedKey) + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeDisapproveCollection, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyApprover, approver.String()), + ), + }) + + return nil +} diff --git a/x/collection/internal/keeper/proxy_test.go b/x/collection/internal/keeper/proxy_test.go new file mode 100644 index 0000000000..a32a8dfb7a --- /dev/null +++ b/x/collection/internal/keeper/proxy_test.go @@ -0,0 +1,53 @@ +package keeper + +import ( + "context" + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func TestApproveDisapproveScenario(t *testing.T) { + ctx := cacheKeeper() + const ( + defaultTokenIDFromContractID2 = defaultTokenType2 + "00000001" + ) + + // prepare collection, FT, NFT + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, "name", defaultMeta, defaultImgURI), addr1)) + require.NoError(t, keeper.IssueFT(ctx, addr1, addr1, types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true), sdk.NewInt(defaultAmount))) + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1)) + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType2, defaultName, defaultMeta), addr1)) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1))) + require.NoError(t, keeper.MintNFT(ctx, addr1, types.NewNFT(defaultContractID, defaultTokenType2+"00000001", defaultName, defaultMeta, addr1))) + + // approve test + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID2)) + require.EqualError(t, keeper.SetApproved(ctx2, addr3, addr1), sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", defaultContractID2).Error()) + require.NoError(t, keeper.SetApproved(ctx, addr3, addr1)) + require.EqualError(t, keeper.SetApproved(ctx, addr3, addr1), sdkerrors.Wrapf(types.ErrCollectionAlreadyApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr3.String(), addr1.String(), defaultContractID).Error()) + + // attach_from/detach_from test + require.EqualError(t, keeper.AttachFrom(ctx, addr2, addr1, defaultTokenID1, defaultTokenIDFromContractID2), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr2.String(), addr1.String(), defaultContractID).Error()) + require.NoError(t, keeper.AttachFrom(ctx, addr3, addr1, defaultTokenID1, defaultTokenIDFromContractID2)) + require.EqualError(t, keeper.DetachFrom(ctx, addr2, addr1, defaultTokenIDFromContractID2), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr2.String(), addr1.String(), defaultContractID).Error()) + require.NoError(t, keeper.DetachFrom(ctx, addr3, addr1, defaultTokenIDFromContractID2)) + + // transfer_from test + require.EqualError(t, keeper.TransferFTFrom(ctx, addr2, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(10))), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr2.String(), addr1.String(), defaultContractID).Error()) + require.NoError(t, keeper.TransferFTFrom(ctx, addr3, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(10)))) + + require.EqualError(t, keeper.TransferNFTFrom(ctx, addr2, addr1, addr2, defaultTokenID1), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr2.String(), addr1.String(), defaultContractID).Error()) + require.NoError(t, keeper.TransferNFTFrom(ctx, addr3, addr1, addr2, defaultTokenID1)) + + // disapprove test + ctx2 = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID2)) + require.EqualError(t, keeper.DeleteApproved(ctx2, addr3, addr1), sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", defaultContractID2).Error()) + require.NoError(t, keeper.DeleteApproved(ctx, addr3, addr1)) + require.EqualError(t, keeper.DeleteApproved(ctx, addr3, addr1), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr3.String(), addr1.String(), defaultContractID).Error()) +} diff --git a/x/collection/internal/keeper/supply.go b/x/collection/internal/keeper/supply.go new file mode 100644 index 0000000000..ce935bd1cb --- /dev/null +++ b/x/collection/internal/keeper/supply.go @@ -0,0 +1,98 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type SupplyKeeper interface { + GetTotalInt(ctx sdk.Context, tokenID, target string) (supply sdk.Int, err error) + GetSupply(ctx sdk.Context) (supply types.Supply) + SetSupply(ctx sdk.Context, supply types.Supply) + MintSupply(ctx sdk.Context, to sdk.AccAddress, amt types.Coins) error + BurnSupply(ctx sdk.Context, from sdk.AccAddress, amt types.Coins) error +} + +var _ SupplyKeeper = (*Keeper)(nil) + +func (k Keeper) GetSupply(ctx sdk.Context) (supply types.Supply) { + store := ctx.KVStore(k.storeKey) + b := store.Get(types.SupplyKey(k.getContractID(ctx))) + if b == nil { + panic("stored supply should not have been nil") + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &supply) + return +} + +func (k Keeper) SetSupply(ctx sdk.Context, supply types.Supply) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(supply) + store.Set(types.SupplyKey(supply.GetContractID()), b) +} + +func (k Keeper) GetTotalInt(ctx sdk.Context, tokenID, target string) (supply sdk.Int, err error) { + if _, err = k.GetToken(ctx, tokenID); err != nil { + return sdk.NewInt(0), err + } + + s := k.GetSupply(ctx) + switch target { + case types.QuerySupply: + return s.GetTotalSupply().AmountOf(tokenID), nil + case types.QueryBurn: + return s.GetTotalBurn().AmountOf(tokenID), nil + case types.QueryMint: + return s.GetTotalMint().AmountOf(tokenID), nil + default: + return sdk.ZeroInt(), sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid request target to query total %s", target) + } +} + +// MintCoins creates new coins from thin air and adds it to the module account. +// Panics if the name maps to a non-minter module account or if the amount is invalid. +func (k Keeper) MintSupply(ctx sdk.Context, to sdk.AccAddress, amt types.Coins) (err error) { + defer func() { + // to recover from overflows + if r := recover(); r != nil { + err = types.WrapIfOverflowPanic(r) + } + }() + + _, err = k.AddCoins(ctx, to, amt) + if err != nil { + return err + } + supply := k.GetSupply(ctx) + supply = supply.Inflate(amt) + // supply should never be negative. Big.Int.Add will be panic if it becomes overflow + + k.SetSupply(ctx, supply) + return nil +} + +// BurnCoins burns coins deletes coins from the balance of the module account. +// Panics if the name maps to a non-burner module account or if the amount is invalid. +func (k Keeper) BurnSupply(ctx sdk.Context, from sdk.AccAddress, amt types.Coins) (err error) { + defer func() { + // to recover from overflows + // however, it will return insufficient fund error instead of panicking in the case + if r := recover(); r != nil { + err = types.WrapIfOverflowPanic(r) + } + }() + + _, err = k.SubtractCoins(ctx, from, amt) + if err != nil { + return err + } + supply := k.GetSupply(ctx) + supply = supply.Deflate(amt) + if supply.GetTotalSupply().IsAnyNegative() { + return sdkerrors.Wrapf(types.ErrInsufficientSupply, "insufficient supply for token [%s]", k.getContractID(ctx)) + } + k.SetSupply(ctx, supply) + + return nil +} diff --git a/x/collection/internal/keeper/supply_test.go b/x/collection/internal/keeper/supply_test.go new file mode 100644 index 0000000000..a63ac6900d --- /dev/null +++ b/x/collection/internal/keeper/supply_test.go @@ -0,0 +1,222 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +func TestKeeper_GetTotalInt(t *testing.T) { + ctx := cacheKeeper() + addr1 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + t.Log("Prepare Supply and FT") + expected := types.DefaultSupply(defaultContractID) + ft := types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.ZeroInt(), true) + { + keeper.SetSupply(ctx, expected) + err := keeper.SetCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI)) + require.NoError(t, err) + err = keeper.IssueFT(ctx, addr1, addr1, ft, sdk.NewInt(defaultAmount)) + require.NoError(t, err) + } + t.Log("Get Total Supply Int") + { + actual, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, int64(defaultAmount), actual.Int64()) + } + t.Log("Get Total Mint Int") + { + actual, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryMint) + require.NoError(t, err) + require.Equal(t, int64(defaultAmount), actual.Int64()) + } + t.Log("Get Total Burn Int") + { + actual, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, int64(0), actual.Int64()) + } +} + +func TestKeeper_MintSupply(t *testing.T) { + ctx := cacheKeeper() + addr1 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + t.Log("Prepare Supply and FT") + expected := types.DefaultSupply(defaultContractID) + ft := types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.ZeroInt(), true) + { + keeper.SetSupply(ctx, expected) + err := keeper.SetCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI)) + require.NoError(t, err) + err = keeper.IssueFT(ctx, addr1, addr1, ft, sdk.NewInt(defaultAmount)) + require.NoError(t, err) + } + t.Log("Get Balance") + { + balance, err := keeper.GetBalance(ctx, defaultTokenIDFT, addr1) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount).Int64(), balance.Int64()) + } + t.Log("Mint Supply") + { + require.NoError(t, keeper.MintSupply(ctx, addr1, types.OneCoins(defaultTokenIDFT))) + } + t.Log("Get Balance") + { + balance, err := keeper.GetBalance(ctx, defaultTokenIDFT, addr1) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount+1).Int64(), balance.Int64()) + } + t.Log("Get Total Supply Int") + { + actual, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount+1), actual) + } + t.Log("Get Total Mint Int") + { + actual, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryMint) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount+1), actual) + } + t.Log("Get Total Burn Int") + { + actual, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, sdk.ZeroInt(), actual) + } +} + +func TestKeeper_BurnSupply(t *testing.T) { + ctx := cacheKeeper() + addr1 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + t.Log("Prepare Supply and FT") + expected := types.DefaultSupply(defaultContractID) + ft := types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.ZeroInt(), true) + { + keeper.SetSupply(ctx, expected) + err := keeper.SetCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI)) + require.NoError(t, err) + err = keeper.IssueFT(ctx, addr1, addr1, ft, sdk.NewInt(defaultAmount)) + require.NoError(t, err) + } + t.Log("Get Balance") + { + balance, err := keeper.GetBalance(ctx, defaultTokenIDFT, addr1) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount).Int64(), balance.Int64()) + } + t.Log("Burn Supply") + { + require.NoError(t, keeper.BurnSupply(ctx, addr1, types.OneCoins(defaultTokenIDFT))) + } + t.Log("Get Balance") + { + balance, err := keeper.GetBalance(ctx, defaultTokenIDFT, addr1) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount-1).Int64(), balance.Int64()) + } + t.Log("Get Total Supply Int") + { + actual, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount-1), actual) + } + t.Log("Get Total Mint Int") + { + actual, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryMint) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount), actual) + } + t.Log("Get Total Burn Int") + { + actual, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(1), actual) + } +} + +func TestKeeper_Handle_Overflows(t *testing.T) { + ctx := cacheKeeper() + addr1 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + // int64 is the set of all signed 64-bit integers. + // Range: -9223372036854775808 through 9223372036854775807. + + // Int wraps integer with 256 bit range bound + // Checks overflow, underflow and division by zero + // Exists in range from -(2^maxBitLen-1) to 2^maxBitLen-1 + + t.Log("Prepare Supply and FT less than the overflow limit") + maxInt64Supply := sdk.NewInt(9223372036854775807) + initialSupply := maxInt64Supply.Mul(maxInt64Supply).Mul(maxInt64Supply).Mul(maxInt64Supply) + ft := types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, initialSupply, true) + newSupply := types.NewSupply(defaultContractID, types.NewCoins(types.NewCoin(defaultTokenIDFT, initialSupply))) + { + keeper.SetSupply(ctx, newSupply) + err := keeper.SetCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI)) + require.NoError(t, err) + err = keeper.IssueFT(ctx, addr1, addr1, ft, sdk.ZeroInt()) + require.NoError(t, err) + } + + ts, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalSupply().AmountOf(defaultTokenIDFT), ts) + + tm, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryMint) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalMint().AmountOf(defaultTokenIDFT), tm) + + tb, err := keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalBurn().AmountOf(defaultTokenIDFT), tb) + + // inflate over the overflow limit + t.Log("Inflate the supply over the overflow limit") + addToOverflow := types.NewCoins(types.NewCoin(defaultTokenIDFT, initialSupply.Mul(sdk.NewInt(8)))) + err = keeper.MintSupply(ctx, addr1, addToOverflow) + require.Equal(t, types.ErrSupplyOverflow, err) + + // should have not changed + t.Log("Totals have not changed") + ts, err = keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalSupply().AmountOf(defaultTokenIDFT), ts) + + tm, err = keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryMint) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalMint().AmountOf(defaultTokenIDFT), tm) + + tb, err = keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalBurn().AmountOf(defaultTokenIDFT), tb) + + // deflate below the overflow limit + t.Log("Deflate the supply below the overflow limit") + subToOverflow := types.NewCoins(types.NewCoin(defaultTokenIDFT, initialSupply.Mul(sdk.NewInt(8)))) + err = keeper.BurnSupply(ctx, addr1, subToOverflow) + require.Error(t, err) + require.Equal(t, types.ErrSupplyOverflow, err) + + // should have not changed + t.Log("Totals have not changed") + ts, err = keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalSupply().AmountOf(defaultTokenIDFT), ts) + + tm, err = keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryMint) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalMint().AmountOf(defaultTokenIDFT), tm) + + tb, err = keeper.GetTotalInt(ctx, defaultTokenIDFT, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalBurn().AmountOf(defaultTokenIDFT), tb) +} diff --git a/x/collection/internal/keeper/test_common.go b/x/collection/internal/keeper/test_common.go new file mode 100644 index 0000000000..6e0ebbc2af --- /dev/null +++ b/x/collection/internal/keeper/test_common.go @@ -0,0 +1,59 @@ +package keeper + +import ( + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestKeeper() (sdk.Context, store.CommitMultiStore, Keeper) { + keyAuth := sdk.NewKVStoreKey(auth.StoreKey) + keyParams := sdk.NewKVStoreKey(params.StoreKey) + tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) + keyCollection := sdk.NewKVStoreKey(types.StoreKey) + keyContract := sdk.NewKVStoreKey(contract.StoreKey) + + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyAuth, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyCollection, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyContract, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + + if err := ms.LoadLatestVersion(); err != nil { + panic(err) + } + + cdc := codec.New() + types.RegisterCodec(cdc) + auth.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + cdc.Seal() + + paramsKeeper := params.NewKeeper(cdc, keyParams, tkeyParams) + authSubspace := paramsKeeper.Subspace(auth.DefaultParamspace) + + // add keepers + accountKeeper := auth.NewAccountKeeper(cdc, keyAuth, authSubspace, auth.ProtoBaseAccount) + paramsSpace := paramsKeeper.Subspace(types.DefaultParamspace) + keeper := NewKeeper( + cdc, + accountKeeper, + contract.NewContractKeeper(cdc, keyContract), + paramsSpace, + keyCollection, + ) + + ctx := sdk.NewContext(ms, abci.Header{ChainID: "test-chain-id"}, false, log.NewNopLogger()) + keeper.SetParams(ctx, types.DefaultParams()) + return ctx, ms, keeper +} diff --git a/x/collection/internal/keeper/token.go b/x/collection/internal/keeper/token.go new file mode 100644 index 0000000000..d20f0b7eea --- /dev/null +++ b/x/collection/internal/keeper/token.go @@ -0,0 +1,318 @@ +// nolint:unparam +package keeper + +import ( + "math/big" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type TokenKeeper interface { + GetToken(ctx sdk.Context, tokenID string) (types.Token, error) + HasToken(ctx sdk.Context, tokenID string) bool + SetToken(ctx sdk.Context, token types.Token) error + DeleteToken(ctx sdk.Context, tokenID string) error + UpdateToken(ctx sdk.Context, token types.Token) error + GetTokens(ctx sdk.Context) (tokens types.Tokens, err error) + GetFT(ctx sdk.Context, tokenID string) (types.FT, error) + GetFTs(ctx sdk.Context) (tokens types.Tokens, err error) + GetNFT(ctx sdk.Context, tokenID string) (types.NFT, error) + GetNFTCount(ctx sdk.Context, tokenType string) (sdk.Int, error) + GetNFTCountInt(ctx sdk.Context, tokenType, target string) (sdk.Int, error) + GetNFTs(ctx sdk.Context, tokenType string) (tokens types.Tokens, err error) + GetNextTokenIDFT(ctx sdk.Context) (string, error) + GetNextTokenIDNFT(ctx sdk.Context, tokenType string) (string, error) +} + +var _ TokenKeeper = (*Keeper)(nil) + +func (k Keeper) GetToken(ctx sdk.Context, tokenID string) (types.Token, error) { + store := ctx.KVStore(k.storeKey) + tokenKey := types.TokenKey(k.getContractID(ctx), tokenID) + bz := store.Get(tokenKey) + if bz == nil { + return nil, sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", k.getContractID(ctx), tokenID) + } + token := k.mustDecodeToken(bz) + return token, nil +} +func (k Keeper) HasToken(ctx sdk.Context, tokenID string) bool { + store := ctx.KVStore(k.storeKey) + tokenKey := types.TokenKey(k.getContractID(ctx), tokenID) + return store.Has(tokenKey) +} + +func (k Keeper) SetToken(ctx sdk.Context, token types.Token) error { + store := ctx.KVStore(k.storeKey) + tokenKey := types.TokenKey(k.getContractID(ctx), token.GetTokenID()) + if store.Has(tokenKey) { + return sdkerrors.Wrapf(types.ErrTokenExist, "ContractID: %s, TokenID: %s", k.getContractID(ctx), token.GetTokenID()) + } + store.Set(tokenKey, k.mustEncodeToken(token)) + tokenType := token.GetTokenType() + if tokenType[0] == types.FungibleFlag[0] { + k.setNextTokenTypeFT(ctx, tokenType) + } else { + k.setNextTokenIndexNFT(ctx, tokenType, token.GetTokenIndex()) + } + return nil +} + +func (k Keeper) UpdateToken(ctx sdk.Context, token types.Token) error { + store := ctx.KVStore(k.storeKey) + tokenKey := types.TokenKey(k.getContractID(ctx), token.GetTokenID()) + if !store.Has(tokenKey) { + return sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TotkenID: %s", k.getContractID(ctx), token.GetTokenID()) + } + store.Set(tokenKey, k.mustEncodeToken(token)) + return nil +} + +func (k Keeper) DeleteToken(ctx sdk.Context, tokenID string) error { + store := ctx.KVStore(k.storeKey) + tokenKey := types.TokenKey(k.getContractID(ctx), tokenID) + if !store.Has(tokenKey) { + return sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TotkenID: %s", k.getContractID(ctx), tokenID) + } + store.Delete(tokenKey) + return nil +} + +func (k Keeper) GetTokens(ctx sdk.Context) (tokens types.Tokens, err error) { + _, err = k.GetCollection(ctx) + if err != nil { + return nil, err + } + k.iterateToken(ctx, "", false, func(t types.Token) bool { + tokens = append(tokens, t) + return false + }) + return tokens, nil +} + +func (k Keeper) GetFTs(ctx sdk.Context) (tokens types.Tokens, err error) { + _, err = k.GetCollection(ctx) + if err != nil { + return nil, err + } + k.iterateToken(ctx, types.FungibleFlag, false, func(t types.Token) bool { + tokens = append(tokens, t) + return false + }) + return tokens, nil +} + +func (k Keeper) GetFT(ctx sdk.Context, tokenID string) (types.FT, error) { + token, err := k.GetToken(ctx, tokenID) + if err != nil { + return nil, err + } + ft, ok := token.(types.FT) + if !ok { + return nil, sdkerrors.Wrapf(types.ErrTokenNotNFT, "TokenID: %s", token.GetTokenID()) + } + return ft, nil +} + +func (k Keeper) GetNFT(ctx sdk.Context, tokenID string) (types.NFT, error) { + token, err := k.GetToken(ctx, tokenID) + if err != nil { + return nil, err + } + nft, ok := token.(types.NFT) + if !ok { + return nil, sdkerrors.Wrapf(types.ErrTokenNotNFT, "TokenID: %s", token.GetTokenID()) + } + return nft, nil +} + +func (k Keeper) GetNFTs(ctx sdk.Context, tokenType string) (tokens types.Tokens, err error) { + _, err = k.GetCollection(ctx) + if err != nil { + return nil, err + } + k.iterateToken(ctx, tokenType, false, func(t types.Token) bool { + tokens = append(tokens, t) + return false + }) + return tokens, nil +} + +func (k Keeper) GetNFTCount(ctx sdk.Context, tokenType string) (sdk.Int, error) { + _, err := k.GetCollection(ctx) + if err != nil { + return sdk.ZeroInt(), err + } + tokens, err := k.GetNFTs(ctx, tokenType) + if err != nil { + return sdk.ZeroInt(), err + } + return sdk.NewInt(int64(len(tokens))), nil +} + +func (k Keeper) GetNFTCountInt(ctx sdk.Context, tokenType, target string) (sdk.Int, error) { + _, err := k.GetCollection(ctx) + if err != nil { + return sdk.ZeroInt(), err + } + _, err = k.GetTokenType(ctx, tokenType) + if err != nil { + return sdk.ZeroInt(), err + } + switch target { + case types.QueryNFTCount: + return k.getNFTCountTotal(ctx, tokenType), nil + case types.QueryNFTMint: + return k.getNFTCountMint(ctx, tokenType), nil + case types.QueryNFTBurn: + return k.getNFTCountBurn(ctx, tokenType), nil + default: + panic("invalid request target to query") + } +} +func (k Keeper) getNFTCountTotal(ctx sdk.Context, tokenType string) sdk.Int { + return k.getNFTCountMint(ctx, tokenType).Sub(k.getNFTCountBurn(ctx, tokenType)) +} +func (k Keeper) getNFTCountMint(ctx sdk.Context, tokenType string) sdk.Int { + return k.getTokenTypeMintCount(ctx, tokenType) +} + +func (k Keeper) getNFTCountBurn(ctx sdk.Context, tokenType string) sdk.Int { + return k.getTokenTypeBurnCount(ctx, tokenType) +} + +func (k Keeper) setNextTokenTypeFT(ctx sdk.Context, tokenType string) { + store := ctx.KVStore(k.storeKey) + tokenType = nextID(tokenType) + store.Set(types.NextTokenTypeFTKey(k.getContractID(ctx)), k.mustEncodeString(tokenType)) +} +func (k Keeper) setNextTokenTypeNFT(ctx sdk.Context, tokenType string) { + store := ctx.KVStore(k.storeKey) + tokenType = nextID(tokenType) + store.Set(types.NextTokenTypeNFTKey(k.getContractID(ctx)), k.mustEncodeString(tokenType)) +} +func (k Keeper) setNextTokenIndexNFT(ctx sdk.Context, tokenType, tokenIndex string) { + store := ctx.KVStore(k.storeKey) + tokenIndex = nextID(tokenIndex) + store.Set(types.NextTokenIDNFTKey(k.getContractID(ctx), tokenType), k.mustEncodeString(tokenIndex)) +} + +func (k Keeper) getNextTokenTypeFT(ctx sdk.Context) (tokenType string, err error) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.NextTokenTypeFTKey(k.getContractID(ctx))) + if bz == nil { + panic("next token type for ft should be exist") + } + tokenType = k.mustDecodeString(bz) + if tokenType[0] != types.FungibleFlag[0] { + return "", sdkerrors.Wrapf(types.ErrTokenTypeFull, "contract id: %s", k.getContractID(ctx)) + } + return tokenType, nil +} + +func (k Keeper) getNextTokenTypeNFT(ctx sdk.Context) (tokenType string, err error) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.NextTokenTypeNFTKey(k.getContractID(ctx))) + if bz == nil { + panic("next token type for nft should be exist") + } + tokenType = k.mustDecodeString(bz) + if tokenType == types.ReservedEmpty { + return "", sdkerrors.Wrapf(types.ErrTokenTypeFull, "contract id: %s", k.getContractID(ctx)) + } + return tokenType, nil +} + +func (k Keeper) getNextTokenIndexNFT(ctx sdk.Context, tokenType string) (tokenIndex string, error error) { + if !k.HasTokenType(ctx, tokenType) { + return "", sdkerrors.Wrapf(types.ErrTokenTypeNotExist, "ContractID: %s, TokenType: %s", k.getContractID(ctx), tokenType) + } + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.NextTokenIDNFTKey(k.getContractID(ctx), tokenType)) + if bz == nil { + panic("next token id for nft token type should be exist") + } + tokenIndex = k.mustDecodeString(bz) + if tokenIndex == types.ReservedEmpty { + return "", sdkerrors.Wrapf(types.ErrTokenIndexFull, "ContractID: %s, TokenType: %s", k.getContractID(ctx), tokenType) + } + return tokenIndex, nil +} + +func (k Keeper) GetNextTokenIDFT(ctx sdk.Context) (string, error) { + if !k.ExistCollection(ctx) { + return "", sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", k.getContractID(ctx)) + } + tokenType, err := k.getNextTokenTypeFT(ctx) + if err != nil { + return "", sdkerrors.Wrapf(types.ErrTokenIDFull, "ContractID: %s, TokenType: %s", k.getContractID(ctx), tokenType) + } + return tokenType + types.ReservedEmpty, nil +} +func (k Keeper) GetNextTokenIDNFT(ctx sdk.Context, tokenType string) (string, error) { + if !k.ExistCollection(ctx) { + return "", sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", k.getContractID(ctx)) + } + tokenIndex, err := k.getNextTokenIndexNFT(ctx, tokenType) + if err != nil { + return "", err + } + + if tokenIndex == types.ReservedEmpty { + return "", sdkerrors.Wrapf(types.ErrTokenIndexFull, "ContractID: %s, TokenType: %s", k.getContractID(ctx), tokenType) + } + return tokenType + tokenIndex, nil +} + +func (k Keeper) iterateToken(ctx sdk.Context, prefix string, reverse bool, process func(types.Token) (stop bool)) { + store := ctx.KVStore(k.storeKey) + var iter sdk.Iterator + if reverse { + iter = sdk.KVStoreReversePrefixIterator(store, types.TokenKey(k.getContractID(ctx), prefix)) + } else { + iter = sdk.KVStorePrefixIterator(store, types.TokenKey(k.getContractID(ctx), prefix)) + } + defer iter.Close() + for { + if !iter.Valid() { + return + } + val := iter.Value() + token := k.mustDecodeToken(val) + if process(token) { + return + } + iter.Next() + } +} + +func (k Keeper) mustEncodeToken(token types.Token) (bz []byte) { + return k.cdc.MustMarshalBinaryBare(token) +} +func (k Keeper) mustDecodeToken(bz []byte) (token types.Token) { + k.cdc.MustUnmarshalBinaryBare(bz, &token) + return token +} + +func fromHex(s string) *big.Int { + r, ok := new(big.Int).SetString(s, 16) + if !ok { + panic("bad hex") + } + return r +} + +func toHex(r *big.Int) string { + return r.Text(16) +} + +func nextID(id string) (nextTokenID string) { + idInt := fromHex(id) + idInt = idInt.Add(idInt, big.NewInt(1)) + nextTokenID = strings.Repeat("0", len(id)) + toHex(idInt) + nextTokenID = nextTokenID[len(nextTokenID)-len(id):] + return nextTokenID +} diff --git a/x/collection/internal/keeper/token_test.go b/x/collection/internal/keeper/token_test.go new file mode 100644 index 0000000000..eeec55722f --- /dev/null +++ b/x/collection/internal/keeper/token_test.go @@ -0,0 +1,282 @@ +package keeper + +import ( + "strings" + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestKeeper_GetToken(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Token") + var expected types.Token + expected = types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true) + { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.TokenKey(defaultContractID, defaultTokenIDFT), keeper.cdc.MustMarshalBinaryBare(expected)) + } + t.Log("Get Token") + { + actual, err := keeper.GetToken(ctx, defaultTokenIDFT) + require.NoError(t, err) + verifyTokenFunc(t, expected, actual) + } + t.Log("Prepare Token") + expected = types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1) + { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.TokenKey(defaultContractID, defaultTokenID1), keeper.cdc.MustMarshalBinaryBare(expected)) + } + t.Log("Get Token") + { + actual, err := keeper.GetToken(ctx, defaultTokenID1) + require.NoError(t, err) + verifyTokenFunc(t, expected, actual) + } +} +func TestKeeper_SetToken(t *testing.T) { + ctx := cacheKeeper() + var expected types.Token + t.Log("Set Token") + expected = types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true) + { + require.NoError(t, keeper.SetToken(ctx, expected)) + } + t.Log("Compare Token") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.TokenKey(defaultContractID, defaultTokenIDFT)) + actual := keeper.mustDecodeToken(bz) + verifyTokenFunc(t, expected, actual) + } + t.Log("Set Token") + expected = types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1) + { + require.NoError(t, keeper.SetToken(ctx, expected)) + } + t.Log("Compare Token") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.TokenKey(defaultContractID, defaultTokenID1)) + actual := keeper.mustDecodeToken(bz) + verifyTokenFunc(t, expected, actual) + } +} + +func TestKeeper_UpdateToken(t *testing.T) { + ctx := cacheKeeper() + var expected, token types.Token + t.Log("Set Token") + token = types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true) + { + require.NoError(t, keeper.SetToken(ctx, token)) + } + t.Log("Update Token") + expected = types.NewFT(defaultContractID, defaultTokenIDFT, "modifiedname", defaultMeta, sdk.NewInt(defaultDecimals), true) + { + require.NoError(t, keeper.UpdateToken(ctx, expected)) + } + t.Log("Compare Token") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.TokenKey(defaultContractID, defaultTokenIDFT)) + actual := keeper.mustDecodeToken(bz) + verifyTokenFunc(t, expected, actual) + } + t.Log("Set Token") + token = types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1) + { + require.NoError(t, keeper.SetToken(ctx, token)) + } + t.Log("Update Token") + expected = types.NewFT(defaultContractID, defaultTokenID1, "modifiedname", defaultMeta, sdk.NewInt(defaultDecimals), true) + { + require.NoError(t, keeper.UpdateToken(ctx, expected)) + } + t.Log("Compare Token") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.TokenKey(defaultContractID, defaultTokenID1)) + actual := keeper.mustDecodeToken(bz) + verifyTokenFunc(t, expected, actual) + } +} + +func TestKeeper_GeTokens(t *testing.T) { + ctx := cacheKeeper() + var allTokens types.Tokens + t.Log("Prepare collection") + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI), addr1)) + t.Log("Prepare FT Tokens") + expected := types.Tokens{ + types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true), + types.NewFT(defaultContractID, defaultTokenIDFT2, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true), + types.NewFT(defaultContractID, defaultTokenIDFT3, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true), + types.NewFT(defaultContractID, defaultTokenIDFT4, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true), + } + allTokens = append(allTokens, expected...) + { + for _, to := range expected { + require.NoError(t, keeper.IssueFT(ctx, addr1, addr1, to.(types.FT), sdk.NewInt(10))) + } + } + t.Log("Compare FT Tokens") + { + actual, err := keeper.GetFTs(ctx) + require.NoError(t, err) + for index := range expected { + verifyTokenFunc(t, expected[index], actual[index]) + } + } + t.Log("Prepare NFT Tokens") + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1)) + expected = types.Tokens{ + types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1), + types.NewNFT(defaultContractID, defaultTokenID2, defaultName, defaultMeta, addr1), + types.NewNFT(defaultContractID, defaultTokenID3, defaultName, defaultMeta, addr1), + types.NewNFT(defaultContractID, defaultTokenID4, defaultName, defaultMeta, addr1), + types.NewNFT(defaultContractID, defaultTokenID5, defaultName, defaultMeta, addr1), + } + allTokens = append(allTokens, expected...) + { + for _, to := range expected { + require.NoError(t, keeper.MintNFT(ctx, addr1, to.(types.NFT))) + } + } + t.Log("Compare NFT Tokens") + { + actual, err := keeper.GetNFTs(ctx, defaultTokenType) + require.NoError(t, err) + for index := range expected { + verifyTokenFunc(t, expected[index], actual[index]) + } + } + t.Log("Compare NFT Tokens Count") + { + count, err := keeper.GetNFTCount(ctx, defaultTokenType) + require.NoError(t, err) + require.Equal(t, int64(5), count.Int64()) + } + + t.Log("Compare NFT Tokens Count Int") + { + count, err := keeper.GetNFTCountInt(ctx, defaultTokenType, types.QueryNFTCount) + require.NoError(t, err) + require.Equal(t, int64(5), count.Int64()) + } + t.Log("Compare NFT Tokens Count Int") + { + count, err := keeper.GetNFTCountInt(ctx, defaultTokenType, types.QueryNFTMint) + require.NoError(t, err) + require.Equal(t, int64(5), count.Int64()) + } + t.Log("Compare NFT Tokens Count Int") + { + count, err := keeper.GetNFTCountInt(ctx, defaultTokenType, types.QueryNFTBurn) + require.NoError(t, err) + require.Equal(t, int64(0), count.Int64()) + } + + t.Log("Compare All Tokens") + { + actual, err := keeper.GetTokens(ctx) + require.NoError(t, err) + for index := range allTokens { + verifyTokenFunc(t, allTokens[index], actual[index]) + } + } +} + +func TestKeeper_GetNextTokenIDFT(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare collection") + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI), addr1)) + t.Log("Get Next Token ID FT") + { + tokenID, err := keeper.GetNextTokenIDFT(ctx) + require.NoError(t, err) + require.Equal(t, defaultTokenIDFT, tokenID) + } + t.Log("Issue a token and get next token id") + { + require.NoError(t, keeper.SetToken(ctx, types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true))) + tokenID, err := keeper.GetNextTokenIDFT(ctx) + require.NoError(t, err) + require.Equal(t, defaultTokenIDFT2, tokenID) + } + t.Log("Set Full") + { + keeper.setNextTokenTypeFT(ctx, "0fffffff") + _, err := keeper.GetNextTokenIDFT(ctx) + require.Error(t, err) + } +} +func TestKeeper_GetNextTokenIDNFT(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare collection") + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI), addr1)) + t.Log("Prepare Token Type") + expected := types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta) + { + require.NoError(t, keeper.SetTokenType(ctx, expected)) + } + t.Log("Get Next Token ID NFT") + { + tokenID, err := keeper.GetNextTokenIDNFT(ctx, defaultTokenType) + require.NoError(t, err) + require.Equal(t, defaultTokenID1, tokenID) + } + t.Log("Issue a token and get next token id") + { + require.NoError(t, keeper.SetToken(ctx, types.NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1))) + tokenID, err := keeper.GetNextTokenIDNFT(ctx, defaultTokenType) + require.NoError(t, err) + require.Equal(t, defaultTokenID2, tokenID) + } + t.Log("Set Full") + { + keeper.setNextTokenIndexNFT(ctx, defaultTokenType, "ffffffff") + _, err := keeper.GetNextTokenIDNFT(ctx, defaultTokenType) + require.Error(t, err) + } +} + +func TestKeeper_getNFTCountMint(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare collection") + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI), addr1)) + t.Log("Prepare Token Type") + require.NoError(t, keeper.IssueNFT(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta), addr1)) + + keeper.setNextTokenIndexNFT(ctx, defaultTokenType, strings.Repeat("f", len(types.ReservedEmpty))) + require.Equal(t, int64(0), keeper.getNFTCountMint(ctx, defaultTokenType).Int64()) +} + +func TestNextTokenID(t *testing.T) { + require.Panics(t, func() { nextID("") }) + require.Equal(t, "b", nextID("a")) + require.Equal(t, "00", nextID("ff")) + require.Equal(t, "0001", nextID("0000")) + require.Equal(t, "000a", nextID("0009")) + require.Equal(t, "0010", nextID("000f")) + require.Equal(t, "0000", nextID("ffff")) + require.Equal(t, "00000000", nextID("ffffffff")) + require.Equal(t, "abce0000", nextID("abcdffff")) + require.Equal(t, "abcdabc1", nextID("abcdabc0")) + require.Equal(t, "abcd0001", nextID("abcd0000")) + require.Equal(t, "abcd0010", nextID("abcd000f")) + require.Equal(t, "abcdeef0", nextID("abcdeeef")) + require.Equal(t, "abcdef00", nextID("abcdeeff")) + require.Equal(t, "abcd999a", nextID("abcd9999")) + require.Equal(t, "abcd99a0", nextID("abcd999f")) + + next := "0000" + for idx := 0; idx < 16*16*16*16; idx++ { + next = nextID(next) + } + require.Equal(t, "0000", next) +} diff --git a/x/collection/internal/keeper/token_type.go b/x/collection/internal/keeper/token_type.go new file mode 100644 index 0000000000..90e9f600d7 --- /dev/null +++ b/x/collection/internal/keeper/token_type.go @@ -0,0 +1,119 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type TokenTypeKeeper interface { + GetNextTokenType(ctx sdk.Context) (tokenType string, err error) + GetTokenTypes(ctx sdk.Context) (types.TokenTypes, error) + GetTokenType(ctx sdk.Context, tokenType string) (types.TokenType, error) + HasTokenType(ctx sdk.Context, tokenType string) bool + SetTokenType(ctx sdk.Context, token types.TokenType) error + UpdateTokenType(ctx sdk.Context, token types.TokenType) error +} + +var _ TokenTypeKeeper = (*Keeper)(nil) + +func (k Keeper) SetTokenType(ctx sdk.Context, tokenType types.TokenType) error { + _, err := k.GetCollection(ctx) + if err != nil { + return err + } + store := ctx.KVStore(k.storeKey) + if store.Has(types.TokenTypeKey(k.getContractID(ctx), tokenType.GetTokenType())) { + return sdkerrors.Wrapf(types.ErrTokenTypeExist, "ContractID: %s, TokenType: %s", k.getContractID(ctx), tokenType.GetTokenType()) + } + store.Set(types.TokenTypeKey(k.getContractID(ctx), tokenType.GetTokenType()), k.cdc.MustMarshalBinaryBare(tokenType)) + k.setNextTokenTypeNFT(ctx, tokenType.GetTokenType()) + k.setNextTokenIndexNFT(ctx, tokenType.GetTokenType(), types.ReservedEmpty) + return nil +} + +func (k Keeper) UpdateTokenType(ctx sdk.Context, tokenType types.TokenType) error { + _, err := k.GetCollection(ctx) + if err != nil { + return err + } + store := ctx.KVStore(k.storeKey) + if !store.Has(types.TokenTypeKey(k.getContractID(ctx), tokenType.GetTokenType())) { + return sdkerrors.Wrapf(types.ErrTokenTypeNotExist, "ContractID: %s, TokenType: %s", k.getContractID(ctx), tokenType.GetTokenType()) + } + store.Set(types.TokenTypeKey(k.getContractID(ctx), tokenType.GetTokenType()), k.cdc.MustMarshalBinaryBare(tokenType)) + return nil +} + +func (k Keeper) GetTokenType(ctx sdk.Context, tokenTypeID string) (types.TokenType, error) { + store := ctx.KVStore(k.storeKey) + tokenTypeKey := types.TokenTypeKey(k.getContractID(ctx), tokenTypeID) + bz := store.Get(tokenTypeKey) + if bz == nil { + return nil, sdkerrors.Wrapf(types.ErrTokenTypeNotExist, "ContractID: %s, TokenType: %s", k.getContractID(ctx), tokenTypeID) + } + tokenType := k.mustDecodeTokenType(bz) + return tokenType, nil +} + +func (k Keeper) GetTokenTypes(ctx sdk.Context) (tokenTypes types.TokenTypes, err error) { + _, err = k.GetCollection(ctx) + if err != nil { + return nil, err + } + k.iterateTokenTypes(ctx, "", false, func(t types.TokenType) bool { + tokenTypes = append(tokenTypes, t) + return false + }) + return tokenTypes, nil +} + +func (k Keeper) HasTokenType(ctx sdk.Context, tokenType string) bool { + store := ctx.KVStore(k.storeKey) + tokenTypeKey := types.TokenTypeKey(k.getContractID(ctx), tokenType) + return store.Has(tokenTypeKey) +} + +func (k Keeper) GetNextTokenType(ctx sdk.Context) (tokenType string, err error) { + if !k.ExistCollection(ctx) { + return "", sdkerrors.Wrapf(types.ErrCollectionNotExist, "ContractID: %s", k.getContractID(ctx)) + } + tokenType, err = k.getNextTokenTypeNFT(ctx) + if err != nil { + return "", err + } + if tokenType[0] == types.FungibleFlag[0] { + return "", sdkerrors.Wrapf(types.ErrTokenTypeFull, "ContractID: %s", k.getContractID(ctx)) + } + return tokenType, nil +} + +func (k Keeper) iterateTokenTypes(ctx sdk.Context, prefix string, reverse bool, process func(types.TokenType) (stop bool)) { + store := ctx.KVStore(k.storeKey) + var iter sdk.Iterator + if reverse { + iter = sdk.KVStoreReversePrefixIterator(store, types.TokenTypeKey(k.getContractID(ctx), prefix)) + } else { + iter = sdk.KVStorePrefixIterator(store, types.TokenTypeKey(k.getContractID(ctx), prefix)) + } + defer iter.Close() + for { + if !iter.Valid() { + return + } + val := iter.Value() + tokenType := k.mustDecodeTokenType(val) + if process(tokenType) { + return + } + iter.Next() + } +} + +func (k Keeper) mustDecodeTokenType(bz []byte) (tokenType types.TokenType) { + err := k.cdc.UnmarshalBinaryBare(bz, &tokenType) + if err != nil { + panic(err) + } + return tokenType +} diff --git a/x/collection/internal/keeper/token_type_test.go b/x/collection/internal/keeper/token_type_test.go new file mode 100644 index 0000000000..d07c214fc2 --- /dev/null +++ b/x/collection/internal/keeper/token_type_test.go @@ -0,0 +1,117 @@ +package keeper + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper_GetTokenType(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Token Type") + expected := types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta) + { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.TokenTypeKey(defaultContractID, defaultTokenType), keeper.cdc.MustMarshalBinaryBare(expected)) + } + t.Log("Get Token Type") + { + actual, err := keeper.GetTokenType(ctx, defaultTokenType) + require.NoError(t, err) + verifyTokenTypeFunc(t, expected, actual) + } +} + +func TestKeeper_SetTokenType(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare collection") + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI), addr1)) + t.Log("Set Token Type") + expected := types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta) + { + require.NoError(t, keeper.SetTokenType(ctx, expected)) + } + t.Log("Compare Token Type") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.TokenTypeKey(defaultContractID, defaultTokenType)) + actual := keeper.mustDecodeTokenType(bz) + verifyTokenTypeFunc(t, expected, actual) + } +} + +func TestKeeper_HasTokenType(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Token Type") + expected := types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta) + { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.TokenTypeKey(defaultContractID, defaultTokenType), keeper.cdc.MustMarshalBinaryBare(expected)) + } + t.Log("Get Token Type") + { + require.True(t, keeper.HasTokenType(ctx, defaultTokenType)) + } +} + +func TestKeeper_UpdateTokenType(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare collection") + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI), addr1)) + t.Log("Set Token Type") + expected := types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta) + { + require.NoError(t, keeper.SetTokenType(ctx, expected)) + } + t.Log("Update Token Type") + { + expected.SetName("modifiedname") + require.NoError(t, keeper.UpdateTokenType(ctx, expected)) + } + + t.Log("Get Token Type") + { + actual, err := keeper.GetTokenType(ctx, defaultTokenType) + require.NoError(t, err) + verifyTokenTypeFunc(t, expected, actual) + } +} + +func TestKeeper_GetNextTokenType(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare collection") + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI), addr1)) + t.Log("Get Next Token Type") + { + tokenType, err := keeper.GetNextTokenType(ctx) + require.NoError(t, err) + require.Equal(t, defaultTokenType, tokenType) + } + t.Log("Set Token Type") + { + require.NoError(t, keeper.SetTokenType(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta))) + require.NoError(t, keeper.SetTokenType(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType2, defaultName, defaultMeta))) + require.NoError(t, keeper.SetTokenType(ctx, types.NewBaseTokenType(defaultContractID, defaultTokenType3, defaultName, defaultMeta))) + } + t.Log("Get TokenTypes") + { + tokenTypes, err := keeper.GetTokenTypes(ctx) + require.NoError(t, err) + require.Equal(t, tokenTypes[0].GetTokenType(), defaultTokenType) + require.Equal(t, tokenTypes[1].GetTokenType(), defaultTokenType2) + require.Equal(t, tokenTypes[2].GetTokenType(), defaultTokenType3) + } + t.Log("Get Next Token Type") + { + tokenType, err := keeper.GetNextTokenType(ctx) + require.NoError(t, err) + require.Equal(t, defaultTokenType4, tokenType) + } + t.Log("Set Full") + { + keeper.setNextTokenTypeNFT(ctx, "ffffffff") + _, err := keeper.getNextTokenTypeNFT(ctx) + require.Error(t, err) + } +} diff --git a/x/collection/internal/keeper/transfer.go b/x/collection/internal/keeper/transfer.go new file mode 100644 index 0000000000..7efc63a6c4 --- /dev/null +++ b/x/collection/internal/keeper/transfer.go @@ -0,0 +1,179 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" +) + +type TransferKeeper interface { + TransferFT(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, amount ...types.Coin) error + TransferNFT(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, tokenID ...string) error + TransferFTFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, to sdk.AccAddress, amount ...types.Coin) error + TransferNFTFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, to sdk.AccAddress, tokenID ...string) error +} + +var _ TransferKeeper = (*Keeper)(nil) + +func (k Keeper) TransferFT(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, amount ...types.Coin) error { + if err := k.transferFT(ctx, from, to, amount); err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransferFT, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + sdk.NewAttribute(types.AttributeKeyAmount, types.NewCoins(amount...).String()), + ), + }) + + return nil +} + +func (k Keeper) transferFT(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, amount types.Coins) error { + return k.SendCoins(ctx, from, to, amount) +} + +func (k Keeper) TransferNFT(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, tokenIDs ...string) error { + for _, tokenID := range tokenIDs { + if err := k.transferNFT(ctx, from, to, tokenID); err != nil { + return err + } + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransferNFT, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + ), + }) + for _, tokenID := range tokenIDs { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransferNFT, + sdk.NewAttribute(types.AttributeKeyTokenID, tokenID), + ), + }) + } + + return nil +} + +func (k Keeper) transferNFT(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, tokenID string) error { + store := ctx.KVStore(k.storeKey) + + token, err := k.GetToken(ctx, tokenID) + if err != nil { + return err + } + + nft, ok := token.(types.NFT) + if !ok { + return sdkerrors.Wrapf(types.ErrTokenNotNFT, "TokenID: %s", token.GetTokenID()) + } + childToParentKey := types.TokenChildToParentKey(k.getContractID(ctx), nft.GetTokenID()) + if store.Has(childToParentKey) { + return sdkerrors.Wrapf(types.ErrTokenCannotTransferChildToken, "TokenID: %s", token.GetTokenID()) + } + if !from.Equals(nft.GetOwner()) { + return sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", token.GetTokenID(), from.String()) + } + if !from.Equals(to) { + if err := k.moveNFToken(ctx, from, to, nft); err != nil { + return err + } + } + + return nil +} + +func (k Keeper) TransferFTFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, to sdk.AccAddress, amount ...types.Coin) error { + if !k.IsApproved(ctx, proxy, from) { + return sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), from.String(), k.getContractID(ctx)) + } + + if err := k.transferFT(ctx, from, to, amount); err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransferFTFrom, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + sdk.NewAttribute(types.AttributeKeyAmount, types.NewCoins(amount...).String()), + ), + }) + + return nil +} + +// nolint:dupl +func (k Keeper) TransferNFTFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, to sdk.AccAddress, tokenIDs ...string) error { + if !k.IsApproved(ctx, proxy, from) { + return sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), from.String(), k.getContractID(ctx)) + } + + for _, tokenID := range tokenIDs { + if err := k.transferNFT(ctx, from, to, tokenID); err != nil { + return err + } + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransferNFTFrom, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + ), + }) + for _, tokenID := range tokenIDs { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransferNFTFrom, + sdk.NewAttribute(types.AttributeKeyTokenID, tokenID), + ), + }) + } + + return nil +} + +func (k Keeper) moveNFToken(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, token types.NFT) error { + if from.Equals(to) { + return nil + } + children, err := k.ChildrenOf(ctx, token.GetTokenID()) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeOperationTransferNFT, + sdk.NewAttribute(types.AttributeKeyTokenID, token.GetTokenID()), + ), + }) + + for _, child := range children { + err := k.moveNFToken(ctx, from, to, child.(types.NFT)) + if err != nil { + return err + } + } + + if err := k.ChangeNFTOwner(ctx, from, to, token.GetTokenID()); err != nil { + return err + } + token.SetOwner(to) + return k.UpdateToken(ctx, token) +} diff --git a/x/collection/internal/keeper/transfer_test.go b/x/collection/internal/keeper/transfer_test.go new file mode 100644 index 0000000000..cad3e9d9f9 --- /dev/null +++ b/x/collection/internal/keeper/transfer_test.go @@ -0,0 +1,132 @@ +package keeper + +import ( + "context" + "testing" + + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func TestKeeper_TransferFT(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.TransferFT(ctx2, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(10))), sdkerrors.Wrap(types.ErrInsufficientToken, "insufficient account funds[abcd1234]; account has no coin").Error()) + require.EqualError(t, keeper.TransferFT(ctx, addr2, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(10))), sdkerrors.Wrap(types.ErrInsufficientToken, "insufficient account funds[abcdef01]; account has no coin").Error()) + require.EqualError(t, keeper.TransferFT(ctx, addr2, addr1, types.Coin{Denom: defaultTokenIDFT, Amount: sdk.NewInt(-1)}), sdkerrors.Wrap(types.ErrInvalidCoin, "send amount must be positive").Error()) + + require.NoError(t, keeper.TransferFT(ctx, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(10)))) + require.NoError(t, keeper.TransferFT(ctx, addr1, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(10)))) + require.NoError(t, keeper.TransferFT(ctx, addr1, addr3, types.NewCoin(defaultTokenIDFT, sdk.NewInt(10)))) +} + +func TestKeeper_TransferFTFrom(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.EqualError(t, keeper.TransferFTFrom(ctx, addr1, addr2, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(10))), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr1.String(), addr2.String(), defaultContractID).Error()) + + prepareProxy(ctx, t) + require.NoError(t, keeper.TransferFTFrom(ctx, addr1, addr2, addr1, types.NewCoin(defaultTokenIDFT, sdk.NewInt(10)))) +} + +func TestKeeper_TransferNFT(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, wrongContractID)) + require.EqualError(t, keeper.TransferNFT(ctx2, addr1, addr2, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", wrongContractID, defaultTokenID1).Error()) + require.EqualError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID6), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID6).Error()) + require.EqualError(t, keeper.TransferNFT(ctx, addr2, addr1, defaultTokenID1), sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", defaultTokenID1, addr2.String()).Error()) + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr1, defaultTokenID1)) + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID1)) +} + +func TestKeeper_TransferNFTFrom(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.EqualError(t, keeper.TransferNFTFrom(ctx, addr1, addr2, addr1, defaultTokenID1), sdkerrors.Wrapf(types.ErrCollectionNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr1.String(), addr2.String(), defaultContractID).Error()) + prepareProxy(ctx, t) + require.NoError(t, keeper.TransferNFTFrom(ctx, addr1, addr2, addr1, defaultTokenID1)) +} + +func TestTransferFTScenario(t *testing.T) { + ctx := cacheKeeper() + + // issue idf token + require.NoError(t, keeper.CreateCollection(ctx, types.NewCollection(defaultContractID, defaultName, defaultMeta, defaultImgURI), addr1)) + require.NoError(t, keeper.IssueFT(ctx, addr1, addr1, types.NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true), sdk.NewInt(defaultAmount))) + + // + // transfer success cases + // + require.NoError(t, keeper.TransferFT(ctx, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount/2)))) + + // + // transfer failure cases + // + // Insufficient coins + require.EqualError(t, keeper.TransferFT(ctx, addr1, addr2, types.NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))), sdkerrors.Wrap(types.ErrInsufficientToken, "insufficient account funds[abcdef01]; 500:0000000100000000 < 1000:0000000100000000").Error()) +} + +func TestTransferNFTScenario(t *testing.T) { + ctx := cacheKeeper() + prepareCollectionTokens(ctx, t) + + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID2)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID2, defaultTokenID3)) + require.NoError(t, keeper.Attach(ctx, addr1, defaultTokenID1, defaultTokenID4)) + + // + // transfer failure cases + // + + // transfer non-exist token : failure + require.EqualError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID8), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s, TokenID: %s", defaultContractID, defaultTokenID8).Error()) + + // transfer a child : failure + require.EqualError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID2), sdkerrors.Wrapf(types.ErrTokenCannotTransferChildToken, "TokenID: %s", defaultTokenID2).Error()) + require.EqualError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID3), sdkerrors.Wrapf(types.ErrTokenCannotTransferChildToken, "TokenID: %s", defaultTokenID3).Error()) + require.EqualError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID4), sdkerrors.Wrapf(types.ErrTokenCannotTransferChildToken, "TokenID: %s", defaultTokenID4).Error()) + + // transfer non-mine : failure + require.EqualError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID5), sdkerrors.Wrapf(types.ErrTokenNotOwnedBy, "TokenID: %s, Owner: %s", defaultTokenID5, addr1.String()).Error()) + + // transfer-cnft cft : failure + require.EqualError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenIDFT), sdkerrors.Wrapf(types.ErrTokenNotNFT, "TokenID: %s", defaultTokenIDFT).Error()) + + // + // transfer success cases + // + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID1)) + require.NoError(t, keeper.TransferNFT(ctx, addr2, addr1, defaultTokenID1)) + require.NoError(t, keeper.TransferNFT(ctx, addr1, addr2, defaultTokenID1)) + + // verify the owner of transferred tokens + // owner of token1 is addr2 + token1, err1 := keeper.GetToken(ctx, defaultTokenID1) + require.NoError(t, err1) + require.Equal(t, token1.(types.NFT).GetOwner(), addr2) + + // owner of token2 is addr2 + token2, err2 := keeper.GetToken(ctx, defaultTokenID2) + require.NoError(t, err2) + require.Equal(t, token2.(types.NFT).GetOwner(), addr2) + + // owner of token3 is addr2 + token3, err3 := keeper.GetToken(ctx, defaultTokenID3) + require.NoError(t, err3) + require.Equal(t, token3.(types.NFT).GetOwner(), addr2) + + // owner of token4 is addr2 + token4, err4 := keeper.GetToken(ctx, defaultTokenID4) + require.NoError(t, err4) + require.Equal(t, token4.(types.NFT).GetOwner(), addr2) +} diff --git a/x/collection/internal/legacy/upgrade.go b/x/collection/internal/legacy/upgrade.go new file mode 100644 index 0000000000..177ded52c9 --- /dev/null +++ b/x/collection/internal/legacy/upgrade.go @@ -0,0 +1,13 @@ +package legacy + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/upgrade" +) + +func UpgradeHandler(version string) upgrade.UpgradeHandler { + // XXX: return handler for the migration version + return func(ctx sdk.Context, plan upgrade.Plan) { + + } +} diff --git a/x/collection/internal/querier/querier.go b/x/collection/internal/querier/querier.go new file mode 100644 index 0000000000..74cdc0820c --- /dev/null +++ b/x/collection/internal/querier/querier.go @@ -0,0 +1,382 @@ +package querier + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + abci "github.com/tendermint/tendermint/abci/types" +) + +// creates a querier for token REST endpoints +func NewQuerier(keeper keeper.Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) { + if len(path) >= 2 { + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, path[1])) + } + switch path[0] { + case types.QueryBalance: + return queryBalance(ctx, req, keeper) + case types.QueryBalances: + return queryBalances(ctx, req, keeper) + case types.QueryPerms: + return queryAccountPermission(ctx, req, keeper) + case types.QueryTokens: + return queryTokens(ctx, req, keeper) + case types.QueryTokensWithTokenType: + return queryTokensWithTokenType(ctx, req, keeper) + case types.QueryTokenTypes: + return queryTokenTypes(ctx, req, keeper) + case types.QueryCollections: + return queryCollections(ctx, req, keeper) + case types.QueryNFTCount: + return queryNFTCount(ctx, req, keeper, types.QueryNFTCount) + case types.QueryNFTMint: + return queryNFTCount(ctx, req, keeper, types.QueryNFTMint) + case types.QueryNFTBurn: + return queryNFTCount(ctx, req, keeper, types.QueryNFTBurn) + case types.QuerySupply: + return queryTotal(ctx, req, keeper, types.QuerySupply) + case types.QueryMint: + return queryTotal(ctx, req, keeper, types.QueryMint) + case types.QueryBurn: + return queryTotal(ctx, req, keeper, types.QueryBurn) + case types.QueryParent: + return queryParent(ctx, req, keeper) + case types.QueryRoot: + return queryRoot(ctx, req, keeper) + case types.QueryChildren: + return queryChildren(ctx, req, keeper) + case types.QueryApprovers: + return queryApprovers(ctx, req, keeper) + case types.QueryIsApproved: + return queryIsApproved(ctx, req, keeper) + default: + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown collection query endpoint") + } + } +} + +func queryBalance(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryTokenIDAccAddressParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + if !keeper.HasContractID(ctx) { + return nil, sdkerrors.Wrap(types.ErrCollectionNotExist, ctx.Context().Value(contract.CtxKey{}).(string)) + } + + if !keeper.HasToken(ctx, params.TokenID) { + return nil, sdkerrors.Wrapf(types.ErrTokenNotExist, "%s %s", ctx.Context().Value(contract.CtxKey{}).(string), params.TokenID) + } + + var balance sdk.Int + if params.TokenID[0] == types.FungibleFlag[0] { + var err error + balance, err = keeper.GetBalance(ctx, params.TokenID, params.Addr) + if err != nil { + if _, err2 := keeper.GetAccount(ctx, params.Addr); err2 != nil { + balance = sdk.ZeroInt() + } else { + return nil, err + } + } + } else { + if keeper.HasNFTOwner(ctx, params.Addr, params.TokenID) { + balance = sdk.NewInt(1) + } else { + balance = sdk.NewInt(0) + } + } + + bz, err2 := keeper.MarshalJSONIndent(balance) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryBalances(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryTokenIDAccAddressParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + coins := make([]types.Coin, 0) + if !keeper.HasContractID(ctx) { + return nil, sdkerrors.Wrap(types.ErrCollectionNotExist, ctx.Context().Value(contract.CtxKey{}).(string)) + } + // FT + acc, err := keeper.GetAccount(ctx, params.Addr) + if err == nil { + coins = acc.GetCoins() + } + + // NFT + tokenIds := keeper.GetNFTsOwner(ctx, params.Addr) + for _, tokenID := range tokenIds { + var coin types.Coin + coin.Amount = sdk.NewInt(1) + coin.Denom = tokenID + coins = append(coins, coin) + } + + bz, err2 := keeper.MarshalJSONIndent(coins) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +// nolint:dupl +func queryTokenTypes(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryTokenIDParams + if len(req.Data) != 0 { + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + } + if len(params.TokenID) == 0 { + tokenTypes, err := keeper.GetTokenTypes(ctx) + if err != nil { + return nil, err + } + bz, err2 := keeper.MarshalJSONIndent(tokenTypes) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + return bz, nil + } + + tokenType, err := keeper.GetTokenType(ctx, params.TokenID) + if err != nil { + return nil, err + } + + bz, err2 := keeper.MarshalJSONIndent(tokenType) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +// nolint:dupl +func queryTokens(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryTokenIDParams + if len(req.Data) != 0 { + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + } + if len(params.TokenID) == 0 { + tokens, err := keeper.GetTokens(ctx) + if err != nil { + return nil, err + } + bz, err2 := keeper.MarshalJSONIndent(tokens) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + return bz, nil + } + + token, err := keeper.GetToken(ctx, params.TokenID) + if err != nil { + return nil, err + } + + bz, err2 := keeper.MarshalJSONIndent(token) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryTokensWithTokenType(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryTokenTypeParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + tokens, err := keeper.GetNFTs(ctx, params.TokenType) + if err != nil { + return nil, err + } + bz, err2 := keeper.MarshalJSONIndent(tokens) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + return bz, nil +} + +func queryAccountPermission(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + if len(req.Data) == 0 { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "data is nil") + } + var params types.QueryAccAddressParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + pms := keeper.GetPermissions(ctx, params.Addr) + + bz, err := keeper.MarshalJSONIndent(pms) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil +} + +func queryCollections(ctx sdk.Context, _ abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + collection, err := keeper.GetCollection(ctx) + if err != nil { + return nil, err + } + bz, err2 := keeper.MarshalJSONIndent(collection) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryNFTCount(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper, target string) ([]byte, error) { + var params types.QueryTokenIDParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + count, err := keeper.GetNFTCountInt(ctx, params.TokenID, target) + if err != nil { + return nil, err + } + bz, err2 := keeper.MarshalJSONIndent(count) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryParent(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryTokenIDParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + token, err := keeper.ParentOf(ctx, params.TokenID) + if err != nil { + return nil, err + } + if token == nil { + return nil, nil + } + + bz, err2 := keeper.MarshalJSONIndent(token) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryRoot(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryTokenIDParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + token, err := keeper.RootOf(ctx, params.TokenID) + if err != nil { + return nil, err + } + + bz, err2 := keeper.MarshalJSONIndent(token) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryChildren(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryTokenIDParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + tokens, err := keeper.ChildrenOf(ctx, params.TokenID) + if err != nil { + return nil, err + } + if tokens == nil { + return nil, nil + } + + bz, err2 := keeper.MarshalJSONIndent(tokens) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryApprovers(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryProxyParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + approvers, err := keeper.GetApprovers(ctx, params.Proxy) + if err != nil { + return nil, err + } + + bz, err2 := keeper.MarshalJSONIndent(approvers) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryIsApproved(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryIsApprovedParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + approved := keeper.IsApproved(ctx, params.Proxy, params.Approver) + + bz, err := keeper.MarshalJSONIndent(approved) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil +} + +func queryTotal(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper, target string) ([]byte, error) { + var params types.QueryTokenIDParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + supply, err := keeper.GetTotalInt(ctx, params.TokenID, target) + if err != nil { + return nil, err + } + + bz, err2 := keeper.MarshalJSONIndent(supply) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} diff --git a/x/collection/internal/querier/querier_encoder.go b/x/collection/internal/querier/querier_encoder.go new file mode 100644 index 0000000000..82420c2980 --- /dev/null +++ b/x/collection/internal/querier/querier_encoder.go @@ -0,0 +1,247 @@ +package querier + +import ( + "encoding/json" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/line/lbm-sdk/v2/x/wasm" +) + +func NewQueryEncoder(collectionQuerier sdk.Querier) wasm.EncodeQuerier { + return func(ctx sdk.Context, jsonQuerier json.RawMessage) ([]byte, error) { + var customQuerier types.WasmCustomQuerier + err := json.Unmarshal(jsonQuerier, &customQuerier) + if err != nil { + return nil, err + } + switch customQuerier.Route { + case types.QueryCollections: + return handleQueryCollections(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryBalance: + return handleQueryBalances(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryBalances: + return handleQueryBalances(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryTokenTypes: + return handleQueryTokenTypes(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryTokens: + return handleQueryTokens(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryTokensWithTokenType: + return handleQueryTokensWithTokenType(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryNFTCount: + return handleQueryNFTCount(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryNFTMint: + return handleQueryNFTCount(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryNFTBurn: + return handleQueryNFTCount(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QuerySupply: + return handleQueryTotal(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryMint: + return handleQueryTotal(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryBurn: + return handleQueryTotal(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryParent: + return handleQueryRootOrParentOrChildren(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryRoot: + return handleQueryRootOrParentOrChildren(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryChildren: + return handleQueryRootOrParentOrChildren(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryPerms: + return handleQueryPerms(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryApprovers: + return handleQueryApprovers(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryIsApproved: + return handleQueryApproved(ctx, collectionQuerier, []string{customQuerier.Route}, customQuerier.Data) + default: + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized Msg route: %T", customQuerier.Route) + } + } +} + +func handleQueryCollections(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryCollectionWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + req := makeRequestQuery(nil) + + contractID := wrapper.CollectionParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func handleQueryBalances(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryBalanceWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + param := types.NewQueryTokenIDAccAddressParams(wrapper.BalanceParam.TokenID, wrapper.BalanceParam.Addr) + req := makeRequestQuery(param) + + contractID := wrapper.BalanceParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func handleQueryTokenTypes(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryTokenTypesWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + param := types.NewQueryTokenIDParams(wrapper.TokenTypesParam.TokenID) + req := makeRequestQuery(param) + + contractID := wrapper.TokenTypesParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func handleQueryTokens(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryTokensWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + param := types.NewQueryTokenIDParams(wrapper.TokensParam.TokenID) + req := makeRequestQuery(param) + + contractID := wrapper.TokensParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func handleQueryTokensWithTokenType(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryTokenTypeWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + param := types.NewQueryTokenTypeParams(wrapper.TokenTypeParam.TokenType) + req := makeRequestQuery(param) + + contractID := wrapper.TokenTypeParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func handleQueryNFTCount(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryNFTCountWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + param := types.NewQueryTokenIDParams(wrapper.TokensParam.TokenID) + req := makeRequestQuery(param) + + contractID := wrapper.TokensParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func handleQueryTotal(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryTotalWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + params := types.NewQueryTokenIDParams(wrapper.TotalParam.TokenID) + req := makeRequestQuery(params) + + contractID := wrapper.TotalParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + + return collectionQuerier(ctx, path, req) +} + +func handleQueryRootOrParentOrChildren(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryTokensWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + param := types.NewQueryTokenIDParams(wrapper.TokensParam.TokenID) + req := makeRequestQuery(param) + + contractID := wrapper.TokensParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func handleQueryPerms(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryPermsWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + param := types.NewQueryAccAddressParams(wrapper.PermParam.Address) + req := makeRequestQuery(param) + + contractID := wrapper.PermParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func handleQueryApproved(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryApprovedWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + param := types.NewQueryIsApprovedParams(wrapper.IsApprovedParam.Proxy, wrapper.IsApprovedParam.Approver) + req := makeRequestQuery(param) + + contractID := wrapper.IsApprovedParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func handleQueryApprovers(ctx sdk.Context, collectionQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryApproversWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + param := types.NewQueryApproverParams(wrapper.ApproversParam.Proxy) + req := makeRequestQuery(param) + + contractID := wrapper.ApproversParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return collectionQuerier(ctx, path, req) +} + +func makeRequestQuery(params interface{}) abci.RequestQuery { + req := abci.RequestQuery{ + Path: "", + Data: []byte(string(codec.MustMarshalJSONIndent(types.ModuleCdc, params))), + } + return req +} diff --git a/x/collection/internal/querier/querier_encoder_test.go b/x/collection/internal/querier/querier_encoder_test.go new file mode 100644 index 0000000000..d77897a572 --- /dev/null +++ b/x/collection/internal/querier/querier_encoder_test.go @@ -0,0 +1,442 @@ +package querier + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/wasm" + "github.com/stretchr/testify/require" +) + +var ( + collectionQueryEncoder wasm.EncodeQuerier +) + +func setupQueryEncoder() { + collectionQuerier := NewQuerier(ckeeper) + + collectionQueryEncoder = NewQueryEncoder(collectionQuerier) +} + +func encodeQuery(t *testing.T, jsonQuerier json.RawMessage, result interface{}) error { + res, err := collectionQueryEncoder(ctx, jsonQuerier) + if len(res) > 0 { + require.NoError(t, ckeeper.UnmarshalJSON(res, result)) + } + return err +} + +func TestNewQuerier_encodeQueryBalance(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"balance","data":{"balance_param":{"contract_id":"%s", "token_id":"%s", "addr":"%s"}}}`, contractID, tokenFTID, addr1) + + var balance sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &balance) + require.NoError(t, err) + require.True(t, balance.Equal(sdk.NewInt(1000))) +} + +func TestNewQuerier_encodeQueryBalances(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"balances","data":{"balance_param":{"contract_id":"%s", "token_id":"%s", "addr":"%s"}}}`, contractID, "", addr1) + + var coins types.Coins + err := encodeQuery(t, json.RawMessage(jsonQuerier), &coins) + require.NoError(t, err) + require.Equal(t, tokenFTID, coins[0].Denom) + require.Equal(t, sdk.NewInt(tokenFTSupply), coins[0].Amount) + require.Equal(t, tokenNFTID1, coins[1].Denom) + require.Equal(t, sdk.NewInt(1), coins[1].Amount) + require.Equal(t, tokenNFTID2, coins[2].Denom) + require.Equal(t, sdk.NewInt(1), coins[2].Amount) + require.Equal(t, tokenNFTID3, coins[3].Denom) + require.Equal(t, sdk.NewInt(1), coins[3].Amount) + paramsNoExist1 := types.QueryAccAddressParams{ + Addr: addr3, + } + var coinsEmpty types.Coins + query(t, paramsNoExist1, types.QueryBalances, &coinsEmpty) + require.Empty(t, coinsEmpty) +} + +func TestNewQuerier_encodeQueryBalanceNonExistentAccount(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"balance","data":{"balance_param":{"contract_id":"%s", "token_id":"%s", "addr":"%s"}}}`, contractID, tokenFTID, addr3) + + var balance sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &balance) + require.NoError(t, err) + require.True(t, balance.Equal(sdk.NewInt(0))) +} + +func TestNewQuerier_encodeQueryBalanceNonExistentContractID(t *testing.T) { + prepare(t) + setupQueryEncoder() + contractID := "12345678" + jsonQuerier := fmt.Sprintf(`{"route":"balance","data":{"balance_param":{"contract_id":"%s", "token_id":"%s", "addr":"%s"}}}`, contractID, tokenFTID, addr1) + + var balance sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &balance) + require.Error(t, err, sdkerrors.Wrap(types.ErrCollectionNotExist, contractID)) +} + +func TestNewQuerier_encodeQueryBalanceNonExistentTokenID(t *testing.T) { + prepare(t) + + tokenID := "00000009" + tokenFTIndex + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"balance","data":{"balance_param":{"contract_id":"%s", "token_id":"%s", "addr":"%s"}}}`, contractID, tokenID, addr1) + + var balance sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &balance) + require.Error(t, err, sdkerrors.Wrapf(types.ErrCollectionNotExist, "%s %s", contractID, tokenID)) +} + +func TestNewQuerier_encodeQueryAccountPermission(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"perms","data":{"perm_param":{"contract_id":"%s", "address":"%s"}}}`, contractID, addr1) + + var permissions types.Permissions + err := encodeQuery(t, json.RawMessage(jsonQuerier), &permissions) + require.NoError(t, err) + + require.Equal(t, len(permissions), 4) + require.Equal(t, permissions[0].String(), "issue") + require.Equal(t, permissions[1].String(), "mint") + require.Equal(t, permissions[2].String(), "burn") + require.Equal(t, permissions[3].String(), "modify") +} + +func TestNewQuerier_encodeQueryTokens_FT(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"tokens","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenFTID) + + var token types.Token + err := encodeQuery(t, json.RawMessage(jsonQuerier), &token) + require.NoError(t, err) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenFTID) + require.Equal(t, token.GetName(), tokenFTName) + require.Equal(t, token.GetTokenType(), tokenFTType) + require.Equal(t, token.GetTokenIndex(), tokenFTIndex) +} + +func TestNewQuerier_encodeQueryTokens_NFT(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"tokens","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTID1) + + var token types.Token + err := encodeQuery(t, json.RawMessage(jsonQuerier), &token) + require.NoError(t, err) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenNFTID1) + require.Equal(t, token.GetName(), tokenNFTName1) + require.Equal(t, token.GetTokenType(), tokenNFTType) + require.Equal(t, token.GetTokenIndex(), tokenNFTIndex1) +} + +func TestNewQuerier_encodeQueryTokens_all(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"tokens","data":{"tokens_param":{"contract_id":"%s", "token_id":""}}}`, contractID) + + var tokens types.Tokens + err := encodeQuery(t, json.RawMessage(jsonQuerier), &tokens) + require.NoError(t, err) + require.Equal(t, len(tokens), 4) + require.Equal(t, tokens[0].GetContractID(), contractID) + require.Equal(t, tokens[0].GetTokenID(), tokenFTID) + require.Equal(t, tokens[0].GetName(), tokenFTName) + require.Equal(t, tokens[0].GetTokenType(), tokenFTType) + require.Equal(t, tokens[0].GetTokenIndex(), tokenFTIndex) + require.Equal(t, tokens[1].GetContractID(), contractID) + require.Equal(t, tokens[1].GetTokenID(), tokenNFTID1) + require.Equal(t, tokens[1].GetName(), tokenNFTName1) + require.Equal(t, tokens[1].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[1].GetTokenIndex(), tokenNFTIndex1) + require.Equal(t, tokens[2].GetContractID(), contractID) + require.Equal(t, tokens[2].GetTokenID(), tokenNFTID2) + require.Equal(t, tokens[2].GetName(), tokenNFTName2) + require.Equal(t, tokens[2].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[2].GetTokenIndex(), tokenNFTIndex2) + require.Equal(t, tokens[3].GetContractID(), contractID) + require.Equal(t, tokens[3].GetTokenID(), tokenNFTID3) + require.Equal(t, tokens[3].GetName(), tokenNFTName3) + require.Equal(t, tokens[3].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[3].GetTokenIndex(), tokenNFTIndex3) +} + +func TestNewQuerier_encodeQueryTokenTypes_one(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"tokentypes","data":{"tokentypes_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTType) + + var tokenType types.TokenType + err := encodeQuery(t, json.RawMessage(jsonQuerier), &tokenType) + require.NoError(t, err) + require.Equal(t, tokenType.GetContractID(), contractID) + require.Equal(t, tokenType.GetTokenType(), tokenNFTType) + require.Equal(t, tokenType.GetName(), tokenNFTTypeName) +} + +func TestNewQuerier_encodeQueryTokenTypes_all(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"tokentypes","data":{"tokentypes_param":{"contract_id":"%s", "token_id":""}}}`, contractID) + + var tokenTypes types.TokenTypes + err := encodeQuery(t, json.RawMessage(jsonQuerier), &tokenTypes) + require.NoError(t, err) + require.Equal(t, len(tokenTypes), 1) + require.Equal(t, tokenTypes[0].GetContractID(), contractID) + require.Equal(t, tokenTypes[0].GetTokenType(), tokenNFTType) + require.Equal(t, tokenTypes[0].GetName(), tokenNFTTypeName) +} + +func TestNewQuerier_encodeQueryTokensWithTokenType(t *testing.T) { + prepare(t) + setupQueryEncoder() + + jsonQuerier := fmt.Sprintf(`{"route":"tokensWithTokenType","data":{"token_type_param":{"contract_id":"%s", "token_type":"%s"}}}`, contractID, tokenNFTType) + var tokens types.Tokens + err := encodeQuery(t, json.RawMessage(jsonQuerier), &tokens) + require.NoError(t, err) + require.Equal(t, len(tokens), 3) + require.Equal(t, tokens[0].GetContractID(), contractID) + require.Equal(t, tokens[0].GetName(), tokenNFTName1) + require.Equal(t, tokens[0].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[1].GetContractID(), contractID) + require.Equal(t, tokens[1].GetName(), tokenNFTName2) + require.Equal(t, tokens[1].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[2].GetContractID(), contractID) + require.Equal(t, tokens[2].GetName(), tokenNFTName3) + require.Equal(t, tokens[2].GetTokenType(), tokenNFTType) + + var tokens2 types.Tokens + jsonQuerier = fmt.Sprintf(`{"route":"tokensWithTokenType","data":{"token_type_param":{"contract_id":"%s", "token_type":"%s"}}}`, contractID, tokenFTType) + err = encodeQuery(t, json.RawMessage(jsonQuerier), &tokens2) + require.NoError(t, err) + require.Equal(t, len(tokens2), 1) + require.Equal(t, tokens2[0].GetContractID(), contractID) + require.Equal(t, tokens2[0].GetName(), tokenFTName) + require.Equal(t, tokens2[0].GetTokenType(), tokenFTType) + + tokenType := "99999999" + var tokensNoExist types.Tokens + jsonQuerier = fmt.Sprintf(`{"route":"tokensWithTokenType","data":{"token_type_param":{"contract_id":"%s", "token_type":"%s"}}}`, contractID, tokenType) + err = encodeQuery(t, json.RawMessage(jsonQuerier), &tokensNoExist) + require.NoError(t, err) + require.Equal(t, len(tokensNoExist), 0) + require.Empty(t, tokensNoExist) +} + +func TestNewQuerier_encodeQueryCollections_one(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"collections","data":{"collection_param":{"contract_id":"%s"}}}`, contractID) + + var collection types.Collection + err := encodeQuery(t, json.RawMessage(jsonQuerier), &collection) + require.NoError(t, err) + require.Equal(t, collection.GetContractID(), contractID) + require.Equal(t, collection.GetName(), collectionName) + require.Equal(t, collection.GetBaseImgURI(), imageURL) +} + +func TestNewQuerier_encodeQueryNFTCount(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"nftcount","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTType) + + var count sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &count) + require.NoError(t, err) + require.Equal(t, count, sdk.NewInt(3)) +} + +func TestNewQuerier_encodeQueryTotalMint_NFT(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"nftmint","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTType) + + var supply sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &supply) + require.NoError(t, err) + require.Equal(t, int64(3), supply.Int64()) +} + +func TestNewQuerier_encodeQueryTotalBurn_NFT(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"nftburn","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTType) + + var supply sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &supply) + require.NoError(t, err) + require.Equal(t, supply.Int64(), int64(0)) +} + +func TestNewQuerier_encodeQueryTotalSupply_FT(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"%s","data":{"total_param":{"contract_id":"%s", "token_id":"%s"}}}`, types.QuerySupply, contractID, tokenFTID) + + var supply sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &supply) + require.NoError(t, err) + require.Equal(t, supply.Int64(), int64(tokenFTSupply)) +} + +func TestNewQuerier_encodeQueryTotalMint_FT(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"%s","data":{"total_param":{"contract_id":"%s", "token_id":"%s"}}}`, types.QueryMint, contractID, tokenFTID) + + var supply sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &supply) + require.NoError(t, err) + require.Equal(t, supply.Int64(), int64(tokenFTSupply)) +} + +func TestNewQuerier_encodeQueryTotalBurn_FT(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"%s","data":{"total_param":{"contract_id":"%s", "token_id":"%s"}}}`, types.QueryBurn, contractID, tokenFTID) + + var supply sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &supply) + require.NoError(t, err) + require.Equal(t, supply.Int64(), int64(0)) +} + +func TestNewQuerier_encodeQueryParent(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"parent","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTID2) + + var token types.Token + err := encodeQuery(t, json.RawMessage(jsonQuerier), &token) + require.NoError(t, err) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenNFTID1) +} + +func TestNewQuerier_encodeQueryParent_nil(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"parent","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTID1) + + var token types.Token + err := encodeQuery(t, json.RawMessage(jsonQuerier), &token) + require.NoError(t, err) + require.Equal(t, token, nil) +} + +func TestNewQuerier_encodeQueryRoot(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"root","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTID3) + + var token types.Token + err := encodeQuery(t, json.RawMessage(jsonQuerier), &token) + require.NoError(t, err) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenNFTID1) +} + +func TestNewQuerier_encodeQueryRoot_self(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"root","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTID1) + + var token types.Token + err := encodeQuery(t, json.RawMessage(jsonQuerier), &token) + require.NoError(t, err) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenNFTID1) +} + +func TestNewQuerier_encodeQueryChildren(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"children","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTID1) + + var tokens types.Tokens + err := encodeQuery(t, json.RawMessage(jsonQuerier), &tokens) + require.NoError(t, err) + require.Equal(t, len(tokens), 2) + require.Equal(t, tokens[0].GetContractID(), contractID) + require.Equal(t, tokens[0].GetTokenID(), tokenNFTID2) + require.Equal(t, tokens[1].GetContractID(), contractID) + require.Equal(t, tokens[1].GetTokenID(), tokenNFTID3) +} + +func TestNewQuerier_encodeQueryChildren_empty(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"children","data":{"tokens_param":{"contract_id":"%s", "token_id":"%s"}}}`, contractID, tokenNFTID2) + + var tokens types.Tokens + err := encodeQuery(t, json.RawMessage(jsonQuerier), &tokens) + require.NoError(t, err) + require.Equal(t, len(tokens), 0) +} + +func TestNewQuerier_encodeQueryApprovers(t *testing.T) { + prepare(t) + + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"approver","data":{"approvers_param":{"contract_id":"%s", "proxy":"%s"}}}`, contractID, addr1) + + var acAd1 []sdk.AccAddress + err := encodeQuery(t, json.RawMessage(jsonQuerier), &acAd1) + require.NoError(t, err) + require.Equal(t, 2, len(acAd1)) + if bytes.Compare(addr2, addr3) <= 0 { + require.Equal(t, addr2, acAd1[0]) + require.Equal(t, addr3, acAd1[1]) + } else { + require.Equal(t, addr2, acAd1[1]) + require.Equal(t, addr3, acAd1[0]) + } + + var acAdEmpty []sdk.AccAddress + paramsEmpty := types.QueryProxyParams{ + Proxy: addr2, + } + query(t, paramsEmpty, types.QueryApprovers, &acAdEmpty) + require.Empty(t, acAdEmpty) +} + +func TestNewQuerier_encodeQueryIsApproved_true(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"approved","data":{"is_approved_param":{"contract_id":"%s", "proxy":"%s", "approver":"%s"}}}`, contractID, addr1, addr2) + + var approved bool + err := encodeQuery(t, json.RawMessage(jsonQuerier), &approved) + require.NoError(t, err) + require.True(t, approved) +} + +func TestNewQuerier_encodeQueryIsApproved_false(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"approved","data":{"is_approved_param":{"contract_id":"%s", "proxy":"%s", "approver":"%s"}}}`, contractID, addr2, addr1) + + var approved bool + err := encodeQuery(t, json.RawMessage(jsonQuerier), &approved) + require.NoError(t, err) + require.False(t, approved) +} diff --git a/x/collection/internal/querier/querier_test.go b/x/collection/internal/querier/querier_test.go new file mode 100644 index 0000000000..7f09614bed --- /dev/null +++ b/x/collection/internal/querier/querier_test.go @@ -0,0 +1,554 @@ +package querier + +import ( + "bytes" + "context" + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +const ( + contractID = "9be17165" + collectionName = "mycol" + imageURL = "url" + meta = "meta" + tokenFTType = "00000001" + tokenFTIndex = "00000000" + tokenFTID = tokenFTType + tokenFTIndex + tokenFTSupply = 1000 + tokenNFTType = "10000001" + tokenNFTIndex1 = "00000001" + tokenNFTIndex2 = "00000002" + tokenNFTIndex3 = "00000003" + tokenNFTID1 = tokenNFTType + tokenNFTIndex1 + tokenNFTID2 = tokenNFTType + tokenNFTIndex2 /* #nosec */ + tokenNFTID3 = tokenNFTType + tokenNFTIndex3 /* #nosec */ + tokenNFTTypeName = "sword" + tokenFTName = "ft_token" + tokenNFTName1 = "nft_token1" /* #nosec */ + tokenNFTName2 = "nft_token2" /* #nosec */ + tokenNFTName3 = "nft_token3" /* #nosec */ +) + +var ( + ms store.CommitMultiStore + ctx sdk.Context + ckeeper keeper.Keeper + addr1 sdk.AccAddress + addr2 sdk.AccAddress + addr3 sdk.AccAddress +) + +func prepare(t *testing.T) { + ctx, ms, ckeeper = keeper.TestKeeper() + msCache := ms.CacheMultiStore() + ctx = ctx.WithMultiStore(msCache) + + addr1 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr3 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + // prepare contract ID + newContractID := ckeeper.NewContractID(ctx) + require.Equal(t, contractID, newContractID) + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, contractID)) + + // prepare collection + require.NoError(t, ckeeper.CreateCollection(ctx2, types.NewCollection(contractID, collectionName, meta, imageURL), addr1)) + require.NoError(t, ckeeper.IssueFT(ctx2, addr1, addr1, types.NewFT(contractID, tokenFTID, tokenFTName, meta, sdk.NewInt(1), true), sdk.NewInt(tokenFTSupply))) + require.NoError(t, ckeeper.IssueNFT(ctx2, types.NewBaseTokenType(contractID, tokenNFTType, tokenNFTTypeName, meta), addr1)) + require.NoError(t, ckeeper.MintNFT(ctx2, addr1, types.NewNFT(contractID, tokenNFTID1, tokenNFTName1, meta, addr1))) + require.NoError(t, ckeeper.MintNFT(ctx2, addr1, types.NewNFT(contractID, tokenNFTID2, tokenNFTName2, meta, addr1))) + require.NoError(t, ckeeper.MintNFT(ctx2, addr1, types.NewNFT(contractID, tokenNFTID3, tokenNFTName3, meta, addr1))) + + require.NoError(t, ckeeper.Attach(ctx2, addr1, tokenNFTID1, tokenNFTID2)) + require.NoError(t, ckeeper.Attach(ctx2, addr1, tokenNFTID1, tokenNFTID3)) + require.NoError(t, ckeeper.GrantPermission(ctx2, addr1, addr2, types.NewMintPermission())) + require.NoError(t, ckeeper.SetApproved(ctx2, addr1, addr2)) + require.NoError(t, ckeeper.SetApproved(ctx2, addr1, addr3)) +} + +func query(t *testing.T, params interface{}, query string, result interface{}) { + res, err := queryInternal(params, query, contractID) + require.NoError(t, err) + if len(res) > 0 { + require.NoError(t, ckeeper.UnmarshalJSON(res, result)) + } +} + +func queryInternal(params interface{}, query, contractID string) ([]byte, error) { + req := abci.RequestQuery{ + Path: "", + Data: []byte(string(codec.MustMarshalJSONIndent(types.ModuleCdc, params))), + } + if params == nil { + req.Data = nil + } + path := []string{query} + if contractID != "" { + path = append(path, contractID) + } + querier := NewQuerier(ckeeper) + return querier(ctx, path, req) +} + +func TestNewQuerier_queryBalance(t *testing.T) { + prepare(t) + params := types.QueryTokenIDAccAddressParams{ + TokenID: tokenFTID, + Addr: addr1, + } + var balance sdk.Int + query(t, params, types.QueryBalance, &balance) + require.True(t, balance.Equal(sdk.NewInt(1000))) +} + +func TestNewQuerier_queryBalances(t *testing.T) { + prepare(t) + params := types.QueryAccAddressParams{ + Addr: addr1, + } + var coins types.Coins + query(t, params, types.QueryBalances, &coins) + require.Equal(t, tokenFTID, coins[0].Denom) + require.Equal(t, sdk.NewInt(tokenFTSupply), coins[0].Amount) + require.Equal(t, tokenNFTID1, coins[1].Denom) + require.Equal(t, sdk.NewInt(1), coins[1].Amount) + require.Equal(t, tokenNFTID2, coins[2].Denom) + require.Equal(t, sdk.NewInt(1), coins[2].Amount) + require.Equal(t, tokenNFTID3, coins[3].Denom) + require.Equal(t, sdk.NewInt(1), coins[3].Amount) + paramsNoExist1 := types.QueryAccAddressParams{ + Addr: addr3, + } + var coinsEmpty types.Coins + query(t, paramsNoExist1, types.QueryBalances, &coinsEmpty) + require.Empty(t, coinsEmpty) +} + +func TestNewQuerier_queryBalanceOwnedNFT(t *testing.T) { + prepare(t) + params := types.QueryTokenIDAccAddressParams{ + TokenID: tokenNFTID1, + Addr: addr1, + } + var balance sdk.Int + query(t, params, types.QueryBalance, &balance) + require.True(t, balance.Equal(sdk.NewInt(1))) +} + +func TestNewQuerier_queryBalanceNoOwnedNFT(t *testing.T) { + prepare(t) + params := types.QueryTokenIDAccAddressParams{ + TokenID: tokenNFTID1, + Addr: addr2, + } + var balance sdk.Int + query(t, params, types.QueryBalance, &balance) + require.True(t, balance.Equal(sdk.NewInt(0))) +} + +func TestNewQuerier_queryBalanceNonExistentAccount(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDAccAddressParams{ + TokenID: tokenFTID, + Addr: addr3, + } + var balance sdk.Int + query(t, params, types.QueryBalance, &balance) + require.True(t, balance.Equal(sdk.NewInt(0))) +} + +func TestNewQuerier_queryBalanceNonExistentContractID(t *testing.T) { + prepare(t) + + contractID := "12345678" + params := types.QueryTokenIDAccAddressParams{ + TokenID: tokenFTID, + Addr: addr1, + } + _, err := queryInternal(params, types.QueryBalance, contractID) + require.Error(t, err, sdkerrors.Wrap(types.ErrCollectionNotExist, contractID)) +} + +func TestNewQuerier_queryBalanceNonExistentTokenID(t *testing.T) { + prepare(t) + + tokenID := "00000009" + tokenFTIndex + params := types.QueryTokenIDAccAddressParams{ + TokenID: tokenID, + Addr: addr1, + } + _, err := queryInternal(params, types.QueryBalance, contractID) + require.Error(t, err, sdkerrors.Wrapf(types.ErrCollectionNotExist, "%s %s", contractID, tokenID)) +} + +func TestNewQuerier_queryAccountPermission(t *testing.T) { + prepare(t) + + params := types.NewQueryAccAddressParams(addr1) + var permissions types.Permissions + query(t, params, types.QueryPerms, &permissions) + require.Equal(t, len(permissions), 4) + require.Equal(t, permissions[0].String(), "issue") + require.Equal(t, permissions[1].String(), "mint") + require.Equal(t, permissions[2].String(), "burn") + require.Equal(t, permissions[3].String(), "modify") +} + +func TestNewQuerier_queryTokens_FT(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenFTID, + } + var token types.Token + query(t, params, types.QueryTokens, &token) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenFTID) + require.Equal(t, token.GetName(), tokenFTName) + require.Equal(t, token.GetTokenType(), tokenFTType) + require.Equal(t, token.GetTokenIndex(), tokenFTIndex) +} + +func TestNewQuerier_queryTokens_NFT(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTID1, + } + var token types.Token + query(t, params, types.QueryTokens, &token) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenNFTID1) + require.Equal(t, token.GetName(), tokenNFTName1) + require.Equal(t, token.GetTokenType(), tokenNFTType) + require.Equal(t, token.GetTokenIndex(), tokenNFTIndex1) +} + +func TestNewQuerier_queryTokens_all(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: "", + } + var tokens types.Tokens + query(t, params, types.QueryTokens, &tokens) + require.Equal(t, len(tokens), 4) + require.Equal(t, tokens[0].GetContractID(), contractID) + require.Equal(t, tokens[0].GetTokenID(), tokenFTID) + require.Equal(t, tokens[0].GetName(), tokenFTName) + require.Equal(t, tokens[0].GetTokenType(), tokenFTType) + require.Equal(t, tokens[0].GetTokenIndex(), tokenFTIndex) + require.Equal(t, tokens[1].GetContractID(), contractID) + require.Equal(t, tokens[1].GetTokenID(), tokenNFTID1) + require.Equal(t, tokens[1].GetName(), tokenNFTName1) + require.Equal(t, tokens[1].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[1].GetTokenIndex(), tokenNFTIndex1) + require.Equal(t, tokens[2].GetContractID(), contractID) + require.Equal(t, tokens[2].GetTokenID(), tokenNFTID2) + require.Equal(t, tokens[2].GetName(), tokenNFTName2) + require.Equal(t, tokens[2].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[2].GetTokenIndex(), tokenNFTIndex2) + require.Equal(t, tokens[3].GetContractID(), contractID) + require.Equal(t, tokens[3].GetTokenID(), tokenNFTID3) + require.Equal(t, tokens[3].GetName(), tokenNFTName3) + require.Equal(t, tokens[3].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[3].GetTokenIndex(), tokenNFTIndex3) +} + +func TestNewQuerier_queryTokenTypes_one(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTType, + } + var tokenType types.TokenType + query(t, params, types.QueryTokenTypes, &tokenType) + require.Equal(t, tokenType.GetContractID(), contractID) + require.Equal(t, tokenType.GetTokenType(), tokenNFTType) + require.Equal(t, tokenType.GetName(), tokenNFTTypeName) +} + +func TestNewQuerier_queryTokenTypes_all(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: "", + } + var tokenTypes types.TokenTypes + query(t, params, types.QueryTokenTypes, &tokenTypes) + require.Equal(t, len(tokenTypes), 1) + require.Equal(t, tokenTypes[0].GetContractID(), contractID) + require.Equal(t, tokenTypes[0].GetTokenType(), tokenNFTType) + require.Equal(t, tokenTypes[0].GetName(), tokenNFTTypeName) +} + +func TestNewQuerier_queryTokensWithTokenType(t *testing.T) { + prepare(t) + + params := types.QueryTokenTypeParams{ + TokenType: tokenNFTType, + } + var tokens types.Tokens + query(t, params, types.QueryTokensWithTokenType, &tokens) + require.Equal(t, len(tokens), 3) + require.Equal(t, tokens[0].GetContractID(), contractID) + require.Equal(t, tokens[0].GetName(), tokenNFTName1) + require.Equal(t, tokens[0].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[1].GetContractID(), contractID) + require.Equal(t, tokens[1].GetName(), tokenNFTName2) + require.Equal(t, tokens[1].GetTokenType(), tokenNFTType) + require.Equal(t, tokens[2].GetContractID(), contractID) + require.Equal(t, tokens[2].GetName(), tokenNFTName3) + require.Equal(t, tokens[2].GetTokenType(), tokenNFTType) + params2 := types.QueryTokenTypeParams{ + TokenType: tokenFTType, + } + var tokens2 types.Tokens + query(t, params2, types.QueryTokensWithTokenType, &tokens2) + require.Equal(t, len(tokens2), 1) + require.Equal(t, tokens2[0].GetContractID(), contractID) + require.Equal(t, tokens2[0].GetName(), tokenFTName) + require.Equal(t, tokens2[0].GetTokenType(), tokenFTType) + paramsNoExist := types.QueryTokenTypeParams{ + TokenType: "99999999", + } + var tokensNoExist types.Tokens + query(t, paramsNoExist, types.QueryTokensWithTokenType, &tokensNoExist) + require.Equal(t, len(tokensNoExist), 0) + require.Empty(t, tokensNoExist) +} + +func TestNewQuerier_queryCollections_one(t *testing.T) { + prepare(t) + + var collection types.Collection + query(t, nil, types.QueryCollections, &collection) + require.Equal(t, collection.GetContractID(), contractID) + require.Equal(t, collection.GetName(), collectionName) + require.Equal(t, collection.GetBaseImgURI(), imageURL) +} + +func TestNewQuerier_queryNFTCount(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTType, + } + var count sdk.Int + query(t, params, types.QueryNFTCount, &count) + require.Equal(t, count, sdk.NewInt(3)) +} + +func TestNewQuerier_queryTotalSupply_FT(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenFTID, + } + var supply sdk.Int + query(t, params, types.QuerySupply, &supply) + require.Equal(t, supply.Int64(), int64(tokenFTSupply)) +} + +func TestNewQuerier_queryTotalMint_FT(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenFTID, + } + var supply sdk.Int + query(t, params, types.QueryMint, &supply) + require.Equal(t, supply.Int64(), int64(tokenFTSupply)) +} + +func TestNewQuerier_queryTotalBurn_FT(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenFTID, + } + var supply sdk.Int + query(t, params, types.QueryBurn, &supply) + require.Equal(t, supply.Int64(), int64(0)) +} + +func TestNewQuerier_queryTotalSupply_NFT(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTType, + } + var supply sdk.Int + query(t, params, types.QueryNFTCount, &supply) + require.Equal(t, int64(3), supply.Int64()) +} + +func TestNewQuerier_queryTotalMint_NFT(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTType, + } + var supply sdk.Int + query(t, params, types.QueryNFTMint, &supply) + require.Equal(t, int64(3), supply.Int64()) +} + +func TestNewQuerier_queryTotalBurn_NFT(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTType, + } + var supply sdk.Int + query(t, params, types.QueryNFTBurn, &supply) + require.Equal(t, supply.Int64(), int64(0)) +} + +func TestNewQuerier_queryParent(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTID2, + } + var token types.Token + query(t, params, types.QueryParent, &token) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenNFTID1) +} + +func TestNewQuerier_queryParent_nil(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTID1, + } + var token types.Token + query(t, params, types.QueryParent, &token) + require.Equal(t, token, nil) +} + +func TestNewQuerier_queryRoot(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTID3, + } + var token types.Token + query(t, params, types.QueryRoot, &token) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenNFTID1) +} + +func TestNewQuerier_queryRoot_self(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTID1, + } + var token types.Token + query(t, params, types.QueryRoot, &token) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetTokenID(), tokenNFTID1) +} + +func TestNewQuerier_queryChildren(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTID1, + } + var tokens types.Tokens + query(t, params, types.QueryChildren, &tokens) + require.Equal(t, len(tokens), 2) + require.Equal(t, tokens[0].GetContractID(), contractID) + require.Equal(t, tokens[0].GetTokenID(), tokenNFTID2) + require.Equal(t, tokens[1].GetContractID(), contractID) + require.Equal(t, tokens[1].GetTokenID(), tokenNFTID3) +} + +func TestNewQuerier_queryChildren_empty(t *testing.T) { + prepare(t) + + params := types.QueryTokenIDParams{ + TokenID: tokenNFTID2, + } + var tokens types.Tokens + query(t, params, types.QueryChildren, &tokens) + require.Equal(t, len(tokens), 0) +} + +func TestNewQuerier_queryApprovers(t *testing.T) { + prepare(t) + params := types.QueryProxyParams{ + Proxy: addr1, + } + var acAd1 []sdk.AccAddress + query(t, params, types.QueryApprovers, &acAd1) + require.Equal(t, 2, len(acAd1)) + if bytes.Compare(addr2, addr3) <= 0 { + require.Equal(t, addr2, acAd1[0]) + require.Equal(t, addr3, acAd1[1]) + } else { + require.Equal(t, addr2, acAd1[1]) + require.Equal(t, addr3, acAd1[0]) + } + + var acAdEmpty []sdk.AccAddress + paramsEmpty := types.QueryProxyParams{ + Proxy: addr2, + } + query(t, paramsEmpty, types.QueryApprovers, &acAdEmpty) + require.Empty(t, acAdEmpty) +} + +func TestNewQuerier_queryIsApproved_true(t *testing.T) { + prepare(t) + params := types.QueryIsApprovedParams{ + Proxy: addr1, + Approver: addr2, + } + var approved bool + query(t, params, types.QueryIsApproved, &approved) + require.True(t, approved) +} + +func TestNewQuerier_queryIsApproved_false(t *testing.T) { + prepare(t) + + params := types.QueryIsApprovedParams{ + Proxy: addr2, + Approver: addr1, + } + var approved bool + query(t, params, types.QueryIsApproved, &approved) + require.False(t, approved) +} + +func TestNewQuerier_invalid(t *testing.T) { + prepare(t) + params := types.QueryTokenIDParams{ + TokenID: tokenNFTID1, + } + querier := NewQuerier(ckeeper) + path := []string{"noquery"} + req := abci.RequestQuery{ + Path: "", + Data: []byte(string(codec.MustMarshalJSONIndent(types.ModuleCdc, params))), + } + _, err := querier(ctx, path, req) + require.EqualError(t, err, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown collection query endpoint").Error()) +} diff --git a/x/collection/internal/types/account.go b/x/collection/internal/types/account.go new file mode 100644 index 0000000000..1b4be05dd7 --- /dev/null +++ b/x/collection/internal/types/account.go @@ -0,0 +1,56 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type Account interface { + GetAddress() sdk.AccAddress + GetContractID() string + GetCoins() Coins + SetCoins(Coins) Account + String() string +} + +type BaseAccount struct { + Address sdk.AccAddress `json:"address"` + ContractID string `json:"contract_id"` + Coins Coins `json:"tokens"` +} + +func NewBaseAccountWithAddress(contractID string, addr sdk.AccAddress) *BaseAccount { + return &BaseAccount{ + ContractID: contractID, + Address: addr, + Coins: NewCoins(), + } +} + +func (acc BaseAccount) String() string { + b, err := json.Marshal(acc) + if err != nil { + panic(err) + } + return string(b) +} + +func (acc BaseAccount) GetContractID() string { + return acc.ContractID +} + +func (acc BaseAccount) GetAddress() sdk.AccAddress { + return acc.Address +} + +func (acc BaseAccount) GetCoins() Coins { + return acc.Coins +} + +func (acc BaseAccount) SetCoins(coins Coins) Account { + acc.Coins = coins + return acc +} + +type Accounts []Account diff --git a/x/collection/internal/types/codec.go b/x/collection/internal/types/codec.go new file mode 100644 index 0000000000..78e00a26a2 --- /dev/null +++ b/x/collection/internal/types/codec.go @@ -0,0 +1,59 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +var ModuleCdc *codec.Codec + +func init() { + ModuleCdc = codec.New() + RegisterCodec(ModuleCdc) + ModuleCdc.Seal() +} + +// RegisterCodec registers concrete types on the Amino codec +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(MsgCreateCollection{}, "collection/MsgCreate", nil) + cdc.RegisterConcrete(MsgIssueFT{}, "collection/MsgIssueFT", nil) + cdc.RegisterConcrete(MsgIssueNFT{}, "collection/MsgIssueNFT", nil) + cdc.RegisterConcrete(MsgMintNFT{}, "collection/MsgMintNFT", nil) + cdc.RegisterConcrete(MsgBurnNFT{}, "collection/MsgBurnNFT", nil) + cdc.RegisterConcrete(MsgBurnNFTFrom{}, "collection/MsgBurnNFTFrom", nil) + cdc.RegisterConcrete(MsgModify{}, "collection/MsgModify", nil) + cdc.RegisterConcrete(MsgMintFT{}, "collection/MsgMintFT", nil) + cdc.RegisterConcrete(MsgBurnFT{}, "collection/MsgBurnFT", nil) + cdc.RegisterConcrete(MsgBurnFTFrom{}, "collection/MsgBurnFTFrom", nil) + cdc.RegisterConcrete(MsgGrantPermission{}, "collection/MsgGrantPermission", nil) + cdc.RegisterConcrete(MsgRevokePermission{}, "collection/MsgRevokePermission", nil) + cdc.RegisterConcrete(MsgTransferFT{}, "collection/MsgTransferFT", nil) + cdc.RegisterConcrete(MsgTransferNFT{}, "collection/MsgTransferNFT", nil) + cdc.RegisterConcrete(MsgTransferFTFrom{}, "collection/MsgTransferFTFrom", nil) + cdc.RegisterConcrete(MsgTransferNFTFrom{}, "collection/MsgTransferNFTFrom", nil) + cdc.RegisterConcrete(MsgAttach{}, "collection/MsgAttach", nil) + cdc.RegisterConcrete(MsgDetach{}, "collection/MsgDetach", nil) + cdc.RegisterConcrete(MsgAttachFrom{}, "collection/MsgAttachFrom", nil) + cdc.RegisterConcrete(MsgDetachFrom{}, "collection/MsgDetachFrom", nil) + cdc.RegisterConcrete(MsgApprove{}, "collection/MsgApprove", nil) + cdc.RegisterConcrete(MsgDisapprove{}, "collection/MsgDisapprove", nil) + + cdc.RegisterInterface((*Token)(nil), nil) + cdc.RegisterInterface((*FT)(nil), nil) + + cdc.RegisterInterface((*Collection)(nil), nil) + cdc.RegisterConcrete(&BaseCollection{}, "collection/Collection", nil) + cdc.RegisterConcrete(&BaseFT{}, "collection/FT", nil) + cdc.RegisterConcrete(&BaseNFT{}, "collection/NFT", nil) + + cdc.RegisterInterface((*Account)(nil), nil) + cdc.RegisterConcrete(&BaseAccount{}, "collection/Account", nil) + + cdc.RegisterInterface((*Supply)(nil), nil) + cdc.RegisterConcrete(&BaseSupply{}, "collection/Supply", nil) + + cdc.RegisterInterface((*TokenType)(nil), nil) + cdc.RegisterConcrete(&BaseTokenType{}, "collection/TokenType", nil) + + cdc.RegisterInterface((*AccountPermissionI)(nil), nil) + cdc.RegisterConcrete(&AccountPermission{}, "collection/AccountPermission", nil) +} diff --git a/x/collection/internal/types/coin.go b/x/collection/internal/types/coin.go new file mode 100644 index 0000000000..e5798c2d15 --- /dev/null +++ b/x/collection/internal/types/coin.go @@ -0,0 +1,698 @@ +// copied from https://github.com/cosmos/cosmos-sdk/blob/v0.38.1/types/coin.go +package types + +import ( + "encoding/json" + "fmt" + "regexp" + "sort" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var ( + OneCoin = func(denom string) Coin { return NewCoin(denom, sdk.NewInt(1)) } + OneCoins = func(denom string) Coins { return NewCoins(OneCoin(denom)) } +) + +// ----------------------------------------------------------------------------- +// Coin + +// Coin hold some amount of one currency. +// +// CONTRACT: A coin will never hold a negative amount of any denomination. +// +type Coin struct { + Denom string `json:"token_id"` + + // To allow the use of unsigned integers (see: #1273) a larger refactor will + // need to be made. So we use signed integers for now with safety measures in + // place preventing negative values being used. + Amount sdk.Int `json:"amount"` +} + +// NewCoin returns a new coin with a denomination and amount. It will panic if +// the amount is negative. +func NewCoin(denom string, amount sdk.Int) Coin { + if err := validate(denom, amount); err != nil { + panic(err) + } + + return Coin{ + Denom: denom, + Amount: amount, + } +} + +// NewInt64Coin returns a new coin with a denomination and amount. It will panic +// if the amount is negative. +func NewInt64Coin(denom string, amount int64) Coin { + return NewCoin(denom, sdk.NewInt(amount)) +} + +// String provides a human-readable representation of a coin +func (coin Coin) String() string { + return fmt.Sprintf("%v:%v", coin.Amount, coin.Denom) +} + +// validate returns an error if the Coin has a negative amount or if +// the denom is invalid. +func validate(denom string, amount sdk.Int) error { + if err := ValidateDenom(denom); err != nil { + return err + } + + if amount.IsNegative() { + return fmt.Errorf("negative coin amount: %v", amount) + } + + return nil +} + +// IsValid returns true if the Coin has a non-negative amount and the denom is vaild. +func (coin Coin) IsValid() bool { + if err := validate(coin.Denom, coin.Amount); err != nil { + return false + } + return true +} + +// IsZero returns if this represents no money +func (coin Coin) IsZero() bool { + return coin.Amount.IsZero() +} + +// IsGTE returns true if they are the same type and the receiver is +// an equal or greater value +func (coin Coin) IsGTE(other Coin) bool { + if coin.Denom != other.Denom { + panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, other.Denom)) + } + + return !coin.Amount.LT(other.Amount) +} + +// IsLT returns true if they are the same type and the receiver is +// a smaller value +func (coin Coin) IsLT(other Coin) bool { + if coin.Denom != other.Denom { + panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, other.Denom)) + } + + return coin.Amount.LT(other.Amount) +} + +// IsEqual returns true if the two sets of Coins have the same value +func (coin Coin) IsEqual(other Coin) bool { + if coin.Denom != other.Denom { + panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, other.Denom)) + } + + return coin.Amount.Equal(other.Amount) +} + +// Adds amounts of two coins with same denom. If the coins differ in denom then +// it panics. +func (coin Coin) Add(coinB Coin) Coin { + if coin.Denom != coinB.Denom { + panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, coinB.Denom)) + } + + return Coin{coin.Denom, coin.Amount.Add(coinB.Amount)} +} + +// Subtracts amounts of two coins with same denom. If the coins differ in denom +// then it panics. +func (coin Coin) Sub(coinB Coin) Coin { + if coin.Denom != coinB.Denom { + panic(fmt.Sprintf("invalid coin denominations; %s, %s", coin.Denom, coinB.Denom)) + } + + res := Coin{coin.Denom, coin.Amount.Sub(coinB.Amount)} + if res.IsNegative() { + panic("negative coin amount") + } + + return res +} + +// IsPositive returns true if coin amount is positive. +// +func (coin Coin) IsPositive() bool { + return coin.Amount.Sign() == 1 +} + +// IsNegative returns true if the coin amount is negative and false otherwise. +// +func (coin Coin) IsNegative() bool { + return coin.Amount.Sign() == -1 +} + +// ----------------------------------------------------------------------------- +// Coins + +// Coins is a set of Coin, one per currency +type Coins []Coin + +// NewCoins constructs a new coin set. +func NewCoins(coins ...Coin) Coins { + // remove zeroes + newCoins := removeZeroCoins(Coins(coins)) + if len(newCoins) == 0 { + return Coins{} + } + + newCoins.Sort() + + // detect duplicate Denoms + if dupIndex := findDup(newCoins); dupIndex != -1 { + panic(fmt.Errorf("find duplicate denom: %s", newCoins[dupIndex])) + } + + if !newCoins.IsValid() { + panic(fmt.Errorf("invalid coin set: %s", newCoins)) + } + + return newCoins +} + +type coinsJSON Coins + +// MarshalJSON implements a custom JSON marshaller for the Coins type to allow +// nil Coins to be encoded as an empty array. +func (coins Coins) MarshalJSON() ([]byte, error) { + if coins == nil { + return json.Marshal(coinsJSON(Coins{})) + } + + return json.Marshal(coinsJSON(coins)) +} + +func (coins Coins) String() string { + if len(coins) == 0 { + return "" + } + + out := "" + for _, coin := range coins { + out += fmt.Sprintf("%v,", coin.String()) + } + return out[:len(out)-1] +} + +// IsValid asserts the Coins are sorted, have positive amount, +// and Denom does not contain upper case characters. +func (coins Coins) IsValid() bool { + switch len(coins) { + case 0: + return true + case 1: + if err := ValidateDenom(coins[0].Denom); err != nil { + return false + } + return coins[0].IsPositive() + default: + // check single coin case + if !(Coins{coins[0]}).IsValid() { + return false + } + + lowDenom := coins[0].Denom + for _, coin := range coins[1:] { + if strings.ToLower(coin.Denom) != coin.Denom { + return false + } + if coin.Denom <= lowDenom { + return false + } + if !coin.IsPositive() { + return false + } + + // we compare each coin against the last denom + lowDenom = coin.Denom + } + + return true + } +} + +// Add adds two sets of coins. +// +// e.g. +// {2A} + {A, 2B} = {3A, 2B} +// {2A} + {0B} = {2A} +// +// NOTE: Add operates under the invariant that coins are sorted by +// denominations. +// +// CONTRACT: Add will never return Coins where one Coin has a non-positive +// amount. In otherwords, IsValid will always return true. +func (coins Coins) Add(coinsB ...Coin) Coins { + return coins.safeAdd(coinsB) +} + +// safeAdd will perform addition of two coins sets. If both coin sets are +// empty, then an empty set is returned. If only a single set is empty, the +// other set is returned. Otherwise, the coins are compared in order of their +// denomination and addition only occurs when the denominations match, otherwise +// the coin is simply added to the sum assuming it's not zero. +func (coins Coins) safeAdd(coinsB Coins) Coins { + sum := ([]Coin)(nil) + indexA, indexB := 0, 0 + lenA, lenB := len(coins), len(coinsB) + + for { + if indexA == lenA { + if indexB == lenB { + // return nil coins if both sets are empty + return sum + } + + // return set B (excluding zero coins) if set A is empty + return append(sum, removeZeroCoins(coinsB[indexB:])...) + } else if indexB == lenB { + // return set A (excluding zero coins) if set B is empty + return append(sum, removeZeroCoins(coins[indexA:])...) + } + + coinA, coinB := coins[indexA], coinsB[indexB] + + switch strings.Compare(coinA.Denom, coinB.Denom) { + case -1: // coin A denom < coin B denom + if !coinA.IsZero() { + sum = append(sum, coinA) + } + + indexA++ + + case 0: // coin A denom == coin B denom + res := coinA.Add(coinB) + if !res.IsZero() { + sum = append(sum, res) + } + + indexA++ + indexB++ + + case 1: // coin A denom > coin B denom + if !coinB.IsZero() { + sum = append(sum, coinB) + } + + indexB++ + } + } +} + +// DenomsSubsetOf returns true if receiver's denom set +// is subset of coinsB's denoms. +func (coins Coins) DenomsSubsetOf(coinsB Coins) bool { + // more denoms in B than in receiver + if len(coins) > len(coinsB) { + return false + } + + for _, coin := range coins { + if coinsB.AmountOf(coin.Denom).IsZero() { + return false + } + } + + return true +} + +// Sub subtracts a set of coins from another. +// +// e.g. +// {2A, 3B} - {A} = {A, 3B} +// {2A} - {0B} = {2A} +// {A, B} - {A} = {B} +// +// CONTRACT: Sub will never return Coins where one Coin has a non-positive +// amount. In otherwords, IsValid will always return true. +func (coins Coins) Sub(coinsB Coins) Coins { + diff, hasNeg := coins.SafeSub(coinsB) + if hasNeg { + panic("negative coin amount") + } + + return diff +} + +// SafeSub performs the same arithmetic as Sub but returns a boolean if any +// negative coin amount was returned. +func (coins Coins) SafeSub(coinsB Coins) (Coins, bool) { + diff := coins.safeAdd(coinsB.negative()) + return diff, diff.IsAnyNegative() +} + +// IsAllGT returns true if for every denom in coinsB, +// the denom is present at a greater amount in coins. +func (coins Coins) IsAllGT(coinsB Coins) bool { + if len(coins) == 0 { + return false + } + + if len(coinsB) == 0 { + return true + } + + if !coinsB.DenomsSubsetOf(coins) { + return false + } + + for _, coinB := range coinsB { + amountA, amountB := coins.AmountOf(coinB.Denom), coinB.Amount + if !amountA.GT(amountB) { + return false + } + } + + return true +} + +// IsAllGTE returns false if for any denom in coinsB, +// the denom is present at a smaller amount in coins; +// else returns true. +func (coins Coins) IsAllGTE(coinsB Coins) bool { + if len(coinsB) == 0 { + return true + } + + if len(coins) == 0 { + return false + } + + for _, coinB := range coinsB { + if coinB.Amount.GT(coins.AmountOf(coinB.Denom)) { + return false + } + } + + return true +} + +// IsAllLT returns True iff for every denom in coins, the denom is present at +// a smaller amount in coinsB. +func (coins Coins) IsAllLT(coinsB Coins) bool { + return coinsB.IsAllGT(coins) +} + +// IsAllLTE returns true iff for every denom in coins, the denom is present at +// a smaller or equal amount in coinsB. +func (coins Coins) IsAllLTE(coinsB Coins) bool { + return coinsB.IsAllGTE(coins) +} + +// IsAnyGT returns true iff for any denom in coins, the denom is present at a +// greater amount in coinsB. +// +// e.g. +// {2A, 3B}.IsAnyGT{A} = true +// {2A, 3B}.IsAnyGT{5C} = false +// {}.IsAnyGT{5C} = false +// {2A, 3B}.IsAnyGT{} = false +func (coins Coins) IsAnyGT(coinsB Coins) bool { + if len(coinsB) == 0 { + return false + } + + for _, coin := range coins { + amt := coinsB.AmountOf(coin.Denom) + if coin.Amount.GT(amt) && !amt.IsZero() { + return true + } + } + + return false +} + +// IsAnyGTE returns true iff coins contains at least one denom that is present +// at a greater or equal amount in coinsB; it returns false otherwise. +// +// NOTE: IsAnyGTE operates under the invariant that both coin sets are sorted +// by denominations and there exists no zero coins. +func (coins Coins) IsAnyGTE(coinsB Coins) bool { + if len(coinsB) == 0 { + return false + } + + for _, coin := range coins { + amt := coinsB.AmountOf(coin.Denom) + if coin.Amount.GTE(amt) && !amt.IsZero() { + return true + } + } + + return false +} + +// IsZero returns true if there are no coins or all coins are zero. +func (coins Coins) IsZero() bool { + for _, coin := range coins { + if !coin.IsZero() { + return false + } + } + return true +} + +// IsEqual returns true if the two sets of Coins have the same value +func (coins Coins) IsEqual(coinsB Coins) bool { + if len(coins) != len(coinsB) { + return false + } + + coins = coins.Sort() + coinsB = coinsB.Sort() + + for i := 0; i < len(coins); i++ { + if !coins[i].IsEqual(coinsB[i]) { + return false + } + } + + return true +} + +// Empty returns true if there are no coins and false otherwise. +func (coins Coins) Empty() bool { + return len(coins) == 0 +} + +// Returns the amount of a denom from coins +func (coins Coins) AmountOf(denom string) sdk.Int { + mustValidateDenom(denom) + + switch len(coins) { + case 0: + return sdk.ZeroInt() + + case 1: + coin := coins[0] + if coin.Denom == denom { + return coin.Amount + } + return sdk.ZeroInt() + + default: + midIdx := len(coins) / 2 // 2:1, 3:1, 4:2 + coin := coins[midIdx] + switch { + case denom < coin.Denom: + return coins[:midIdx].AmountOf(denom) + case denom == coin.Denom: + return coin.Amount + default: + return coins[midIdx+1:].AmountOf(denom) + } + } +} + +// GetDenomByIndex returns the Denom of the certain coin to make the findDup generic +func (coins Coins) GetDenomByIndex(i int) string { + return coins[i].Denom +} + +// IsAllPositive returns true if there is at least one coin and all currencies +// have a positive value. +func (coins Coins) IsAllPositive() bool { + if len(coins) == 0 { + return false + } + + for _, coin := range coins { + if !coin.IsPositive() { + return false + } + } + + return true +} + +// IsAnyNegative returns true if there is at least one coin whose amount +// is negative; returns false otherwise. It returns false if the coin set +// is empty too. +// +func (coins Coins) IsAnyNegative() bool { + for _, coin := range coins { + if coin.IsNegative() { + return true + } + } + + return false +} + +// negative returns a set of coins with all amount negative. +// +func (coins Coins) negative() Coins { + res := make([]Coin, 0, len(coins)) + + for _, coin := range coins { + res = append(res, Coin{ + Denom: coin.Denom, + Amount: coin.Amount.Neg(), + }) + } + + return res +} + +// removeZeroCoins removes all zero coins from the given coin set in-place. +func removeZeroCoins(coins Coins) Coins { + i, l := 0, len(coins) + for i < l { + if coins[i].IsZero() { + // remove coin + coins = append(coins[:i], coins[i+1:]...) + l-- + } else { + i++ + } + } + + return coins[:i] +} + +// ----------------------------------------------------------------------------- +// Sort interface + +// nolint +func (coins Coins) Len() int { return len(coins) } +func (coins Coins) Less(i, j int) bool { return coins[i].Denom < coins[j].Denom } +func (coins Coins) Swap(i, j int) { coins[i], coins[j] = coins[j], coins[i] } + +var _ sort.Interface = Coins{} + +// Sort is a helper function to sort the set of coins inplace +func (coins Coins) Sort() Coins { + sort.Sort(coins) + return coins +} + +// ----------------------------------------------------------------------------- +// Parsing + +var ( + // Denominations can be 3 ~ 16 characters long. + reDnmString = `[a-f0-9]{16}` + reDnm = regexp.MustCompile(fmt.Sprintf(`^%s$`, reDnmString)) + reAmt = `[[:digit:]]+` + reSpc = `:` + reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, reDnmString)) +) + +// ParseCoins will parse out a list of coins separated by commas. +// If nothing is provided, it returns nil Coins. +// Returned coins are sorted. +func ParseCoins(coinsStr string) (Coins, error) { + coinsStr = strings.TrimSpace(coinsStr) + if len(coinsStr) == 0 { + return nil, nil + } + + coinStrs := strings.Split(coinsStr, ",") + coins := make(Coins, len(coinStrs)) + for i, coinStr := range coinStrs { + coin, err := ParseCoin(coinStr) + if err != nil { + return nil, err + } + + coins[i] = coin + } + + // sort coins for determinism + coins.Sort() + + // validate coins before returning + if !coins.IsValid() { + return nil, fmt.Errorf("parseCoins invalid: %#v", coins) + } + + return coins, nil +} + +// ParseCoin parses a cli input for one coin type, returning errors if invalid. +// This returns an error on an empty string as well. +func ParseCoin(coinStr string) (coin Coin, err error) { + coinStr = strings.TrimSpace(coinStr) + + matches := reCoin.FindStringSubmatch(coinStr) + if matches == nil { + return Coin{}, fmt.Errorf("invalid coin expression: %s", coinStr) + } + + denomStr, amountStr := matches[2], matches[1] + + amount, ok := sdk.NewIntFromString(amountStr) + if !ok { + return Coin{}, fmt.Errorf("failed to parse coin amount: %s", amountStr) + } + + if err := ValidateDenom(denomStr); err != nil { + return Coin{}, fmt.Errorf("invalid denom cannot contain upper case characters or spaces: %s", err) + } + + return NewCoin(denomStr, amount), nil +} + +// ValidateDenom validates a denomination string returning an error if it is +// invalid. +func ValidateDenom(denom string) error { + if !reDnm.MatchString(denom) { + return fmt.Errorf("invalid denom: %s", denom) + } + return nil +} + +func mustValidateDenom(denom string) { + if err := ValidateDenom(denom); err != nil { + panic(err) + } +} + +type findDupDescriptor interface { + GetDenomByIndex(int) string + Len() int +} + +// findDup works on the assumption that coins is sorted +func findDup(coins findDupDescriptor) int { + if coins.Len() <= 1 { + return -1 + } + + prevDenom := coins.GetDenomByIndex(0) + for i := 1; i < coins.Len(); i++ { + if coins.GetDenomByIndex(i) == prevDenom { + return i + } + prevDenom = coins.GetDenomByIndex(i) + } + + return -1 +} diff --git a/x/collection/internal/types/coin_test.go b/x/collection/internal/types/coin_test.go new file mode 100644 index 0000000000..48f79cd9b1 --- /dev/null +++ b/x/collection/internal/types/coin_test.go @@ -0,0 +1,712 @@ +// copied from https://github.com/cosmos/cosmos-sdk/blob/v0.38.1/types/coin_test.go +package types + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var ( + testDenom1 = "0000000100000000" + testDenom2 = "0000000200000000" + testDenom3 = "0000000300000000" + testDenom4 = "0000000400000000" + testDenomA = "0000000a00000000" +) + +// ---------------------------------------------------------------------------- +// Coin tests + +func TestCoin(t *testing.T) { + require.Panics(t, func() { NewInt64Coin(testDenom1, -1) }) + require.Panics(t, func() { NewCoin(testDenom1, sdk.NewInt(-1)) }) + require.Panics(t, func() { NewInt64Coin(strings.ToUpper(testDenomA), 10) }) + require.Panics(t, func() { NewCoin(strings.ToUpper(testDenomA), sdk.NewInt(10)) }) + require.Equal(t, sdk.NewInt(5), NewInt64Coin(testDenom1, 5).Amount) + require.Equal(t, sdk.NewInt(5), NewCoin(testDenom1, sdk.NewInt(5)).Amount) + require.Equal(t, OneCoins(testDenom1)[0].Amount.Int64(), int64(1)) +} + +func TestIsEqualCoin(t *testing.T) { + cases := []struct { + inputOne Coin + inputTwo Coin + expected bool + panics bool + }{ + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 1), true, false}, + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom2, 1), false, true}, + {NewInt64Coin(testDenom3, 1), NewInt64Coin(testDenom3, 10), false, false}, + } + + for tcIndex, tc := range cases { + tc := tc + if tc.panics { + require.Panics(t, func() { tc.inputOne.IsEqual(tc.inputTwo) }) + } else { + res := tc.inputOne.IsEqual(tc.inputTwo) + require.Equal(t, tc.expected, res, "coin equality relation is incorrect, tc #%d", tcIndex) + } + } +} + +func TestCoinIsValid(t *testing.T) { + cases := []struct { + coin Coin + expectPass bool + }{ + {Coin{testDenom1, sdk.NewInt(-1)}, false}, + {Coin{testDenom1, sdk.NewInt(0)}, true}, + {Coin{testDenom1, sdk.NewInt(1)}, true}, + {Coin{"a", sdk.NewInt(1)}, false}, + {Coin{"a very long coin denom", sdk.NewInt(1)}, false}, + {Coin{"atOm", sdk.NewInt(1)}, false}, + {Coin{" ", sdk.NewInt(1)}, false}, + } + + for i, tc := range cases { + require.Equal(t, tc.expectPass, tc.coin.IsValid(), "unexpected result for IsValid, tc #%d", i) + } +} + +func TestAddCoin(t *testing.T) { + cases := []struct { + inputOne Coin + inputTwo Coin + expected Coin + shouldPanic bool + }{ + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 2), false}, + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom1, 1), false}, + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom2, 1), NewInt64Coin(testDenom1, 1), true}, + } + + for tcIndex, tc := range cases { + tc := tc + if tc.shouldPanic { + require.Panics(t, func() { tc.inputOne.Add(tc.inputTwo) }) + } else { + res := tc.inputOne.Add(tc.inputTwo) + require.Equal(t, tc.expected, res, "sum of coins is incorrect, tc #%d", tcIndex) + } + } +} + +func TestSubCoin(t *testing.T) { + cases := []struct { + inputOne Coin + inputTwo Coin + expected Coin + shouldPanic bool + }{ + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom2, 1), NewInt64Coin(testDenom1, 1), true}, + {NewInt64Coin(testDenom1, 10), NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 9), false}, + {NewInt64Coin(testDenom1, 5), NewInt64Coin(testDenom1, 3), NewInt64Coin(testDenom1, 2), false}, + {NewInt64Coin(testDenom1, 5), NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom1, 5), false}, + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 5), Coin{}, true}, + } + + for tcIndex, tc := range cases { + tc := tc + if tc.shouldPanic { + require.Panics(t, func() { tc.inputOne.Sub(tc.inputTwo) }) + } else { + res := tc.inputOne.Sub(tc.inputTwo) + require.Equal(t, tc.expected, res, "difference of coins is incorrect, tc #%d", tcIndex) + } + } + + tc := struct { + inputOne Coin + inputTwo Coin + expected int64 + }{NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 1), 0} + res := tc.inputOne.Sub(tc.inputTwo) + require.Equal(t, tc.expected, res.Amount.Int64()) +} + +func TestIsGTECoin(t *testing.T) { + cases := []struct { + inputOne Coin + inputTwo Coin + expected bool + panics bool + }{ + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 1), true, false}, + {NewInt64Coin(testDenom1, 2), NewInt64Coin(testDenom1, 1), true, false}, + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom2, 1), false, true}, + } + + for tcIndex, tc := range cases { + tc := tc + if tc.panics { + require.Panics(t, func() { tc.inputOne.IsGTE(tc.inputTwo) }) + } else { + res := tc.inputOne.IsGTE(tc.inputTwo) + require.Equal(t, tc.expected, res, "coin GTE relation is incorrect, tc #%d", tcIndex) + } + } +} + +func TestIsLTCoin(t *testing.T) { + cases := []struct { + inputOne Coin + inputTwo Coin + expected bool + panics bool + }{ + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 1), false, false}, + {NewInt64Coin(testDenom1, 2), NewInt64Coin(testDenom1, 1), false, false}, + {NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom2, 1), false, true}, + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom2, 1), false, true}, + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 1), false, false}, + {NewInt64Coin(testDenom1, 1), NewInt64Coin(testDenom1, 2), true, false}, + } + + for tcIndex, tc := range cases { + tc := tc + if tc.panics { + require.Panics(t, func() { tc.inputOne.IsLT(tc.inputTwo) }) + } else { + res := tc.inputOne.IsLT(tc.inputTwo) + require.Equal(t, tc.expected, res, "coin LT relation is incorrect, tc #%d", tcIndex) + } + } +} + +func TestCoinIsZero(t *testing.T) { + coin := NewInt64Coin(testDenom1, 0) + res := coin.IsZero() + require.True(t, res) + + coin = NewInt64Coin(testDenom1, 1) + res = coin.IsZero() + require.False(t, res) +} + +// ---------------------------------------------------------------------------- +// Coins tests + +func TestIsZeroCoins(t *testing.T) { + cases := []struct { + inputOne Coins + expected bool + }{ + {Coins{}, true}, + {Coins{NewInt64Coin(testDenom1, 0)}, true}, + {Coins{NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom2, 0)}, true}, + {Coins{NewInt64Coin(testDenom1, 1)}, false}, + {Coins{NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom2, 1)}, false}, + } + + for _, tc := range cases { + res := tc.inputOne.IsZero() + require.Equal(t, tc.expected, res) + } +} + +func TestEqualCoins(t *testing.T) { + cases := []struct { + inputOne Coins + inputTwo Coins + expected bool + panics bool + }{ + {Coins{}, Coins{}, true, false}, + {Coins{NewInt64Coin(testDenom1, 0)}, Coins{NewInt64Coin(testDenom1, 0)}, true, false}, + {Coins{NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom2, 1)}, Coins{NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom2, 1)}, true, false}, + {Coins{NewInt64Coin(testDenom1, 0)}, Coins{NewInt64Coin(testDenom2, 0)}, false, true}, + {Coins{NewInt64Coin(testDenom1, 0)}, Coins{NewInt64Coin(testDenom1, 1)}, false, false}, + {Coins{NewInt64Coin(testDenom1, 0)}, Coins{NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom2, 1)}, false, false}, + {Coins{NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom2, 1)}, Coins{NewInt64Coin(testDenom1, 0), NewInt64Coin(testDenom2, 1)}, true, false}, + } + + for tcnum, tc := range cases { + tc := tc + if tc.panics { + require.Panics(t, func() { tc.inputOne.IsEqual(tc.inputTwo) }) + } else { + res := tc.inputOne.IsEqual(tc.inputTwo) + require.Equal(t, tc.expected, res, "Equality is differed from exported. tc #%d, expected %b, actual %b.", tcnum, tc.expected, res) + } + } +} + +func TestAddCoins(t *testing.T) { + zero := sdk.NewInt(0) + one := sdk.NewInt(1) + two := sdk.NewInt(2) + + cases := []struct { + inputOne Coins + inputTwo Coins + expected Coins + }{ + {Coins{{testDenom1, one}, {testDenom2, one}}, Coins{{testDenom1, one}, {testDenom2, one}}, Coins{{testDenom1, two}, {testDenom2, two}}}, + {Coins{{testDenom1, zero}, {testDenom2, one}}, Coins{{testDenom1, zero}, {testDenom2, zero}}, Coins{{testDenom2, one}}}, + {Coins{{testDenom1, two}}, Coins{{testDenom2, zero}}, Coins{{testDenom1, two}}}, + {Coins{{testDenom1, one}}, Coins{{testDenom1, one}, {testDenom2, two}}, Coins{{testDenom1, two}, {testDenom2, two}}}, + {Coins{{testDenom1, zero}, {testDenom2, zero}}, Coins{{testDenom1, zero}, {testDenom2, zero}}, Coins(nil)}, + } + + for tcIndex, tc := range cases { + res := tc.inputOne.Add(tc.inputTwo...) + assert.True(t, res.IsValid()) + require.Equal(t, tc.expected, res, "sum of coins is incorrect, tc #%d", tcIndex) + } +} + +func TestSubCoins(t *testing.T) { + zero := sdk.NewInt(0) + one := sdk.NewInt(1) + two := sdk.NewInt(2) + + testCases := []struct { + inputOne Coins + inputTwo Coins + expected Coins + shouldPanic bool + }{ + {Coins{{testDenom1, two}}, Coins{{testDenom1, one}, {testDenom2, two}}, Coins{{testDenom1, one}, {testDenom2, two}}, true}, + {Coins{{testDenom1, two}}, Coins{{testDenom2, zero}}, Coins{{testDenom1, two}}, false}, + {Coins{{testDenom1, one}}, Coins{{testDenom2, zero}}, Coins{{testDenom1, one}}, false}, + {Coins{{testDenom1, one}, {testDenom2, one}}, Coins{{testDenom1, one}}, Coins{{testDenom2, one}}, false}, + {Coins{{testDenom1, one}, {testDenom2, one}}, Coins{{testDenom1, two}}, Coins{}, true}, + } + + for i, tc := range testCases { + tc := tc + if tc.shouldPanic { + require.Panics(t, func() { tc.inputOne.Sub(tc.inputTwo) }) + } else { + res := tc.inputOne.Sub(tc.inputTwo) + assert.True(t, res.IsValid()) + require.Equal(t, tc.expected, res, "sum of coins is incorrect, tc #%d", i) + } + } +} + +func TestCoins(t *testing.T) { + good := Coins{ + {testDenom1, sdk.NewInt(1)}, + {testDenom2, sdk.NewInt(1)}, + {testDenom3, sdk.NewInt(1)}, + } + mixedCase1 := Coins{ + {"000A0000", sdk.NewInt(1)}, + {"A0000000", sdk.NewInt(1)}, + {"ABCD0000", sdk.NewInt(1)}, + } + mixedCase2 := Coins{ + {"000A0000", sdk.NewInt(1)}, + {testDenom3, sdk.NewInt(1)}, + } + mixedCase3 := Coins{ + {"000A0000", sdk.NewInt(1)}, + } + empty := NewCoins() + badSort1 := Coins{ + {testDenom3, sdk.NewInt(1)}, + {testDenom1, sdk.NewInt(1)}, + {testDenom2, sdk.NewInt(1)}, + } + + // both are after the first one, but the second and third are in the wrong order + badSort2 := Coins{ + {testDenom1, sdk.NewInt(1)}, + {testDenom3, sdk.NewInt(1)}, + {testDenom2, sdk.NewInt(1)}, + } + badAmt := Coins{ + {testDenom1, sdk.NewInt(1)}, + {testDenom3, sdk.NewInt(0)}, + {testDenom2, sdk.NewInt(1)}, + } + dup := Coins{ + {testDenom1, sdk.NewInt(1)}, + {testDenom1, sdk.NewInt(1)}, + {testDenom2, sdk.NewInt(1)}, + } + neg := Coins{ + {testDenom1, sdk.NewInt(-1)}, + {testDenom2, sdk.NewInt(1)}, + } + + assert.True(t, good.IsValid(), "Coins are valid") + assert.False(t, mixedCase1.IsValid(), "Coins denoms contain upper case characters") + assert.False(t, mixedCase2.IsValid(), "First Coins denoms contain upper case characters") + assert.False(t, mixedCase3.IsValid(), "Single denom in Coins contains upper case characters") + assert.True(t, good.IsAllPositive(), "Expected coins to be positive: %v", good) + assert.False(t, empty.IsAllPositive(), "Expected coins to not be positive: %v", empty) + assert.True(t, good.IsAllGTE(empty), "Expected %v to be >= %v", good, empty) + assert.False(t, good.IsAllLT(empty), "Expected %v to be < %v", good, empty) + assert.True(t, empty.IsAllLT(good), "Expected %v to be < %v", empty, good) + assert.False(t, badSort1.IsValid(), "Coins are not sorted") + assert.False(t, badSort2.IsValid(), "Coins are not sorted") + assert.False(t, badAmt.IsValid(), "Coins cannot include 0 amounts") + assert.False(t, dup.IsValid(), "Duplicate coin") + assert.False(t, neg.IsValid(), "Negative first-denom coin") +} + +func TestCoinsGT(t *testing.T) { + one := sdk.NewInt(1) + two := sdk.NewInt(2) + + assert.False(t, Coins{}.IsAllGT(Coins{})) + assert.True(t, Coins{{testDenom1, one}}.IsAllGT(Coins{})) + assert.False(t, Coins{{testDenom1, one}}.IsAllGT(Coins{{testDenom1, one}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllGT(Coins{{testDenom2, one}})) + assert.True(t, Coins{{testDenom1, one}, {testDenom2, two}}.IsAllGT(Coins{{testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllGT(Coins{{testDenom2, two}})) +} + +// nolint:dupl +func TestCoinsLT(t *testing.T) { + one := sdk.NewInt(1) + two := sdk.NewInt(2) + + assert.False(t, Coins{}.IsAllLT(Coins{})) + assert.False(t, Coins{{testDenom1, one}}.IsAllLT(Coins{})) + assert.False(t, Coins{{testDenom1, one}}.IsAllLT(Coins{{testDenom1, one}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllLT(Coins{{testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllLT(Coins{{testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllLT(Coins{{testDenom2, two}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllLT(Coins{{testDenom1, one}, {testDenom2, one}})) + assert.True(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllLT(Coins{{testDenom1, two}, {testDenom2, two}})) + assert.True(t, Coins{}.IsAllLT(Coins{{testDenom1, one}})) +} + +// nolint:dupl +func TestCoinsLTE(t *testing.T) { + one := sdk.NewInt(1) + two := sdk.NewInt(2) + + assert.True(t, Coins{}.IsAllLTE(Coins{})) + assert.False(t, Coins{{testDenom1, one}}.IsAllLTE(Coins{})) + assert.True(t, Coins{{testDenom1, one}}.IsAllLTE(Coins{{testDenom1, one}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllLTE(Coins{{testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllLTE(Coins{{testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllLTE(Coins{{testDenom2, two}})) + assert.True(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllLTE(Coins{{testDenom1, one}, {testDenom2, one}})) + assert.True(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllLTE(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.True(t, Coins{}.IsAllLTE(Coins{{testDenom1, one}})) +} + +func TestSortCoins(t *testing.T) { + good := Coins{ + NewInt64Coin(testDenom1, 1), + NewInt64Coin(testDenom2, 1), + NewInt64Coin(testDenom3, 1), + } + empty := Coins{ + NewInt64Coin(testDenom4, 0), + } + badSort1 := Coins{ + NewInt64Coin(testDenom3, 1), + NewInt64Coin(testDenom1, 1), + NewInt64Coin(testDenom2, 1), + } + badSort2 := Coins{ // both are after the first one, but the second and third are in the wrong order + NewInt64Coin(testDenom1, 1), + NewInt64Coin(testDenom3, 1), + NewInt64Coin(testDenom2, 1), + } + badAmt := Coins{ + NewInt64Coin(testDenom1, 1), + NewInt64Coin(testDenom3, 0), + NewInt64Coin(testDenom2, 1), + } + dup := Coins{ + NewInt64Coin(testDenom1, 1), + NewInt64Coin(testDenom1, 1), + NewInt64Coin(testDenom2, 1), + } + + cases := []struct { + coins Coins + before, after bool // valid before/after sort + }{ + {good, true, true}, + {empty, false, false}, + {badSort1, false, true}, + {badSort2, false, true}, + {badAmt, false, false}, + {dup, false, false}, + } + + for tcIndex, tc := range cases { + require.Equal(t, tc.before, tc.coins.IsValid(), "coin validity is incorrect before sorting, tc #%d", tcIndex) + tc.coins.Sort() + require.Equal(t, tc.after, tc.coins.IsValid(), "coin validity is incorrect after sorting, tc #%d", tcIndex) + } +} + +func TestAmountOf(t *testing.T) { + case0 := Coins{} + case1 := Coins{ + NewInt64Coin(testDenom4, 0), + } + case2 := Coins{ + NewInt64Coin(testDenom1, 1), + NewInt64Coin(testDenom2, 1), + NewInt64Coin(testDenom3, 1), + } + case3 := Coins{ + NewInt64Coin(testDenom2, 1), + NewInt64Coin(testDenom3, 1), + } + case4 := Coins{ + NewInt64Coin(testDenom1, 8), + } + + cases := []struct { + coins Coins + amountOf int64 + amountOfSpace int64 + amountOfGAS int64 + amountOfMINERAL int64 + amountOfTREE int64 + }{ + {case0, 0, 0, 0, 0, 0}, + {case1, 0, 0, 0, 0, 0}, + {case2, 0, 0, 1, 1, 1}, + {case3, 0, 0, 0, 1, 1}, + {case4, 0, 0, 8, 0, 0}, + } + + for _, tc := range cases { + assert.Equal(t, sdk.NewInt(tc.amountOfGAS), tc.coins.AmountOf(testDenom1)) + assert.Equal(t, sdk.NewInt(tc.amountOfMINERAL), tc.coins.AmountOf(testDenom2)) + assert.Equal(t, sdk.NewInt(tc.amountOfTREE), tc.coins.AmountOf(testDenom3)) + } + + assert.Panics(t, func() { cases[0].coins.AmountOf("Invalid") }) +} + +// nolint:dupl +func TestCoinsIsAnyGTE(t *testing.T) { + one := sdk.NewInt(1) + two := sdk.NewInt(2) + + assert.False(t, Coins{}.IsAnyGTE(Coins{})) + assert.False(t, Coins{{testDenom1, one}}.IsAnyGTE(Coins{})) + assert.False(t, Coins{}.IsAnyGTE(Coins{{testDenom1, one}})) + assert.False(t, Coins{{testDenom1, one}}.IsAnyGTE(Coins{{testDenom1, two}})) + assert.False(t, Coins{{testDenom1, one}}.IsAnyGTE(Coins{{testDenom2, one}})) + assert.True(t, Coins{{testDenom1, one}, {testDenom2, two}}.IsAnyGTE(Coins{{testDenom1, two}, {testDenom2, one}})) + assert.True(t, Coins{{testDenom1, one}}.IsAnyGTE(Coins{{testDenom1, one}})) + assert.True(t, Coins{{testDenom1, two}}.IsAnyGTE(Coins{{testDenom1, one}})) + assert.True(t, Coins{{testDenom1, one}}.IsAnyGTE(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.True(t, Coins{{testDenom2, two}}.IsAnyGTE(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.False(t, Coins{{testDenom2, one}}.IsAnyGTE(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.True(t, Coins{{testDenom1, one}, {testDenom2, two}}.IsAnyGTE(Coins{{testDenom1, one}, {testDenom2, one}})) + assert.True(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAnyGTE(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.True(t, Coins{{"00000aaa00000000", one}, {"00000bbb00000000", one}}.IsAnyGTE(Coins{{testDenom2, one}, {"00000ccc00000000", one}, {"00000bbb00000000", one}, {"00000ddd00000000", one}})) +} + +// nolint:dupl +func TestCoinsIsAllGT(t *testing.T) { + one := sdk.NewInt(1) + two := sdk.NewInt(2) + + assert.False(t, Coins{}.IsAllGT(Coins{})) + assert.True(t, Coins{{testDenom1, one}}.IsAllGT(Coins{})) + assert.False(t, Coins{}.IsAllGT(Coins{{testDenom1, one}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllGT(Coins{{testDenom1, two}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllGT(Coins{{testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, two}}.IsAllGT(Coins{{testDenom1, two}, {testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllGT(Coins{{testDenom1, one}})) + assert.True(t, Coins{{testDenom1, two}}.IsAllGT(Coins{{testDenom1, one}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllGT(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.False(t, Coins{{testDenom2, two}}.IsAllGT(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.False(t, Coins{{testDenom2, one}}.IsAllGT(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, two}}.IsAllGT(Coins{{testDenom1, one}, {testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllGT(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.False(t, Coins{{"xxx", one}, {"yyy", one}}.IsAllGT(Coins{{testDenom2, one}, {"ccc", one}, {"yyy", one}, {"zzz", one}})) +} + +func TestCoinsIsAllGTE(t *testing.T) { + one := sdk.NewInt(1) + two := sdk.NewInt(2) + + assert.True(t, Coins{}.IsAllGTE(Coins{})) + assert.True(t, Coins{{testDenom1, one}}.IsAllGTE(Coins{})) + assert.True(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllGTE(Coins{{testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllGTE(Coins{{testDenom2, two}})) + assert.False(t, Coins{}.IsAllGTE(Coins{{testDenom1, one}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllGTE(Coins{{testDenom1, two}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllGTE(Coins{{testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, two}}.IsAllGTE(Coins{{testDenom1, two}, {testDenom2, one}})) + assert.True(t, Coins{{testDenom1, one}}.IsAllGTE(Coins{{testDenom1, one}})) + assert.True(t, Coins{{testDenom1, two}}.IsAllGTE(Coins{{testDenom1, one}})) + assert.False(t, Coins{{testDenom1, one}}.IsAllGTE(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.False(t, Coins{{testDenom2, two}}.IsAllGTE(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.False(t, Coins{{testDenom2, one}}.IsAllGTE(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.True(t, Coins{{testDenom1, one}, {testDenom2, two}}.IsAllGTE(Coins{{testDenom1, one}, {testDenom2, one}})) + assert.False(t, Coins{{testDenom1, one}, {testDenom2, one}}.IsAllGTE(Coins{{testDenom1, one}, {testDenom2, two}})) + assert.False(t, Coins{{"xxx", one}, {"yyy", one}}.IsAllGTE(Coins{{testDenom2, one}, {"ccc", one}, {"yyy", one}, {"zzz", one}})) +} + +func TestNewCoins(t *testing.T) { + tenatom := NewInt64Coin(testDenom1, 10) + tenbtc := NewInt64Coin(testDenom2, 10) + zeroeth := NewInt64Coin(testDenom3, 0) + tests := []struct { + name string + coins Coins + want Coins + wantPanic bool + }{ + {"empty args", []Coin{}, Coins{}, false}, + {"one coin", []Coin{tenatom}, Coins{tenatom}, false}, + {"sort after create", []Coin{tenbtc, tenatom}, Coins{tenatom, tenbtc}, false}, + {"sort and remove zeroes", []Coin{zeroeth, tenbtc, tenatom}, Coins{tenatom, tenbtc}, false}, + {"panic on dups", []Coin{tenatom, tenatom}, Coins{}, true}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if tt.wantPanic { + require.Panics(t, func() { NewCoins(tt.coins...) }) + return + } + got := NewCoins(tt.coins...) + require.True(t, got.IsEqual(tt.want)) + }) + } +} + +func TestCoinsIsAnyGT(t *testing.T) { + twoAtom := NewInt64Coin(testDenom1, 2) + fiveAtom := NewInt64Coin(testDenom1, 5) + threeEth := NewInt64Coin(testDenom2, 3) + sixEth := NewInt64Coin(testDenom2, 6) + twoBtc := NewInt64Coin(testDenom3, 2) + + require.False(t, Coins{}.IsAnyGT(Coins{})) + + require.False(t, Coins{fiveAtom}.IsAnyGT(Coins{})) + require.False(t, Coins{}.IsAnyGT(Coins{fiveAtom})) + require.True(t, Coins{fiveAtom}.IsAnyGT(Coins{twoAtom})) + require.False(t, Coins{twoAtom}.IsAnyGT(Coins{fiveAtom})) + + require.True(t, Coins{twoAtom, sixEth}.IsAnyGT(Coins{twoBtc, fiveAtom, threeEth})) + require.False(t, Coins{twoBtc, twoAtom, threeEth}.IsAnyGT(Coins{fiveAtom, sixEth})) + require.False(t, Coins{twoAtom, sixEth}.IsAnyGT(Coins{twoBtc, fiveAtom})) +} + +func TestFindDup(t *testing.T) { + abc := NewInt64Coin(testDenom1, 10) + def := NewInt64Coin(testDenom2, 10) + ghi := NewInt64Coin(testDenom3, 10) + + type args struct { + coins Coins + } + tests := []struct { + name string + args args + want int + }{ + {"empty", args{NewCoins()}, -1}, + {"one coin", args{NewCoins(NewInt64Coin("00000abc00000000", 10))}, -1}, + {"no dups", args{Coins{abc, def, ghi}}, -1}, + {"dup at first position", args{Coins{abc, abc, def}}, 1}, + {"dup after first position", args{Coins{abc, def, def}}, 2}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if got := findDup(tt.args.coins); got != tt.want { + t.Errorf("findDup() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestMarshalJSONCoins(t *testing.T) { + cdc := codec.New() + RegisterCodec(cdc) + + testCases := []struct { + name string + input Coins + strOutput string + }{ + {"nil coins", nil, `[]`}, + {"empty coins", Coins{}, `[]`}, + {"non-empty coins", NewCoins(NewInt64Coin("00000fee00000000", 50)), `[{"token_id":"00000fee00000000","amount":"50"}]`}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + bz, err := cdc.MarshalJSON(tc.input) + require.NoError(t, err) + require.Equal(t, tc.strOutput, string(bz)) + + var newCoins Coins + require.NoError(t, cdc.UnmarshalJSON(bz, &newCoins)) + + if tc.input.Empty() { + require.Nil(t, newCoins) + } else { + require.Equal(t, tc.input, newCoins) + } + }) + } +} + +func TestParseCoin(t *testing.T) { + cases := []struct { + coinStr string + expectPass bool + }{ + {"1:0000000100000000", true}, + {"1000:0000000100000000", true}, + {"21302131270312:0000000100000000", true}, + {"1:000000010000000", false}, + {"10000000100000000", false}, + {"0001:0000000100000000", true}, + {"1 : 0000000100000000", false}, + {"1:0000000a00000000", true}, + {"1:0000000A00000000", false}, + } + + for i, tc := range cases { + _, err := ParseCoin(tc.coinStr) + if tc.expectPass { + require.NoError(t, err, "unexpected result for IsValid, tc #%d", i) + } else { + require.Error(t, err, "unexpected result for IsValid, tc #%d", i) + } + } +} + +func TestParseCoins(t *testing.T) { + cases := []struct { + coinStr string + expectPass bool + }{ + {"1:0000000100000000", true}, + {"1:0000000100000000,2:0000000200000000", true}, + {"1:0000000100000000,2:0000000200000000,3:0000000300000000", true}, + {"1:0000000100000000 , 2:0000000200000000 , 3:0000000300000000", true}, + } + + for i, tc := range cases { + _, err := ParseCoins(tc.coinStr) + if tc.expectPass { + require.NoError(t, err, "unexpected result for IsValid, tc #%d", i) + } else { + require.Error(t, err, "unexpected result for IsValid, tc #%d", i) + } + } +} diff --git a/x/collection/internal/types/collection.go b/x/collection/internal/types/collection.go new file mode 100644 index 0000000000..78991d3d27 --- /dev/null +++ b/x/collection/internal/types/collection.go @@ -0,0 +1,69 @@ +package types + +import ( + "encoding/json" +) + +type Findable interface { + IDAtIndex(index int) string + Len() int +} +type Collection interface { + GetContractID() string + GetName() string + SetName(name string) + GetBaseImgURI() string + SetBaseImgURI(baseImgURI string) + GetMeta() string + SetMeta(meta string) + String() string +} +type BaseCollection struct { + ContractID string `json:"contract_id"` + Name string `json:"name"` + Meta string `json:"meta"` + BaseImgURI string `json:"base_img_uri"` +} + +func NewCollection(contractID, name, meta, baseImgURI string) Collection { + return &BaseCollection{ + ContractID: contractID, + Name: name, + Meta: meta, + BaseImgURI: baseImgURI, + } +} + +func (c BaseCollection) GetContractID() string { return c.ContractID } +func (c BaseCollection) GetName() string { return c.Name } +func (c *BaseCollection) SetName(name string) { + c.Name = name +} + +func (c BaseCollection) GetMeta() string { return c.Meta } +func (c *BaseCollection) SetMeta(meta string) { + c.Meta = meta +} + +func (c BaseCollection) GetBaseImgURI() string { return c.BaseImgURI } +func (c *BaseCollection) SetBaseImgURI(baseImgURI string) { + c.BaseImgURI = baseImgURI +} + +func (c BaseCollection) String() string { + b, err := json.Marshal(c) + if err != nil { + panic(err) + } + return string(b) +} + +type Collections []Collection + +func (collections Collections) String() string { + b, err := json.Marshal(collections) + if err != nil { + panic(err) + } + return string(b) +} diff --git a/x/collection/internal/types/collection_test.go b/x/collection/internal/types/collection_test.go new file mode 100644 index 0000000000..5267312e61 --- /dev/null +++ b/x/collection/internal/types/collection_test.go @@ -0,0 +1,17 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSetCollection(t *testing.T) { + collection := NewCollection(defaultContractID, defaultName, defaultMeta, defaultBaseImgURI) + collection.SetName("new_name") + collection.SetBaseImgURI("new_uri") + collection.SetMeta("new_meta") + require.Equal(t, "new_name", collection.GetName()) + require.Equal(t, "new_uri", collection.GetBaseImgURI()) + require.Equal(t, "new_meta", collection.GetMeta()) +} diff --git a/x/collection/internal/types/common_test.go b/x/collection/internal/types/common_test.go new file mode 100644 index 0000000000..4cc490147e --- /dev/null +++ b/x/collection/internal/types/common_test.go @@ -0,0 +1,26 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +const ( + defaultName = "name" + defaultContractID = "abcdef01" + defaultBaseImgURI = "base-img-uri" + defaultMeta = "{}" + defaultDecimals = 6 + defaultAmount = 1000 + defaultTokenType = "10000001" + defaultTokenIndex = "00000001" + defaultTokenID1 = defaultTokenType + defaultTokenIndex + defaultTokenID2 = defaultTokenType + "00000002" + defaultTokenTypeFT = "00000001" + defaultTokenIDFT = defaultTokenTypeFT + "00000000" +) + +var ( + addr1 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) +) diff --git a/x/collection/internal/types/encoder.go b/x/collection/internal/types/encoder.go new file mode 100644 index 0000000000..e553a846b0 --- /dev/null +++ b/x/collection/internal/types/encoder.go @@ -0,0 +1,134 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + EncodeRouterKey = "collectionencode" +) + +type MsgRoute string + +const ( + RCreateCollection = MsgRoute("create") + RIssueNFT = MsgRoute("issue_nft") + RIssueFT = MsgRoute("issue_ft") + RMintFT = MsgRoute("mint_ft") + RMintNFT = MsgRoute("mint_nft") + RBurnNFT = MsgRoute("burn_nft") + RBurnFT = MsgRoute("burn_ft") + RBurnFTFrom = MsgRoute("burn_ft_from") + RBurnNFTFrom = MsgRoute("burn_nft_from") + RTransferFT = MsgRoute("transfer_ft") + RTransferNFT = MsgRoute("transfer_nft") + RTransferFTFrom = MsgRoute("transfer_ft_from") + RTransferNFTFrom = MsgRoute("transfer_nft_from") + RModify = MsgRoute("modify") + RApprove = MsgRoute("approve") + RDisapprove = MsgRoute("disapprove") + RGrantPerm = MsgRoute("grant_perm") + RRevokePerm = MsgRoute("revoke_perm") + RAttach = MsgRoute("attach") + RDetach = MsgRoute("detach") + RAttachFrom = MsgRoute("attach_from") + RDetachFrom = MsgRoute("detach_from") +) + +type WasmCustomMsg struct { + Route string `json:"route"` + Data json.RawMessage `json:"data"` +} + +type WasmCustomQuerier struct { + Route string `json:"route"` + Data json.RawMessage `json:"data"` +} + +type QueryCollectionWrapper struct { + CollectionParam CollectionParam `json:"collection_param"` +} + +type QueryBalanceWrapper struct { + BalanceParam BalanceParam `json:"balance_param"` +} + +type QueryTokenTypesWrapper struct { + TokenTypesParam TokenTypesParam `json:"tokentypes_param"` +} + +type QueryTokensWrapper struct { + TokensParam TokensParam `json:"tokens_param"` +} + +type QueryTokenTypeWrapper struct { + TokenTypeParam TokenTypeParam `json:"token_type_param"` +} + +type QueryNFTCountWrapper struct { + TokensParam TokensParam `json:"tokens_param"` +} + +type QueryTotalWrapper struct { + TotalParam TotalParam `json:"total_param"` +} + +type QueryPermsWrapper struct { + PermParam PermParam `json:"perm_param"` +} + +type QueryApprovedWrapper struct { + IsApprovedParam IsApprovedParam `json:"is_approved_param"` +} + +type QueryApproversWrapper struct { + ApproversParam ApproversParam `json:"approvers_param"` +} + +type CollectionParam struct { + ContractID string `json:"contract_id"` +} + +type BalanceParam struct { + ContractID string `json:"contract_id"` + TokenID string `json:"token_id"` + Addr sdk.AccAddress `json:"addr"` +} + +type TokenTypesParam struct { + ContractID string `json:"contract_id"` + TokenID string `json:"token_id"` +} + +type TokensParam struct { + ContractID string `json:"contract_id"` + TokenID string `json:"token_id"` +} + +type TokenTypeParam struct { + ContractID string `json:"contract_id"` + TokenType string `json:"token_type"` +} + +type TotalParam struct { + ContractID string `json:"contract_id"` + TokenID string `json:"token_id"` +} + +type PermParam struct { + ContractID string `json:"contract_id"` + Address sdk.AccAddress `json:"address"` +} + +type ApproversParam struct { + ContractID string `json:"contract_id"` + Proxy sdk.AccAddress `json:"proxy"` +} + +type IsApprovedParam struct { + ContractID string `json:"contract_id"` + Proxy sdk.AccAddress `json:"proxy"` + Approver sdk.AccAddress `json:"approver"` +} diff --git a/x/collection/internal/types/errors.go b/x/collection/internal/types/errors.go new file mode 100644 index 0000000000..8b8d87edcc --- /dev/null +++ b/x/collection/internal/types/errors.go @@ -0,0 +1,68 @@ +package types + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var ( + ErrTokenExist = sdkerrors.Register(ModuleName, 1, "token symbol, token-id already exists") + ErrTokenNotExist = sdkerrors.Register(ModuleName, 2, "token symbol, token-id does not exist") + ErrTokenNotMintable = sdkerrors.Register(ModuleName, 3, "token symbol, token-id is not mintable") + ErrInvalidTokenName = sdkerrors.Register(ModuleName, 4, "token name should not be empty") + ErrInvalidTokenID = sdkerrors.Register(ModuleName, 5, "invalid token id") + ErrInvalidTokenDecimals = sdkerrors.Register(ModuleName, 6, "token decimals should be within the range in 0 ~ 18") + ErrInvalidIssueFT = sdkerrors.Register(ModuleName, 7, "Issuing token with amount[1], decimals[0], mintable[false] prohibited. Issue nft token instead.") + ErrInvalidAmount = sdkerrors.Register(ModuleName, 8, "invalid token amount") + ErrInvalidBaseImgURILength = sdkerrors.Register(ModuleName, 9, "invalid base_img_uri length") + ErrInvalidNameLength = sdkerrors.Register(ModuleName, 10, "invalid name length") + ErrInvalidTokenType = sdkerrors.Register(ModuleName, 11, "invalid token type pattern found") + ErrInvalidTokenIndex = sdkerrors.Register(ModuleName, 12, "invalid token index pattern found") + ErrCollectionExist = sdkerrors.Register(ModuleName, 13, "collection already exists") + ErrCollectionNotExist = sdkerrors.Register(ModuleName, 14, "collection does not exists") + ErrTokenTypeExist = sdkerrors.Register(ModuleName, 15, "token type for contract_id, token-type already exists") + ErrTokenTypeNotExist = sdkerrors.Register(ModuleName, 16, "token type for contract_id, token-type does not exist") + ErrTokenTypeFull = sdkerrors.Register(ModuleName, 17, "all token type for contract_id are occupied") + ErrTokenIndexFull = sdkerrors.Register(ModuleName, 18, "all non-fungible token index for contract_id, token-type are occupied") + ErrTokenIDFull = sdkerrors.Register(ModuleName, 19, "all fungible token-id for contract_id are occupied") + ErrTokenNoPermission = sdkerrors.Register(ModuleName, 20, "account does not have the permission") + ErrTokenAlreadyAChild = sdkerrors.Register(ModuleName, 21, "token is already a child of some other") + ErrTokenNotAChild = sdkerrors.Register(ModuleName, 22, "token is not a child of some other") + ErrTokenNotOwnedBy = sdkerrors.Register(ModuleName, 23, "token is being not owned by") + ErrTokenCannotTransferChildToken = sdkerrors.Register(ModuleName, 24, "cannot transfer a child token") + ErrTokenNotNFT = sdkerrors.Register(ModuleName, 25, "token is not a NFT") + ErrCannotAttachToItself = sdkerrors.Register(ModuleName, 26, "cannot attach token to itself") + ErrCannotAttachToADescendant = sdkerrors.Register(ModuleName, 27, "cannot attach token to a descendant") + ErrApproverProxySame = sdkerrors.Register(ModuleName, 28, "approver is same with proxy") + ErrCollectionNotApproved = sdkerrors.Register(ModuleName, 29, "proxy is not approved on the collection") + ErrCollectionAlreadyApproved = sdkerrors.Register(ModuleName, 30, "proxy is already approved on the collection") + ErrAccountExist = sdkerrors.Register(ModuleName, 31, "account already exists") + ErrAccountNotExist = sdkerrors.Register(ModuleName, 32, "account does not exists") + ErrInsufficientSupply = sdkerrors.Register(ModuleName, 33, "insufficient supply") + ErrInvalidCoin = sdkerrors.Register(ModuleName, 34, "invalid coin") + ErrInvalidChangesFieldCount = sdkerrors.Register(ModuleName, 35, "invalid count of field changes") + ErrEmptyChanges = sdkerrors.Register(ModuleName, 36, "changes is empty") + ErrInvalidChangesField = sdkerrors.Register(ModuleName, 37, "invalid field of changes") + ErrTokenIndexWithoutType = sdkerrors.Register(ModuleName, 38, "There is a token index but no token type") + ErrTokenTypeFTWithoutIndex = sdkerrors.Register(ModuleName, 39, "There is a token type of ft but no token index") + ErrInsufficientToken = sdkerrors.Register(ModuleName, 40, "insufficient token") + ErrDuplicateChangesField = sdkerrors.Register(ModuleName, 41, "duplicate field of changes") + ErrInvalidMetaLength = sdkerrors.Register(ModuleName, 42, "invalid meta length") + ErrSupplyOverflow = sdkerrors.Register(ModuleName, 43, "supply for collection reached maximum") + ErrEmptyField = sdkerrors.Register(ModuleName, 44, "required field cannot be empty") + ErrCompositionTooDeep = sdkerrors.Register(ModuleName, 45, "cannot attach token (composition too deep)") + ErrCompositionTooWide = sdkerrors.Register(ModuleName, 46, "cannot attach token (composition too wide)") + ErrBurnNonRootNFT = sdkerrors.Register(ModuleName, 47, "cannot burn non-root NFTs") + ErrInvalidPermissionAction = sdkerrors.Register(ModuleName, 48, "invalid permission action") +) + +func WrapIfOverflowPanic(r interface{}) error { + if isOverflowPanic(r) { + return ErrSupplyOverflow + } + // unknown panic, bubble up :( + panic(r) +} + +func isOverflowPanic(r interface{}) bool { + return r == "Int overflow" || r == "negative coin amount" +} diff --git a/x/collection/internal/types/events.go b/x/collection/internal/types/events.go new file mode 100644 index 0000000000..bc039fb23b --- /dev/null +++ b/x/collection/internal/types/events.go @@ -0,0 +1,54 @@ +package types + +var ( + EventTypeIssueFT = "issue_ft" + EventTypeIssueNFT = "issue_nft" + EventTypeMintFT = "mint_ft" + EventTypeBurnFT = "burn_ft" + EventTypeMintNFT = "mint_nft" + EventTypeModifyCollection = "modify_collection" + EventTypeModifyTokenType = "modify_token_type" /* #nosec */ + EventTypeModifyToken = "modify_token" + EventTypeGrantPermToken = "grant_perm" + EventTypeRevokePermToken = "revoke_perm" + EventTypeCreateCollection = "create_collection" + EventTypeAttachToken = "attach" /* #nosec */ + EventTypeDetachToken = "detach" /* #nosec */ + EventTypeAttachFrom = "attach_from" + EventTypeDetachFrom = "detach_from" + EventTypeTransfer = "transfer" + EventTypeTransferFT = "transfer_ft" + EventTypeTransferNFT = "transfer_nft" + EventTypeTransferFTFrom = "transfer_ft_from" + EventTypeTransferNFTFrom = "transfer_nft_from" + EventTypeOperationTransferNFT = "operation_transfer_nft" + EventTypeApproveCollection = "approve_collection" + EventTypeDisapproveCollection = "disapprove_collection" + EventTypeBurnNFT = "burn_nft" + EventTypeBurnFTFrom = "burn_ft_from" + EventTypeBurnNFTFrom = "burn_nft_from" + EventTypeOperationBurnNFT = "operation_burn_nft" + EventTypeOperationRootChanged = "operation_root_changed" + + AttributeKeyName = "name" + AttributeKeyMeta = "meta" + AttributeKeyContractID = "contract_id" + AttributeKeyTokenID = "token_id" + AttributeKeyOwner = "owner" + AttributeKeyAmount = "amount" + AttributeKeyDecimals = "decimals" + AttributeKeyBaseImgURI = "base_img_uri" + AttributeKeyMintable = "mintable" + AttributeKeyTokenType = "token_type" + AttributeKeyFrom = "from" + AttributeKeyTo = "to" + AttributeKeyPerm = "perm" + AttributeKeyToTokenID = "to_token_id" + AttributeKeyFromTokenID = "from_token_id" + AttributeKeyApprover = "approver" + AttributeKeyProxy = "proxy" + AttributeKeyOldRoot = "old_root_token_id" + AttributeKeyNewRoot = "new_root_token_id" + + AttributeValueCategory = ModuleName +) diff --git a/x/collection/internal/types/expected_keeper.go b/x/collection/internal/types/expected_keeper.go new file mode 100644 index 0000000000..865b6266c3 --- /dev/null +++ b/x/collection/internal/types/expected_keeper.go @@ -0,0 +1,12 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + auth "github.com/cosmos/cosmos-sdk/x/auth/exported" +) + +type AccountKeeper interface { + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) auth.Account + GetAccount(ctx sdk.Context, addr sdk.AccAddress) auth.Account + SetAccount(ctx sdk.Context, acc auth.Account) +} diff --git a/x/collection/internal/types/key.go b/x/collection/internal/types/key.go new file mode 100644 index 0000000000..8cdac7d26c --- /dev/null +++ b/x/collection/internal/types/key.go @@ -0,0 +1,96 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + ModuleName = "collection" + + StoreKey = ModuleName + RouterKey = ModuleName +) + +var ( + AccountKeyPrefix = []byte{0x00} + CollectionKeyPrefix = []byte{0x01} + SupplyKeyPrefix = []byte{0x02} + TokenKeyPrefix = []byte{0x03} + TokenTypeKeyPrefix = []byte{0x04} + TokenChildToParentKeyPrefix = []byte{0x05} + TokenParentToChildKeyPrefix = []byte{0x06} + CollectionApprovedKeyPrefix = []byte{0x07} + NextTokenTypeFTKeyPrefix = []byte{0x08} + NextTokenTypeNFTKeyPrefix = []byte{0x09} + NextTokenIDNFTKeyPrefix = []byte{0x0a} + PermKeyPrefix = []byte{0x0b} + AccountOwnNFTKeyPrefix = []byte{0x0c} + TokenTypeMintCountPrefix = []byte{0x0d} + TokenTypeBurnCountPrefix = []byte{0x0e} +) + +func AccountKey(contractID string, acc sdk.AccAddress) []byte { + return append(append(AccountKeyPrefix, []byte(contractID)...), acc...) +} + +func SupplyKey(contractID string) []byte { + return append(SupplyKeyPrefix, []byte(contractID)...) +} + +func CollectionKey(contractID string) []byte { + return append(CollectionKeyPrefix, []byte(contractID)...) +} + +func TokenKey(contractID, tokenID string) []byte { + return append(append(TokenKeyPrefix, []byte(contractID)...), []byte(tokenID)...) +} + +func TokenTypeKey(contractID, tokenType string) []byte { + return append(append(TokenTypeKeyPrefix, []byte(contractID)...), []byte(tokenType)...) +} + +func TokenChildToParentKey(contractID, tokenID string) []byte { + return append(append(TokenChildToParentKeyPrefix, []byte(contractID)...), []byte(tokenID)...) +} + +func TokenParentToChildSubKey(contractID, parent string) []byte { + return append(append(TokenParentToChildKeyPrefix, []byte(contractID)...), []byte(parent)...) +} + +func TokenParentToChildKey(contractID, parent, child string) []byte { + return append(append(append(TokenParentToChildKeyPrefix, []byte(contractID)...), []byte(parent)...), []byte(child)...) +} + +func CollectionApprovedKey(contractID string, proxy sdk.AccAddress, approver sdk.AccAddress) []byte { + return append(CollectionApproversKey(contractID, proxy), approver.Bytes()...) +} + +func CollectionApproversKey(contractID string, proxy sdk.AccAddress) []byte { + return append(append(CollectionApprovedKeyPrefix, []byte(contractID)...), proxy.Bytes()...) +} + +func NextTokenTypeFTKey(contractID string) []byte { + return append(NextTokenTypeFTKeyPrefix, []byte(contractID)...) +} +func NextTokenTypeNFTKey(contractID string) []byte { + return append(NextTokenTypeNFTKeyPrefix, []byte(contractID)...) +} +func NextTokenIDNFTKey(contractID, tokenType string) []byte { + return append(append(NextTokenIDNFTKeyPrefix, []byte(contractID)...), []byte(tokenType)...) +} + +func PermKey(contractID string, addr sdk.AccAddress) []byte { + return append(append(PermKeyPrefix, []byte(contractID)...), addr...) +} + +func AccountOwnNFTKey(contractID string, owner sdk.AccAddress, tokenID string) []byte { + return append(append(append(AccountOwnNFTKeyPrefix, []byte(contractID)...), owner.Bytes()...), []byte(tokenID)...) +} + +func TokenTypeMintCount(contractID, tokenType string) []byte { + return append(append(TokenTypeMintCountPrefix, []byte(contractID)...), []byte(tokenType)...) +} + +func TokenTypeBurnCount(contractID, tokenType string) []byte { + return append(append(TokenTypeBurnCountPrefix, []byte(contractID)...), []byte(tokenType)...) +} diff --git a/x/collection/internal/types/key_test.go b/x/collection/internal/types/key_test.go new file mode 100644 index 0000000000..d21a419773 --- /dev/null +++ b/x/collection/internal/types/key_test.go @@ -0,0 +1,19 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +func TestKeys(t *testing.T) { + addr1 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + contractID1 := "abcdef012" + contractID2 := "abcdef013" + + require.NotEqual(t, CollectionApprovedKey(contractID1, addr1, addr2), CollectionApprovedKey(contractID1, addr2, addr1)) + require.NotEqual(t, CollectionApprovedKey(contractID1, addr1, addr2), CollectionApprovedKey(contractID2, addr1, addr2)) +} diff --git a/x/collection/internal/types/modify.go b/x/collection/internal/types/modify.go new file mode 100644 index 0000000000..a0e22b2c6f --- /dev/null +++ b/x/collection/internal/types/modify.go @@ -0,0 +1,29 @@ +package types + +type Change struct { + Field string `json:"field"` + Value string `json:"value"` +} + +func NewChange(field string, value string) Change { + return Change{ + Field: field, + Value: value, + } +} + +type Changes []Change + +func NewChanges(changes ...Change) Changes { + return changes +} + +func NewChangesWithMap(changesMap map[string]string) Changes { + changes := make([]Change, len(changesMap)) + idx := 0 + for k, v := range changesMap { + changes[idx] = Change{Field: k, Value: v} + idx++ + } + return NewChanges(changes...) +} diff --git a/x/collection/internal/types/msgs_collection.go b/x/collection/internal/types/msgs_collection.go new file mode 100644 index 0000000000..9cb1fd55bc --- /dev/null +++ b/x/collection/internal/types/msgs_collection.go @@ -0,0 +1,51 @@ +package types + +import ( + "unicode/utf8" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var _ sdk.Msg = (*MsgCreateCollection)(nil) + +type MsgCreateCollection struct { + Owner sdk.AccAddress `json:"owner"` + Name string `json:"name"` + Meta string `json:"meta"` + BaseImgURI string `json:"base_img_uri"` +} + +func NewMsgCreateCollection(owner sdk.AccAddress, name, meta, baseImgURI string) MsgCreateCollection { + return MsgCreateCollection{ + Owner: owner, + Name: name, + Meta: meta, + BaseImgURI: baseImgURI, + } +} + +func (msg MsgCreateCollection) ValidateBasic() error { + if msg.Owner.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty") + } + if !ValidateName(msg.Name) { + return sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.Name, MaxTokenNameLength, utf8.RuneCountInString(msg.Name)) + } + if !ValidateMeta(msg.Meta) { + return sdkerrors.Wrapf(ErrInvalidMetaLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.Meta, MaxTokenMetaLength, utf8.RuneCountInString(msg.Meta)) + } + if !ValidateBaseImgURI(msg.BaseImgURI) { + return sdkerrors.Wrapf(ErrInvalidBaseImgURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.BaseImgURI, MaxBaseImgURILength, utf8.RuneCountInString(msg.BaseImgURI)) + } + return nil +} + +func (MsgCreateCollection) Route() string { return RouterKey } +func (MsgCreateCollection) Type() string { return "create_collection" } +func (msg MsgCreateCollection) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} +func (msg MsgCreateCollection) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Owner} +} diff --git a/x/collection/internal/types/msgs_compossible.go b/x/collection/internal/types/msgs_compossible.go new file mode 100644 index 0000000000..4b5d13d773 --- /dev/null +++ b/x/collection/internal/types/msgs_compossible.go @@ -0,0 +1,255 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ contract.Msg = (*MsgAttach)(nil) +var _ contract.Msg = (*MsgDetach)(nil) +var _ contract.Msg = (*MsgAttachFrom)(nil) +var _ contract.Msg = (*MsgDetachFrom)(nil) + +type MsgAttach struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + ToTokenID string `json:"to_token_id"` + TokenID string `json:"token_id"` +} + +func NewMsgAttach(from sdk.AccAddress, contractID string, toTokenID string, tokenID string) MsgAttach { + return MsgAttach{ + From: from, + ContractID: contractID, + ToTokenID: toTokenID, + TokenID: tokenID, + } +} + +func (msg MsgAttach) MarshalJSON() ([]byte, error) { + type msgAlias MsgAttach + return json.Marshal(msgAlias(msg)) +} + +func (msg *MsgAttach) UnmarshalJSON(data []byte) error { + type msgAlias *MsgAttach + return json.Unmarshal(data, msgAlias(msg)) +} + +func (MsgAttach) Route() string { return RouterKey } + +func (MsgAttach) Type() string { return "attach" } + +func (msg MsgAttach) GetContractID() string { return msg.ContractID } + +func (msg MsgAttach) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + + if err := ValidateTokenID(msg.ToTokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, msg.ToTokenID) + } + + if err := ValidateTokenID(msg.TokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, msg.TokenID) + } + + if msg.ToTokenID == msg.TokenID { + return sdkerrors.Wrapf(ErrCannotAttachToItself, "TokenID: %s", msg.TokenID) + } + + return nil +} + +func (msg MsgAttach) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgAttach) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +type MsgDetach struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + TokenID string `json:"token_id"` +} + +func NewMsgDetach(from sdk.AccAddress, contractID string, tokenID string) MsgDetach { + return MsgDetach{ + From: from, + ContractID: contractID, + TokenID: tokenID, + } +} + +func (msg MsgDetach) MarshalJSON() ([]byte, error) { + type msgAlias MsgDetach + return json.Marshal(msgAlias(msg)) +} + +func (msg *MsgDetach) UnmarshalJSON(data []byte) error { + type msgAlias *MsgDetach + return json.Unmarshal(data, msgAlias(msg)) +} + +func (MsgDetach) Route() string { return RouterKey } + +func (MsgDetach) Type() string { return "detach" } + +func (msg MsgDetach) GetContractID() string { return msg.ContractID } + +func (msg MsgDetach) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + + if err := ValidateTokenID(msg.TokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, msg.TokenID) + } + + return nil +} + +func (msg MsgDetach) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgDetach) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +type MsgAttachFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + ToTokenID string `json:"to_token_id"` + TokenID string `json:"token_id"` +} + +func NewMsgAttachFrom(proxy sdk.AccAddress, contractID string, from sdk.AccAddress, toTokenID string, tokenID string) MsgAttachFrom { + return MsgAttachFrom{ + Proxy: proxy, + ContractID: contractID, + From: from, + ToTokenID: toTokenID, + TokenID: tokenID, + } +} + +func (msg MsgAttachFrom) MarshalJSON() ([]byte, error) { + type msgAlias MsgAttachFrom + return json.Marshal(msgAlias(msg)) +} + +func (msg *MsgAttachFrom) UnmarshalJSON(data []byte) error { + type msgAlias *MsgAttachFrom + return json.Unmarshal(data, msgAlias(msg)) +} + +func (MsgAttachFrom) Route() string { return RouterKey } + +func (MsgAttachFrom) Type() string { return "attach_from" } + +func (msg MsgAttachFrom) GetContractID() string { return msg.ContractID } + +func (msg MsgAttachFrom) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + if err := ValidateTokenID(msg.ToTokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, msg.ToTokenID) + } + if err := ValidateTokenID(msg.TokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, msg.TokenID) + } + + if msg.ToTokenID == msg.TokenID { + return sdkerrors.Wrapf(ErrCannotAttachToItself, "TokenID: %s", msg.TokenID) + } + + return nil +} + +func (msg MsgAttachFrom) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgAttachFrom) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Proxy} +} + +type MsgDetachFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + TokenID string `json:"token_id"` +} + +func NewMsgDetachFrom(proxy sdk.AccAddress, contractID string, from sdk.AccAddress, tokenID string) MsgDetachFrom { + return MsgDetachFrom{ + Proxy: proxy, + ContractID: contractID, + From: from, + TokenID: tokenID, + } +} + +func (msg MsgDetachFrom) MarshalJSON() ([]byte, error) { + type msgAlias MsgDetachFrom + return json.Marshal(msgAlias(msg)) +} + +func (msg *MsgDetachFrom) UnmarshalJSON(data []byte) error { + type msgAlias *MsgDetachFrom + return json.Unmarshal(data, msgAlias(msg)) +} + +func (MsgDetachFrom) Route() string { return RouterKey } + +func (MsgDetachFrom) Type() string { return "detach_from" } + +func (msg MsgDetachFrom) GetContractID() string { return msg.ContractID } + +func (msg MsgDetachFrom) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + if err := ValidateTokenID(msg.TokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, msg.TokenID) + } + + return nil +} + +func (msg MsgDetachFrom) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgDetachFrom) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Proxy} +} diff --git a/x/collection/internal/types/msgs_issue.go b/x/collection/internal/types/msgs_issue.go new file mode 100644 index 0000000000..91c1ddd497 --- /dev/null +++ b/x/collection/internal/types/msgs_issue.go @@ -0,0 +1,117 @@ +package types + +import ( + "unicode/utf8" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ contract.Msg = (*MsgIssueFT)(nil) + +type MsgIssueFT struct { + Owner sdk.AccAddress `json:"owner"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Name string `json:"name"` + Meta string `json:"meta"` + Amount sdk.Int `json:"amount"` + Mintable bool `json:"mintable"` + Decimals sdk.Int `json:"decimals"` +} + +func NewMsgIssueFT(owner, to sdk.AccAddress, contractID string, name, meta string, amount sdk.Int, decimal sdk.Int, mintable bool) MsgIssueFT { + return MsgIssueFT{ + Owner: owner, + ContractID: contractID, + To: to, + Name: name, + Meta: meta, + Amount: amount, + Mintable: mintable, + Decimals: decimal, + } +} + +func (msg MsgIssueFT) Route() string { return RouterKey } +func (msg MsgIssueFT) Type() string { return "issue_ft" } +func (msg MsgIssueFT) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Owner} } +func (msg MsgIssueFT) GetContractID() string { return msg.ContractID } +func (msg MsgIssueFT) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgIssueFT) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if len(msg.Name) == 0 { + return ErrInvalidTokenName + } + if msg.Owner.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty") + } + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "to address cannot be empty") + } + + if msg.Amount.Equal(sdk.NewInt(1)) && msg.Decimals.Equal(sdk.NewInt(0)) && !msg.Mintable { + return ErrInvalidIssueFT + } + + if msg.Decimals.GT(sdk.NewInt(18)) || msg.Decimals.IsNegative() { + return sdkerrors.Wrapf(ErrInvalidTokenDecimals, "Decimals: %s", msg.Decimals) + } + + if !ValidateName(msg.Name) { + return sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.Name, MaxTokenNameLength, utf8.RuneCountInString(msg.Name)) + } + if !ValidateMeta(msg.Meta) { + return sdkerrors.Wrapf(ErrInvalidMetaLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.Meta, MaxTokenMetaLength, utf8.RuneCountInString(msg.Meta)) + } + return nil +} + +var _ contract.Msg = (*MsgIssueNFT)(nil) + +type MsgIssueNFT struct { + Owner sdk.AccAddress `json:"owner"` + ContractID string `json:"contract_id"` + Name string `json:"name"` + Meta string `json:"meta"` +} + +func NewMsgIssueNFT(owner sdk.AccAddress, contractID, name, meta string) MsgIssueNFT { + return MsgIssueNFT{ + Owner: owner, + ContractID: contractID, + Name: name, + Meta: meta, + } +} + +func (MsgIssueNFT) Route() string { return RouterKey } +func (MsgIssueNFT) Type() string { return "issue_nft" } +func (msg MsgIssueNFT) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Owner} } +func (msg MsgIssueNFT) GetContractID() string { return msg.ContractID } +func (msg MsgIssueNFT) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgIssueNFT) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + if msg.Owner.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty") + } + if !ValidateName(msg.Name) { + return sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.Name, MaxTokenNameLength, utf8.RuneCountInString(msg.Name)) + } + if !ValidateMeta(msg.Meta) { + return sdkerrors.Wrapf(ErrInvalidMetaLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.Meta, MaxTokenMetaLength, utf8.RuneCountInString(msg.Meta)) + } + return nil +} diff --git a/x/collection/internal/types/msgs_mint.go b/x/collection/internal/types/msgs_mint.go new file mode 100644 index 0000000000..108d80996a --- /dev/null +++ b/x/collection/internal/types/msgs_mint.go @@ -0,0 +1,327 @@ +package types + +import ( + "unicode/utf8" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ contract.Msg = (*MsgMintNFT)(nil) + +type MintNFTParam struct { + Name string `json:"name"` + Meta string `json:"meta"` + TokenType string `json:"token_type"` +} + +func NewMintNFTParam(name, meta, tokenType string) MintNFTParam { + return MintNFTParam{ + Name: name, + Meta: meta, + TokenType: tokenType, + } +} + +type MsgMintNFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + MintNFTParams []MintNFTParam `json:"params"` +} + +func NewMsgMintNFT(from sdk.AccAddress, contractID string, to sdk.AccAddress, mintNFTParams ...MintNFTParam) MsgMintNFT { + return MsgMintNFT{ + From: from, + ContractID: contractID, + To: to, + MintNFTParams: mintNFTParams, + } +} + +func (msg MsgMintNFT) Route() string { return RouterKey } +func (msg MsgMintNFT) Type() string { return "mint_nft" } +func (msg MsgMintNFT) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgMintNFT) GetContractID() string { return msg.ContractID } +func (msg MsgMintNFT) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgMintNFT) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from address cannot be empty") + } + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "to address cannot be empty") + } + + if len(msg.MintNFTParams) == 0 { + return sdkerrors.Wrap(ErrEmptyField, "params cannot be empty") + } + for _, mintNFTParam := range msg.MintNFTParams { + if len(mintNFTParam.Name) == 0 { + return ErrInvalidTokenName + } + if !ValidateName(mintNFTParam.Name) { + return sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", mintNFTParam.Name, MaxTokenNameLength, utf8.RuneCountInString(mintNFTParam.Name)) + } + if !ValidateMeta(mintNFTParam.Meta) { + return sdkerrors.Wrapf(ErrInvalidMetaLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", mintNFTParam.Meta, MaxTokenMetaLength, utf8.RuneCountInString(mintNFTParam.Meta)) + } + if err := ValidateTokenTypeNFT(mintNFTParam.TokenType); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, err.Error()) + } + } + + return nil +} + +var _ contract.Msg = (*MsgBurnNFT)(nil) + +type MsgBurnNFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + TokenIDs []string `json:"token_ids"` +} + +func NewMsgBurnNFT(from sdk.AccAddress, contractID string, tokenIDs ...string) MsgBurnNFT { + return MsgBurnNFT{ + From: from, + ContractID: contractID, + TokenIDs: tokenIDs, + } +} + +func (msg MsgBurnNFT) Route() string { return RouterKey } +func (msg MsgBurnNFT) Type() string { return "burn_nft" } +func (msg MsgBurnNFT) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgBurnNFT) GetContractID() string { return msg.ContractID } +func (msg MsgBurnNFT) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgBurnNFT) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty") + } + + if len(msg.TokenIDs) == 0 { + return sdkerrors.Wrap(ErrEmptyField, "token_ids cannot be empty") + } + for _, tokenID := range msg.TokenIDs { + if err := ValidateTokenID(tokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, err.Error()) + } + if err := ValidateTokenTypeNFT(tokenID[:TokenTypeLength]); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, err.Error()) + } + } + + return nil +} + +var _ contract.Msg = (*MsgBurnNFTFrom)(nil) + +type MsgBurnNFTFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + TokenIDs []string `json:"token_ids"` +} + +func NewMsgBurnNFTFrom(proxy sdk.AccAddress, contractID string, from sdk.AccAddress, tokenIDs ...string) MsgBurnNFTFrom { + return MsgBurnNFTFrom{ + Proxy: proxy, + ContractID: contractID, + From: from, + TokenIDs: tokenIDs, + } +} + +func (msg MsgBurnNFTFrom) Route() string { return RouterKey } +func (msg MsgBurnNFTFrom) Type() string { return "burn_nft_from" } +func (msg MsgBurnNFTFrom) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Proxy} } +func (msg MsgBurnNFTFrom) GetContractID() string { return msg.ContractID } +func (msg MsgBurnNFTFrom) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgBurnNFTFrom) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + if msg.Proxy.Equals(msg.From) { + return sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", msg.Proxy.String()) + } + + if len(msg.TokenIDs) == 0 { + return sdkerrors.Wrap(ErrEmptyField, "token_ids cannot be empty") + } + for _, tokenID := range msg.TokenIDs { + if err := ValidateTokenID(tokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, err.Error()) + } + if err := ValidateTokenTypeNFT(tokenID[:TokenTypeLength]); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, err.Error()) + } + } + return nil +} + +var _ contract.Msg = (*MsgMintFT)(nil) + +type MsgMintFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Amount Coins `json:"amount"` +} + +func NewMsgMintFT(from sdk.AccAddress, contractID string, to sdk.AccAddress, amount ...Coin) MsgMintFT { + return MsgMintFT{ + From: from, + ContractID: contractID, + To: to, + Amount: amount, + } +} +func (MsgMintFT) Route() string { return RouterKey } +func (MsgMintFT) Type() string { return "mint_ft" } +func (msg MsgMintFT) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgMintFT) GetContractID() string { return msg.ContractID } +func (msg MsgMintFT) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgMintFT) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + for _, tokenID := range msg.Amount { + if err := ValidateDenom(tokenID.Denom); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, "invalid token id") + } + } + + if !msg.Amount.IsValid() { + return sdkerrors.Wrap(ErrInvalidAmount, msg.Amount.String()) + } + + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from address cannot be empty") + } + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "to address cannot be empty") + } + + return nil +} + +var _ contract.Msg = (*MsgBurnFT)(nil) + +type MsgBurnFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + Amount Coins `json:"amount"` +} + +func NewMsgBurnFT(from sdk.AccAddress, contractID string, amount ...Coin) MsgBurnFT { + return MsgBurnFT{ + From: from, + ContractID: contractID, + Amount: amount, + } +} +func (MsgBurnFT) Route() string { return RouterKey } +func (MsgBurnFT) Type() string { return "burn_ft" } +func (msg MsgBurnFT) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgBurnFT) GetContractID() string { return msg.ContractID } +func (msg MsgBurnFT) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgBurnFT) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + for _, tokenID := range msg.Amount { + if err := ValidateDenom(tokenID.Denom); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, "invalid token id") + } + } + + if !msg.Amount.IsValid() { + return sdkerrors.Wrap(ErrInvalidAmount, msg.Amount.String()) + } + + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from address cannot be empty") + } + return nil +} + +var _ contract.Msg = (*MsgBurnFTFrom)(nil) + +type MsgBurnFTFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + Amount Coins `json:"amount"` +} + +func NewMsgBurnFTFrom(proxy sdk.AccAddress, contractID string, from sdk.AccAddress, amount ...Coin) MsgBurnFTFrom { + return MsgBurnFTFrom{ + Proxy: proxy, + ContractID: contractID, + From: from, + Amount: amount, + } +} + +func (MsgBurnFTFrom) Route() string { return RouterKey } +func (MsgBurnFTFrom) Type() string { return "burn_ft_from" } +func (msg MsgBurnFTFrom) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Proxy} } +func (msg MsgBurnFTFrom) GetContractID() string { return msg.ContractID } +func (msg MsgBurnFTFrom) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgBurnFTFrom) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + if msg.Proxy.Equals(msg.From) { + return sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", msg.Proxy.String()) + } + for _, tokenID := range msg.Amount { + if err := ValidateDenom(tokenID.Denom); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, "invalid token id") + } + } + + if !msg.Amount.IsValid() { + return sdkerrors.Wrap(ErrInvalidAmount, msg.Amount.String()) + } + return nil +} diff --git a/x/collection/internal/types/msgs_modify.go b/x/collection/internal/types/msgs_modify.go new file mode 100644 index 0000000000..487fd7ea29 --- /dev/null +++ b/x/collection/internal/types/msgs_modify.go @@ -0,0 +1,67 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ contract.Msg = (*MsgModify)(nil) + +type MsgModify struct { + Owner sdk.AccAddress `json:"owner"` + ContractID string `json:"contract_id"` + TokenType string `json:"token_type"` + TokenIndex string `json:"token_index"` + Changes Changes `json:"changes"` +} + +func NewMsgModify(owner sdk.AccAddress, contractID, tokenType, tokenIndex string, changes Changes) MsgModify { + return MsgModify{ + Owner: owner, + ContractID: contractID, + TokenType: tokenType, + TokenIndex: tokenIndex, + Changes: changes, + } +} + +func (msg MsgModify) Route() string { return RouterKey } +func (msg MsgModify) Type() string { return "modify_token" } +func (msg MsgModify) GetContractID() string { return msg.ContractID } +func (msg MsgModify) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} +func (msg MsgModify) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Owner} } + +func (msg MsgModify) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + if msg.Owner.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty") + } + + if msg.TokenType != "" { + if err := ValidateTokenType(msg.TokenType); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenType, msg.TokenType) + } + if ValidateTokenTypeFT(msg.TokenType) == nil && msg.TokenIndex == "" { + return sdkerrors.Wrap(ErrTokenTypeFTWithoutIndex, msg.TokenType) + } + } + if msg.TokenIndex != "" && ValidateTokenIndex(msg.TokenIndex) != nil { + return sdkerrors.Wrap(ErrInvalidTokenIndex, msg.TokenIndex) + } + + validator := NewChangesValidator() + if err := validator.SetMode(msg.TokenType, msg.TokenIndex); err != nil { + return err + } + if err := validator.Validate(msg.Changes); err != nil { + return err + } + + return nil +} diff --git a/x/collection/internal/types/msgs_modify_test.go b/x/collection/internal/types/msgs_modify_test.go new file mode 100644 index 0000000000..44d87fe162 --- /dev/null +++ b/x/collection/internal/types/msgs_modify_test.go @@ -0,0 +1,200 @@ +package types + +import ( + "testing" + "unicode/utf8" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +const ( + ModifyMsgType = "modify_token" +) + +func TestNewMsgModify(t *testing.T) { + msg := AMsgModify().Build() + + require.Equal(t, ModifyMsgType, msg.Type()) + require.Equal(t, ModuleName, msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, msg.Owner, msg.GetSigners()[0]) +} + +func TestMarshalMsgModify(t *testing.T) { + // Given + msg := AMsgModify().Build() + + // When marshal and unmarshal it + msg2 := MsgModify{} + err := ModuleCdc.UnmarshalJSON(msg.GetSignBytes(), &msg2) + require.NoError(t, err) + + // Then they are equal + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.TokenIndex, msg2.TokenIndex) + require.Equal(t, msg.TokenType, msg2.TokenType) + require.Equal(t, msg.Changes, msg2.Changes) + require.Equal(t, msg.Owner, msg2.Owner) +} + +func TestMsgModify_ValidateBasic(t *testing.T) { + t.Log("normal case") + { + msg := AMsgModify().Build() + require.NoError(t, msg.ValidateBasic()) + } + t.Log("empty contractID found") + { + msg := AMsgModify().Contract("").Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(contract.ErrInvalidContractID, "ContractID: ").Error()) + } + t.Log("empty owner") + { + msg := AMsgModify().Owner(nil).Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty").Error()) + } + t.Log("invalid contractID found") + { + msg := AMsgModify().Contract("0123456789001234567890").Build() + require.EqualError(t, + msg.ValidateBasic(), + sdkerrors.Wrapf(contract.ErrInvalidContractID, "ContractID: %s", msg.ContractID).Error()) + } + t.Log("img uri too long") + { + msg := AMsgModify().Changes(NewChangesWithMap(map[string]string{"base_img_uri": length1001String})). + Build() + + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrInvalidBaseImgURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", length1001String, MaxBaseImgURILength, utf8.RuneCountInString(length1001String)).Error()) + } + t.Log("name too long") + { + msg := AMsgModify().Changes(NewChangesWithMap(map[string]string{"name": length1001String})).Build() + + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", length1001String, MaxTokenNameLength, utf8.RuneCountInString(length1001String)).Error()) + } + t.Log("invalid changes field") + { + msg := AMsgModify().Changes(NewChangesWithMap(map[string]string{"invalid_field": "val"})).Build() + + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidChangesField, "Field: invalid_field").Error()) + } + t.Log("no token uri field") + { + msg := AMsgModify().Changes(NewChangesWithMap(map[string]string{"name": "new_name"})).Build() + require.NoError(t, msg.ValidateBasic()) + } + t.Log("Test with changes more than max") + { + // Given msg with changes more than max + changeList := make([]Change, MaxChangeFieldsCount+1) + msg := AMsgModify().Changes(changeList).Build() + + // When validate basic, Then error is occurred + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrInvalidChangesFieldCount, "You can not change fields more than [%d] at once, current count: [%d]", MaxChangeFieldsCount, len(changeList)).Error()) + } + t.Log("Test with nft token type") + { + msg := AMsgModify().TokenType(defaultTokenType).Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidChangesField, "Field: base_img_uri").Error()) + + msg = AMsgModify().TokenType(defaultTokenType). + Changes(NewChangesWithMap(map[string]string{"name": "new_name"})). + Build() + require.NoError(t, msg.ValidateBasic()) + } + t.Log("Test with nft token type and index") + { + msg := AMsgModify().TokenType(defaultTokenType).TokenIndex(defaultTokenIndex).Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidChangesField, "Field: base_img_uri").Error()) + + msg = AMsgModify().TokenType(defaultTokenType).TokenIndex(defaultTokenIndex). + Changes(NewChangesWithMap(map[string]string{"name": "new_name"})). + Build() + require.NoError(t, msg.ValidateBasic()) + } + t.Log("Test with ft token type and index") + { + msg := AMsgModify().TokenType(defaultTokenTypeFT).TokenIndex(defaultTokenIndex).Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidChangesField, "Field: base_img_uri").Error()) + + msg = AMsgModify().TokenType(defaultTokenTypeFT).TokenIndex(defaultTokenIndex). + Changes(NewChangesWithMap(map[string]string{"name": "new_name"})). + Build() + require.NoError(t, msg.ValidateBasic()) + } + t.Log("Test with ft token type and not index") + { + msg := AMsgModify().TokenType(defaultTokenTypeFT).Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrTokenTypeFTWithoutIndex, defaultTokenTypeFT).Error()) + } + t.Log("Test with invalid token type") + { + invalidTokenType := "010101" + msg := AMsgModify().TokenType(invalidTokenType).Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenType, invalidTokenType).Error()) + } + t.Log("Test with invalid token index") + { + invalidTokenIndex := "010101" + msg := AMsgModify().TokenIndex(invalidTokenIndex).Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenIndex, invalidTokenIndex).Error()) + } + t.Log("Test with token index not token type") + { + msg := AMsgModify().TokenIndex(defaultTokenIndex).Build() + require.EqualError(t, msg.ValidateBasic(), ErrTokenIndexWithoutType.Error()) + } +} + +func AMsgModify() *MsgModifyBuilder { + return &MsgModifyBuilder{ + msgModify: NewMsgModify( + sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()), + defaultContractID, + "", + "", + NewChangesWithMap(map[string]string{ + "name": "new_name", + "base_img_uri": "new_base_img_uri", + }), + ), + } +} + +type MsgModifyBuilder struct { + msgModify MsgModify +} + +func (b *MsgModifyBuilder) Build() MsgModify { + return b.msgModify +} + +func (b *MsgModifyBuilder) Owner(owner sdk.AccAddress) *MsgModifyBuilder { + b.msgModify.Owner = owner + return b +} + +func (b *MsgModifyBuilder) Contract(contractID string) *MsgModifyBuilder { + b.msgModify.ContractID = contractID + return b +} + +func (b *MsgModifyBuilder) TokenType(tokenType string) *MsgModifyBuilder { + b.msgModify.TokenType = tokenType + return b +} + +func (b *MsgModifyBuilder) TokenIndex(tokenIndex string) *MsgModifyBuilder { + b.msgModify.TokenIndex = tokenIndex + return b +} + +func (b *MsgModifyBuilder) Changes(changes Changes) *MsgModifyBuilder { + b.msgModify.Changes = changes + return b +} diff --git a/x/collection/internal/types/msgs_perm.go b/x/collection/internal/types/msgs_perm.go new file mode 100644 index 0000000000..2a641d3de7 --- /dev/null +++ b/x/collection/internal/types/msgs_perm.go @@ -0,0 +1,95 @@ +package types + +import ( + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ sdk.Msg = (*MsgGrantPermission)(nil) + +func NewMsgGrantPermission(from sdk.AccAddress, contractID string, to sdk.AccAddress, perm Permission) MsgGrantPermission { + return MsgGrantPermission{ + From: from, + ContractID: contractID, + To: to, + Permission: perm, + } +} + +type MsgGrantPermission struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Permission Permission `json:"permission"` +} + +func (MsgGrantPermission) Route() string { return RouterKey } +func (MsgGrantPermission) Type() string { return "grant_perm" } +func (msg MsgGrantPermission) GetContractID() string { return msg.ContractID } +func (msg MsgGrantPermission) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgGrantPermission) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgGrantPermission) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.From.Empty() || msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "addresses cannot be empty") + } + + if msg.From.Equals(msg.To) { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from, to address can not be the same") + } + + return validateAction(msg.Permission.String(), MintAction, BurnAction, IssueAction, ModifyAction) +} + +var _ sdk.Msg = (*MsgRevokePermission)(nil) + +func NewMsgRevokePermission(from sdk.AccAddress, contractID string, perm Permission) MsgRevokePermission { + return MsgRevokePermission{ + From: from, + ContractID: contractID, + Permission: perm, + } +} + +type MsgRevokePermission struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + Permission Permission `json:"permission"` +} + +func (MsgRevokePermission) Route() string { return RouterKey } +func (MsgRevokePermission) Type() string { return "revoke_perm" } +func (msg MsgRevokePermission) GetContractID() string { return msg.ContractID } +func (msg MsgRevokePermission) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgRevokePermission) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgRevokePermission) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "addresses cannot be empty") + } + + return validateAction(msg.Permission.String(), MintAction, BurnAction, IssueAction, ModifyAction) +} +func validateAction(action string, actions ...string) error { + for _, a := range actions { + if action == a { + return nil + } + } + return sdkerrors.Wrap(ErrInvalidPermissionAction, + fmt.Sprintf("permission action should be one of [%s]", strings.Join(actions, ","))) +} diff --git a/x/collection/internal/types/msgs_proxy.go b/x/collection/internal/types/msgs_proxy.go new file mode 100644 index 0000000000..04bc39c451 --- /dev/null +++ b/x/collection/internal/types/msgs_proxy.go @@ -0,0 +1,91 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ contract.Msg = (*MsgApprove)(nil) + +type MsgApprove struct { + Approver sdk.AccAddress `json:"approver"` + ContractID string `json:"contract_id"` + Proxy sdk.AccAddress `json:"proxy"` +} + +func NewMsgApprove(approver sdk.AccAddress, contractID string, proxy sdk.AccAddress) MsgApprove { + return MsgApprove{ + Approver: approver, + ContractID: contractID, + Proxy: proxy, + } +} + +func (msg MsgApprove) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return nil + } + if msg.Approver.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Approver cannot be empty") + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.Approver.Equals(msg.Proxy) { + return sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", msg.Approver.String()) + } + return nil +} + +func (MsgApprove) Route() string { return RouterKey } +func (MsgApprove) Type() string { return "approve_collection" } +func (msg MsgApprove) GetContractID() string { return msg.ContractID } +func (msg MsgApprove) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Approver} +} +func (msg MsgApprove) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +var _ contract.Msg = (*MsgDisapprove)(nil) + +type MsgDisapprove struct { + Approver sdk.AccAddress `json:"approver"` + ContractID string `json:"contract_id"` + Proxy sdk.AccAddress `json:"proxy"` +} + +func NewMsgDisapprove(approver sdk.AccAddress, contractID string, proxy sdk.AccAddress) MsgDisapprove { + return MsgDisapprove{ + Approver: approver, + ContractID: contractID, + Proxy: proxy, + } +} + +func (msg MsgDisapprove) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.Approver.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Approver cannot be empty") + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.Approver.Equals(msg.Proxy) { + return sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", msg.Approver.String()) + } + return nil +} + +func (MsgDisapprove) Route() string { return RouterKey } +func (MsgDisapprove) Type() string { return "disapprove_collection" } +func (msg MsgDisapprove) GetContractID() string { return msg.ContractID } +func (msg MsgDisapprove) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Approver} +} +func (msg MsgDisapprove) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} diff --git a/x/collection/internal/types/msgs_test.go b/x/collection/internal/types/msgs_test.go new file mode 100644 index 0000000000..da54cb8d22 --- /dev/null +++ b/x/collection/internal/types/msgs_test.go @@ -0,0 +1,615 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +func TestMsgBasics(t *testing.T) { + cdc := ModuleCdc + + { + msg := NewMsgIssueFT(addr1, addr1, defaultContractID, defaultName, defaultMeta, sdk.NewInt(1), sdk.NewInt(8), true) + require.Equal(t, "issue_ft", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgIssueFT{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Name, msg2.Name) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.Owner, msg2.Owner) + require.Equal(t, msg.Amount, msg.Amount) + require.Equal(t, msg.Decimals, msg2.Decimals) + require.Equal(t, msg.Mintable, msg2.Mintable) + } + { + msg := NewMsgIssueNFT(addr1, defaultContractID, defaultName, defaultMeta) + require.Equal(t, "issue_nft", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgIssueNFT{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.Owner, msg2.Owner) + require.Equal(t, msg.Name, msg2.Name) + } + { + msg := NewMsgMintFT(addr1, defaultContractID, addr1, OneCoin(defaultTokenIDFT)) + require.Equal(t, "mint_ft", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgMintFT{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.Amount, msg2.Amount) + + msg3 := NewMsgMintFT(addr1, defaultContractID, addr1, Coin{"x000000100000000", sdk.NewInt(1)}) + require.EqualError(t, msg3.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "invalid token id").Error()) + msg4 := NewMsgMintFT(addr1, defaultContractID, addr1, Coin{"vf12e00000000", sdk.NewInt(1)}) + require.EqualError(t, msg4.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "invalid token id").Error()) + msg5 := NewMsgMintFT(addr1, defaultContractID, addr1, Coin{"!000000100000000", sdk.NewInt(1)}) + require.EqualError(t, msg5.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "invalid token id").Error()) + } + { + param := NewMintNFTParam(defaultName, defaultMeta, defaultTokenType) + msg := NewMsgMintNFT(addr1, defaultContractID, addr1, param) + require.Equal(t, "mint_nft", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgMintNFT{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.MintNFTParams[0].Name, msg2.MintNFTParams[0].Name) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.MintNFTParams[0].TokenType, msg2.MintNFTParams[0].TokenType) + + falseParam := NewMintNFTParam("", defaultMeta, defaultTokenType) + msg3 := NewMsgMintNFT(addr1, defaultContractID, addr1, falseParam) + require.Error(t, msg3.ValidateBasic()) + + falseParam = NewMintNFTParam(defaultName, defaultMeta, "abc") + msg4 := NewMsgMintNFT(addr1, defaultContractID, addr1, falseParam) + require.Error(t, msg4.ValidateBasic()) + + msg5 := NewMsgMintNFT(addr1, defaultContractID, addr1) + require.Error(t, msg5.ValidateBasic()) + } + { + msg := NewMsgBurnNFT(addr1, defaultContractID, defaultTokenID1) + require.Equal(t, "burn_nft", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgBurnNFT{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.TokenIDs, msg2.TokenIDs) + + msg3 := NewMsgBurnNFT(addr1, defaultContractID) + require.Error(t, msg3.ValidateBasic()) + } + { + addr2 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + msg := NewMsgGrantPermission(addr1, defaultContractID, addr2, NewIssuePermission()) + require.Equal(t, "grant_perm", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgGrantPermission{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.Permission, msg2.Permission) + } + + { + msg := NewMsgRevokePermission(addr1, defaultContractID, NewIssuePermission()) + require.Equal(t, "revoke_perm", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgRevokePermission{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.Permission, msg2.Permission) + } + { + msg := NewMsgTransferFT(addr1, defaultContractID, addr2, NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.Equal(t, "transfer_ft", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgTransferFT{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.Amount, msg2.Amount) + } + + { + msg := NewMsgTransferFT(nil, defaultContractID, addr2, NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgTransferFT(addr1, defaultContractID, nil, NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty").Error()) + + msg = NewMsgTransferFT(addr1, "", addr2, NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(contract.ErrInvalidContractID, "ContractID: ").Error()) + + require.Panics(t, func() { + NewMsgTransferFT(addr1, defaultContractID, addr2, NewCoin("1", sdk.NewInt(defaultAmount))) + }, "") + + require.Panics(t, func() { + NewMsgTransferFT(addr1, defaultContractID, addr2, NewCoin("1", sdk.NewInt(-1*defaultAmount))) + }, "") + } + + { + msg := NewMsgTransferNFT(addr1, defaultContractID, addr2, defaultTokenID1) + require.Equal(t, "transfer_nft", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgTransferNFT{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.TokenIDs, msg2.TokenIDs) + } + + { + msg := NewMsgTransferNFT(nil, defaultContractID, addr2, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgTransferNFT(addr1, defaultContractID, nil, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty").Error()) + + msg = NewMsgTransferNFT(addr1, "", addr2, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(contract.ErrInvalidContractID, "ContractID: ").Error()) + + msg = NewMsgTransferNFT(addr1, defaultContractID, addr2, "1") + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "symbol [1] mismatched to [^[a-f0-9]{16}$]").Error()) + + msg = NewMsgTransferNFT(addr1, defaultContractID, addr2) + require.Error(t, msg.ValidateBasic()) + } + + { + msg := NewMsgTransferFTFrom(addr1, defaultContractID, addr2, addr2, NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.Equal(t, "transfer_ft_from", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgTransferFTFrom{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.Amount, msg2.Amount) + } + + { + msg := NewMsgTransferFTFrom(nil, defaultContractID, addr2, addr2, NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty").Error()) + + msg = NewMsgTransferFTFrom(addr1, defaultContractID, nil, addr2, NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgTransferFTFrom(addr1, defaultContractID, addr2, nil, NewCoin(defaultTokenIDFT, sdk.NewInt(defaultAmount))) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty").Error()) + + require.Panics(t, func() { + NewMsgTransferFT(addr1, defaultContractID, addr2, NewCoin("1", sdk.NewInt(defaultAmount))) + }, "") + + require.Panics(t, func() { + NewMsgTransferFT(addr1, defaultContractID, addr2, NewCoin("1", sdk.NewInt(-1*defaultAmount))) + }, "") + } + // nolint:dupl + { + msg := NewMsgTransferNFTFrom(addr1, defaultContractID, addr2, addr2, defaultTokenID1) + require.Equal(t, "transfer_nft_from", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgTransferNFTFrom{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.TokenIDs, msg2.TokenIDs) + } + + { + msg := NewMsgTransferNFTFrom(nil, defaultContractID, addr2, addr2, defaultTokenIDFT) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty").Error()) + + msg = NewMsgTransferNFTFrom(addr1, defaultContractID, nil, addr2, defaultTokenIDFT) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgTransferNFTFrom(addr1, defaultContractID, addr2, nil, defaultTokenIDFT) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty").Error()) + + msg = NewMsgTransferNFTFrom(addr1, defaultContractID, addr2, addr2, "1") + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "symbol [1] mismatched to [^[a-f0-9]{16}$]").Error()) + + msg = NewMsgTransferNFTFrom(addr1, defaultContractID, addr2, addr2) + require.Error(t, msg.ValidateBasic()) + } + + { + msg := NewMsgAttach(addr1, defaultContractID, defaultTokenID1, defaultTokenID2) + require.Equal(t, "attach", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgAttach{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.ToTokenID, msg2.ToTokenID) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.TokenID, msg2.TokenID) + } + + { + msg := NewMsgAttach(nil, defaultContractID, defaultTokenID1, defaultTokenID2) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgAttach(addr1, "s", defaultTokenID1, defaultTokenID2) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(contract.ErrInvalidContractID, "ContractID: s").Error()) + + msg = NewMsgAttach(addr1, defaultContractID, "1", defaultTokenID2) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "1").Error()) + + msg = NewMsgAttach(addr1, defaultContractID, defaultTokenID1, "2") + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "2").Error()) + + msg = NewMsgAttach(addr1, defaultContractID, defaultTokenID1, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrCannotAttachToItself, "TokenID: %s", defaultTokenID1).Error()) + } + + { + msg := NewMsgDetach(addr1, defaultContractID, defaultTokenID1) + require.Equal(t, "detach", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgDetach{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.TokenID, msg2.TokenID) + } + + { + msg := NewMsgDetach(nil, defaultContractID, "item0001") + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgDetach(addr1, "s", "item0001") + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(contract.ErrInvalidContractID, "ContractID: s").Error()) + + msg = NewMsgDetach(addr1, defaultContractID, "1") + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "1").Error()) + } + // nolint:dupl + { + msg := NewMsgAttachFrom(addr1, defaultContractID, addr2, defaultTokenID1, defaultTokenID2) + require.Equal(t, "attach_from", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgAttachFrom{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.ToTokenID, msg2.ToTokenID) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.TokenID, msg2.TokenID) + } + + { + msg := NewMsgAttachFrom(nil, defaultContractID, addr2, defaultTokenID1, defaultTokenID2) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty").Error()) + + msg = NewMsgAttachFrom(addr1, defaultContractID, nil, defaultTokenID1, defaultTokenID2) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgAttachFrom(addr1, "s", addr2, defaultTokenID1, defaultTokenID2) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(contract.ErrInvalidContractID, "ContractID: s").Error()) + + msg = NewMsgAttachFrom(addr1, defaultContractID, addr2, "1", defaultTokenID2) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "1").Error()) + + msg = NewMsgAttachFrom(addr1, defaultContractID, addr2, defaultTokenID1, "2") + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "2").Error()) + + msg = NewMsgAttachFrom(addr1, defaultContractID, addr2, defaultTokenID1, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrCannotAttachToItself, "TokenID: %s", defaultTokenID1).Error()) + } + // nolint:dupl + { + msg := NewMsgDetachFrom(addr1, defaultContractID, addr2, defaultTokenID1) + require.Equal(t, "detach_from", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgDetachFrom{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.TokenID, msg2.TokenID) + } + + { + msg := NewMsgDetachFrom(nil, defaultContractID, addr2, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty").Error()) + + msg = NewMsgDetachFrom(addr1, defaultContractID, nil, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgDetachFrom(addr1, "s", addr2, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(contract.ErrInvalidContractID, "ContractID: s").Error()) + + msg = NewMsgDetachFrom(addr1, defaultContractID, addr2, "1") + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "1").Error()) + } + + { + msg := NewMsgApprove(addr1, defaultContractID, addr2) + require.Equal(t, "approve_collection", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgApprove{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.Approver, msg2.Approver) + require.Equal(t, msg.ContractID, msg2.ContractID) + } + + { + msg := NewMsgDisapprove(addr1, defaultContractID, addr2) + require.Equal(t, "disapprove_collection", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgDisapprove{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.Approver, msg2.Approver) + require.Equal(t, msg.ContractID, msg2.ContractID) + } + + { + msg := NewMsgBurnFT(addr1, defaultContractID, OneCoin(defaultTokenIDFT)) + require.Equal(t, "burn_ft", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgBurnFT{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.Amount, msg2.Amount) + } + { + msg := NewMsgBurnFT(addr1, defaultContractID, Coin{"vf12e00000000", sdk.NewInt(1)}) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "invalid token id").Error()) + msg = NewMsgBurnFT(addr1, defaultContractID, Coin{defaultTokenIDFT, sdk.NewInt(-1)}) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidAmount, "-1:0000000100000000").Error()) + } + { + msg := NewMsgBurnFTFrom(addr1, defaultContractID, addr2, OneCoin(defaultTokenIDFT)) + require.Equal(t, "burn_ft_from", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgBurnFTFrom{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.Amount, msg2.Amount) + } + + { + msg := NewMsgBurnFTFrom(addr1, defaultContractID, addr1, OneCoin(defaultTokenIDFT)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", addr1.String()).Error()) + + msg = NewMsgBurnFTFrom(nil, defaultContractID, addr1, OneCoin(defaultTokenIDFT)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty").Error()) + + msg = NewMsgBurnFTFrom(addr1, defaultContractID, nil, OneCoin(defaultTokenIDFT)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgBurnFTFrom(addr1, defaultContractID, addr2, Coin{"vf12e00000000", sdk.NewInt(1)}) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenID, "invalid token id").Error()) + msg = NewMsgBurnFTFrom(addr1, defaultContractID, addr2, Coin{defaultTokenIDFT, sdk.NewInt(-1)}) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidAmount, "-1:0000000100000000").Error()) + } + + { + msg := NewMsgBurnNFTFrom(addr1, defaultContractID, addr2, defaultTokenID1) + require.Equal(t, "burn_nft_from", msg.Type()) + require.Equal(t, "collection", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgBurnNFTFrom{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.TokenIDs, msg2.TokenIDs) + } + + { + msg := NewMsgBurnNFTFrom(addr1, defaultContractID, addr1, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", addr1.String()).Error()) + + msg = NewMsgBurnNFTFrom(nil, defaultContractID, addr1, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty").Error()) + + msg = NewMsgBurnNFTFrom(addr1, defaultContractID, nil, defaultTokenID1) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgBurnNFTFrom(addr1, defaultContractID, addr1) + require.Error(t, msg.ValidateBasic()) + } +} diff --git a/x/collection/internal/types/msgs_transfer.go b/x/collection/internal/types/msgs_transfer.go new file mode 100644 index 0000000000..2fe27d6c30 --- /dev/null +++ b/x/collection/internal/types/msgs_transfer.go @@ -0,0 +1,287 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ contract.Msg = (*MsgTransferFT)(nil) +var _ contract.Msg = (*MsgTransferNFT)(nil) +var _ contract.Msg = (*MsgTransferFTFrom)(nil) +var _ contract.Msg = (*MsgTransferNFTFrom)(nil) + +var _ json.Marshaler = (*MsgTransferFT)(nil) +var _ json.Unmarshaler = (*MsgTransferFT)(nil) +var _ json.Marshaler = (*MsgTransferNFT)(nil) +var _ json.Unmarshaler = (*MsgTransferNFT)(nil) +var _ json.Marshaler = (*MsgTransferFTFrom)(nil) +var _ json.Unmarshaler = (*MsgTransferFTFrom)(nil) +var _ json.Marshaler = (*MsgTransferNFTFrom)(nil) +var _ json.Unmarshaler = (*MsgTransferNFTFrom)(nil) + +type MsgTransferFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Amount Coins `json:"amount"` +} + +func NewMsgTransferFT(from sdk.AccAddress, contractID string, to sdk.AccAddress, amount ...Coin) MsgTransferFT { + return MsgTransferFT{ + From: from, + ContractID: contractID, + To: to, + Amount: amount, + } +} + +func (msg MsgTransferFT) MarshalJSON() ([]byte, error) { + type msgAlias MsgTransferFT + return json.Marshal(msgAlias(msg)) +} + +func (msg *MsgTransferFT) UnmarshalJSON(data []byte) error { + type msgAlias *MsgTransferFT + return json.Unmarshal(data, msgAlias(msg)) +} + +func (MsgTransferFT) Route() string { return RouterKey } + +func (MsgTransferFT) Type() string { return "transfer_ft" } + +func (msg MsgTransferFT) GetContractID() string { return msg.ContractID } + +func (msg MsgTransferFT) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty") + } + + for _, tokenID := range msg.Amount { + if err := ValidateDenom(tokenID.Denom); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, "invalid token id") + } + } + + if !msg.Amount.IsValid() { + return sdkerrors.Wrap(ErrInvalidAmount, "invalid amount") + } + return nil +} + +func (msg MsgTransferFT) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgTransferFT) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +type MsgTransferNFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + TokenIDs []string `json:"token_ids"` +} + +func NewMsgTransferNFT(from sdk.AccAddress, contractID string, to sdk.AccAddress, tokenIDs ...string) MsgTransferNFT { + return MsgTransferNFT{ + From: from, + ContractID: contractID, + To: to, + TokenIDs: tokenIDs, + } +} + +func (msg MsgTransferNFT) MarshalJSON() ([]byte, error) { + type msgAlias MsgTransferNFT + return json.Marshal(msgAlias(msg)) +} + +func (msg *MsgTransferNFT) UnmarshalJSON(data []byte) error { + type msgAlias *MsgTransferNFT + return json.Unmarshal(data, msgAlias(msg)) +} + +func (MsgTransferNFT) Route() string { return RouterKey } + +func (MsgTransferNFT) Type() string { return "transfer_nft" } + +func (msg MsgTransferNFT) GetContractID() string { return msg.ContractID } + +func (msg MsgTransferNFT) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty") + } + + if len(msg.TokenIDs) == 0 { + return sdkerrors.Wrap(ErrEmptyField, "token_ids cannot be empty") + } + for _, tokenID := range msg.TokenIDs { + if err := ValidateTokenID(tokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, err.Error()) + } + } + + return nil +} + +func (msg MsgTransferNFT) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgTransferNFT) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +type MsgTransferFTFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + To sdk.AccAddress `json:"to"` + Amount Coins `json:"amount"` +} + +func NewMsgTransferFTFrom(proxy sdk.AccAddress, contractID string, from sdk.AccAddress, to sdk.AccAddress, amount ...Coin) MsgTransferFTFrom { + return MsgTransferFTFrom{ + Proxy: proxy, + ContractID: contractID, + From: from, + To: to, + Amount: amount, + } +} + +func (msg MsgTransferFTFrom) MarshalJSON() ([]byte, error) { + type msgAlias MsgTransferFTFrom + return json.Marshal(msgAlias(msg)) +} + +func (msg *MsgTransferFTFrom) UnmarshalJSON(data []byte) error { + type msgAlias *MsgTransferFTFrom + return json.Unmarshal(data, msgAlias(msg)) +} + +func (MsgTransferFTFrom) Route() string { return RouterKey } + +func (MsgTransferFTFrom) Type() string { return "transfer_ft_from" } + +func (msg MsgTransferFTFrom) GetContractID() string { return msg.ContractID } + +func (msg MsgTransferFTFrom) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty") + } + if msg.From.Equals(msg.Proxy) { + return sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", msg.From.String()) + } + if !msg.Amount.IsValid() { + return sdkerrors.Wrap(ErrInvalidAmount, "invalid amount") + } + return nil +} + +func (msg MsgTransferFTFrom) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgTransferFTFrom) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Proxy} +} + +type MsgTransferNFTFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + To sdk.AccAddress `json:"to"` + TokenIDs []string `json:"token_ids"` +} + +func NewMsgTransferNFTFrom(proxy sdk.AccAddress, contractID string, from sdk.AccAddress, to sdk.AccAddress, tokenIDs ...string) MsgTransferNFTFrom { + return MsgTransferNFTFrom{ + Proxy: proxy, + ContractID: contractID, + From: from, + To: to, + TokenIDs: tokenIDs, + } +} + +func (msg MsgTransferNFTFrom) MarshalJSON() ([]byte, error) { + type msgAlias MsgTransferNFTFrom + return json.Marshal(msgAlias(msg)) +} + +func (msg *MsgTransferNFTFrom) UnmarshalJSON(data []byte) error { + type msgAlias *MsgTransferNFTFrom + return json.Unmarshal(data, msgAlias(msg)) +} + +func (MsgTransferNFTFrom) Route() string { return RouterKey } + +func (MsgTransferNFTFrom) Type() string { return "transfer_nft_from" } + +func (msg MsgTransferNFTFrom) GetContractID() string { return msg.ContractID } + +func (msg MsgTransferNFTFrom) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty") + } + if msg.From.Equals(msg.Proxy) { + return sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", msg.From.String()) + } + + if len(msg.TokenIDs) == 0 { + return sdkerrors.Wrap(ErrEmptyField, "token_ids cannot be empty") + } + for _, tokenID := range msg.TokenIDs { + if err := ValidateTokenID(tokenID); err != nil { + return sdkerrors.Wrap(ErrInvalidTokenID, err.Error()) + } + } + return nil +} + +func (msg MsgTransferNFTFrom) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgTransferNFTFrom) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Proxy} +} diff --git a/x/collection/internal/types/params.go b/x/collection/internal/types/params.go new file mode 100644 index 0000000000..da8e5fe0cf --- /dev/null +++ b/x/collection/internal/types/params.go @@ -0,0 +1,87 @@ +package types + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/params/subspace" +) + +const ( + DefaultParamspace = ModuleName + + DefaultMaxComposableDepth uint64 = 20 + DefaultMaxComposableWidth uint64 = 20 +) + +var ( + KeyMaxComposableDepth = []byte("MaxComposableDepth") + KeyMaxComposableWidth = []byte("MaxComposableWidth") +) + +var _ subspace.ParamSet = &Params{} + +type Params struct { + MaxComposableDepth uint64 `json:"max_composable_depth" yaml:"max_composable_depth"` + MaxComposableWidth uint64 `json:"max_composable_width" yaml:"max_composable_width"` +} + +func NewParams(maxComposableDepth, maxComposableWidth uint64) Params { + return Params{ + MaxComposableDepth: maxComposableDepth, + MaxComposableWidth: maxComposableWidth, + } +} + +func (p *Params) ParamSetPairs() subspace.ParamSetPairs { + return subspace.ParamSetPairs{ + params.NewParamSetPair(KeyMaxComposableDepth, &p.MaxComposableDepth, validateMaxComposableDepth), + params.NewParamSetPair(KeyMaxComposableWidth, &p.MaxComposableWidth, validateMaxComposableWidth), + } +} + +func (p Params) Validate() error { + if err := validateMaxComposableDepth(p.MaxComposableDepth); err != nil { + return err + } + + if err := validateMaxComposableWidth(p.MaxComposableWidth); err != nil { + return err + } + + return nil +} + +func validateMaxComposableDepth(i interface{}) error { + v, ok := i.(uint64) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v == 0 { + return fmt.Errorf("invalid max composable depth: %d", v) + } + + return nil +} + +func validateMaxComposableWidth(i interface{}) error { + v, ok := i.(uint64) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v == 0 { + return fmt.Errorf("invalid max composable width: %d", v) + } + + return nil +} + +func ParamKeyTable() subspace.KeyTable { + return subspace.NewKeyTable().RegisterParamSet(&Params{}) +} + +func DefaultParams() Params { + return NewParams(DefaultMaxComposableDepth, DefaultMaxComposableWidth) +} diff --git a/x/collection/internal/types/params_test.go b/x/collection/internal/types/params_test.go new file mode 100644 index 0000000000..e73ad2e0dc --- /dev/null +++ b/x/collection/internal/types/params_test.go @@ -0,0 +1,25 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewParams(t *testing.T) { + params := NewParams(10, 20) + require.Equal(t, uint64(10), params.MaxComposableDepth) + require.Equal(t, uint64(20), params.MaxComposableWidth) +} + +func TestValidate(t *testing.T) { + require.NoError(t, NewParams(20, 20).Validate()) + require.Error(t, NewParams(0, 20).Validate()) + require.Error(t, NewParams(20, 0).Validate()) +} + +func TestDefaultParams(t *testing.T) { + params := DefaultParams() + require.Equal(t, DefaultMaxComposableDepth, params.MaxComposableDepth) + require.Equal(t, DefaultMaxComposableWidth, params.MaxComposableWidth) +} diff --git a/x/collection/internal/types/perm.go b/x/collection/internal/types/perm.go new file mode 100644 index 0000000000..2710a3a326 --- /dev/null +++ b/x/collection/internal/types/perm.go @@ -0,0 +1,145 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + MintAction = "mint" + BurnAction = "burn" + IssueAction = "issue" + ModifyAction = "modify" +) + +type Permission string + +func NewMintPermission() Permission { + return MintAction +} + +func NewBurnPermission() Permission { + return BurnAction +} + +func NewIssuePermission() Permission { + return IssueAction +} + +func NewModifyPermission() Permission { + return ModifyAction +} + +func (p Permission) Equal(p2 Permission) bool { + return p == p2 +} +func (p Permission) String() string { + return string(p) +} + +func (p Permission) Validate() bool { + if p == MintAction { + return true + } + if p == BurnAction { + return true + } + if p == IssueAction { + return true + } + if p == ModifyAction { + return true + } + return false +} + +type Permissions []Permission + +func NewPermissions(perms ...Permission) Permissions { + pms := Permissions{} + for _, perm := range perms { + pms.AddPermission(perm) + } + return pms +} + +func (pms *Permissions) GetPermissions() []Permission { + return []Permission(*pms) +} + +func (pms *Permissions) RemoveElement(idx int) { + *pms = append((*pms)[:idx], (*pms)[idx+1:]...) +} + +func (pms *Permissions) AddPermission(p Permission) { + for _, pin := range *pms { + if pin.Equal(p) { + return + } + } + *pms = append(*pms, p) +} + +func (pms *Permissions) RemovePermission(p Permission) { + for idx, pin := range *pms { + if pin.Equal(p) { + pms.RemoveElement(idx) + return + } + } +} + +func (pms Permissions) HasPermission(p Permission) bool { + for _, pin := range pms { + if pin.Equal(p) { + return true + } + } + return false +} +func (pms Permissions) String() string { + return fmt.Sprintf("%#v", pms) +} + +type AccountPermissionI interface { + GetAddress() sdk.AccAddress + HasPermission(Permission) bool + AddPermission(Permission) + RemovePermission(Permission) + String() string + GetPermissions() Permissions +} + +type AccountPermission struct { + Address sdk.AccAddress + Permissions Permissions +} + +func NewAccountPermission(addr sdk.AccAddress) AccountPermissionI { + return &AccountPermission{ + Address: addr, + } +} + +func (ap *AccountPermission) String() string { + return fmt.Sprintf("%#v", ap) +} + +func (ap *AccountPermission) GetPermissions() Permissions { + return ap.Permissions.GetPermissions() +} + +func (ap *AccountPermission) GetAddress() sdk.AccAddress { + return ap.Address +} + +func (ap *AccountPermission) HasPermission(p Permission) bool { + return ap.Permissions.HasPermission(p) +} +func (ap *AccountPermission) AddPermission(p Permission) { + ap.Permissions.AddPermission(p) +} +func (ap *AccountPermission) RemovePermission(p Permission) { + ap.Permissions.RemovePermission(p) +} diff --git a/x/collection/internal/types/perm_test.go b/x/collection/internal/types/perm_test.go new file mode 100644 index 0000000000..480bdd70fc --- /dev/null +++ b/x/collection/internal/types/perm_test.go @@ -0,0 +1,24 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPermission(t *testing.T) { + issuePerm := NewIssuePermission() + mintPerm := NewMintPermission() + burnPerm := NewBurnPermission() + modifyPerm := NewModifyPermission() + + require.True(t, issuePerm.Validate()) + require.True(t, mintPerm.Validate()) + require.True(t, burnPerm.Validate()) + require.True(t, modifyPerm.Validate()) + + require.True(t, mintPerm.Equal(mintPerm)) + require.False(t, mintPerm.Equal(burnPerm)) + require.False(t, mintPerm.Equal(modifyPerm)) + require.False(t, mintPerm.Equal(issuePerm)) +} diff --git a/x/collection/internal/types/querier.go b/x/collection/internal/types/querier.go new file mode 100644 index 0000000000..7de81c6000 --- /dev/null +++ b/x/collection/internal/types/querier.go @@ -0,0 +1,86 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + QuerierRoute = ModuleName + QueryBalances = "balances" + QueryBalance = "balance" + QueryTokens = "tokens" + QueryTokensWithTokenType = "tokensWithTokenType" + QueryTokenTypes = "tokentypes" + QueryPerms = "perms" + QueryCollections = "collections" + QuerySupply = "supply" + QueryMint = "mint" + QueryBurn = "burn" + QueryNFTCount = "nftcount" + QueryNFTMint = "nftmint" + QueryNFTBurn = "nftburn" + QueryParent = "parent" + QueryRoot = "root" + QueryChildren = "children" + QueryIsApproved = "approved" + QueryApprovers = "approver" +) + +type NodeQuerier interface { + QueryWithData(path string, data []byte) ([]byte, int64, error) + WithHeight(height int64) context.CLIContext +} + +type QueryTokenIDParams struct { + TokenID string `json:"token_id"` +} + +func NewQueryTokenIDParams(tokenID string) QueryTokenIDParams { + return QueryTokenIDParams{TokenID: tokenID} +} + +type QueryTokenTypeParams struct { + TokenType string `json:"token_type"` +} + +func NewQueryTokenTypeParams(tokenType string) QueryTokenTypeParams { + return QueryTokenTypeParams{TokenType: tokenType} +} + +type QueryTokenIDAccAddressParams struct { + TokenID string `json:"token_id"` + Addr sdk.AccAddress `json:"addr"` +} + +func NewQueryTokenIDAccAddressParams(tokenID string, addr sdk.AccAddress) QueryTokenIDAccAddressParams { + return QueryTokenIDAccAddressParams{TokenID: tokenID, Addr: addr} +} + +type QueryAccAddressParams struct { + Addr sdk.AccAddress `json:"addr"` +} + +func NewQueryAccAddressParams(addr sdk.AccAddress) QueryAccAddressParams { + return QueryAccAddressParams{Addr: addr} +} + +type QueryIsApprovedParams struct { + Proxy sdk.AccAddress `json:"proxy"` + Approver sdk.AccAddress `json:"approver"` +} + +func NewQueryIsApprovedParams(proxy sdk.AccAddress, approver sdk.AccAddress) QueryIsApprovedParams { + return QueryIsApprovedParams{ + Proxy: proxy, + Approver: approver, + } +} + +type QueryProxyParams struct { + Proxy sdk.AccAddress `json:"proxy"` +} + +func NewQueryApproverParams(proxy sdk.AccAddress) QueryProxyParams { + return QueryProxyParams{Proxy: proxy} +} diff --git a/x/collection/internal/types/supply.go b/x/collection/internal/types/supply.go new file mode 100644 index 0000000000..7caf01eb60 --- /dev/null +++ b/x/collection/internal/types/supply.go @@ -0,0 +1,108 @@ +package types + +import ( + "encoding/json" + "fmt" +) + +type Supply interface { + GetContractID() string + GetTotalSupply() Coins + SetTotalSupply(total Coins) Supply + GetTotalMint() Coins + GetTotalBurn() Coins + + Inflate(amount Coins) Supply + Deflate(amount Coins) Supply + + String() string + ValidateBasic() error +} + +var _ Supply = (*BaseSupply)(nil) + +type BaseSupply struct { + ContractID string `json:"contract_id"` + TotalSupply Coins `json:"total_supply"` + TotalMint Coins `json:"total_mint"` + TotalBurn Coins `json:"total_burn"` +} + +func (supply BaseSupply) GetContractID() string { + return supply.ContractID +} + +func (supply BaseSupply) SetTotalSupply(total Coins) Supply { + supply.TotalSupply = total + supply.TotalMint = total + supply.TotalBurn = NewCoins() + return supply +} + +func (supply BaseSupply) GetTotalSupply() Coins { + return supply.TotalSupply +} + +func (supply BaseSupply) GetTotalMint() Coins { + return supply.TotalMint +} + +func (supply BaseSupply) GetTotalBurn() Coins { + return supply.TotalBurn +} + +func NewSupply(contractID string, total Coins) Supply { + return BaseSupply{ContractID: contractID, TotalSupply: total, TotalMint: total, TotalBurn: NewCoins()} +} + +func DefaultSupply(contractID string) Supply { + return NewSupply(contractID, NewCoins()) +} + +func (supply BaseSupply) Inflate(amount Coins) Supply { + supply.TotalSupply = supply.TotalSupply.Add(amount...) + supply.TotalMint = supply.TotalMint.Add(amount...) + supply.checkInvariant() + return supply +} + +func (supply BaseSupply) Deflate(amount Coins) Supply { + supply.TotalSupply = supply.TotalSupply.Sub(amount) + supply.TotalBurn = supply.TotalBurn.Add(amount...) + supply.checkInvariant() + return supply +} + +func (supply BaseSupply) String() string { + b, err := json.Marshal(supply) + if err != nil { + panic(err) + } + return string(b) +} + +func (supply BaseSupply) ValidateBasic() error { + if !supply.TotalSupply.IsValid() { + return fmt.Errorf("invalid total supply: %s", supply.TotalSupply.String()) + } + if !supply.TotalMint.IsValid() { + return fmt.Errorf("invalid total mint: %s", supply.TotalMint.String()) + } + if !supply.TotalBurn.IsValid() { + return fmt.Errorf("invalid total burn: %s", supply.TotalBurn.String()) + } + return nil +} + +// panic if totalSupply != totalMint - totalBurn +func (supply BaseSupply) checkInvariant() { + if !supply.TotalSupply.IsEqual(supply.TotalMint.Sub(supply.TotalBurn)) { + panic(fmt.Sprintf( + "Collection [%v]'s total supply [%v] does not match with total mint [%v] - total burn [%v]", + supply.GetContractID(), + supply.TotalSupply, + supply.TotalMint, + supply.TotalBurn, + )) + } +} diff --git a/x/collection/internal/types/supply_test.go b/x/collection/internal/types/supply_test.go new file mode 100644 index 0000000000..749566adbc --- /dev/null +++ b/x/collection/internal/types/supply_test.go @@ -0,0 +1,78 @@ +package types + +import ( + "encoding/json" + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/stretchr/testify/require" +) + +func TestSupply(t *testing.T) { + var supply Supply + supply = DefaultSupply(defaultTokenIDFT) + + // create default + require.Equal(t, defaultTokenIDFT, supply.GetContractID()) + require.Equal(t, NewCoins(), supply.GetTotalSupply()) + require.Equal(t, NewCoins(), supply.GetTotalMint()) + require.Equal(t, NewCoins(), supply.GetTotalBurn()) + + // set total supply + initialSupply := NewCoins(NewCoin(defaultTokenIDFT, sdk.NewInt(3))) + supply = supply.SetTotalSupply(initialSupply) + require.Equal(t, initialSupply, supply.GetTotalSupply()) + require.Equal(t, initialSupply, supply.GetTotalMint()) + require.Equal(t, NewCoins(), supply.GetTotalBurn()) + + // inflate + toInflate := NewCoins(NewCoin(defaultTokenIDFT, sdk.NewInt(2))) + supply = supply.Inflate(toInflate) + require.Equal(t, initialSupply.Add(toInflate...), supply.GetTotalSupply()) + require.Equal(t, initialSupply.Add(toInflate...), supply.GetTotalMint()) + require.Equal(t, NewCoins(), supply.GetTotalBurn()) + + // deflate + toDeflate := NewCoins(NewCoin(defaultTokenIDFT, sdk.NewInt(4))) + supply = supply.Deflate(toDeflate) + require.Equal(t, initialSupply.Add(toInflate...).Sub(toDeflate), supply.GetTotalSupply()) + require.Equal(t, initialSupply.Add(toInflate...), supply.GetTotalMint()) + require.Equal(t, toDeflate, supply.GetTotalBurn()) + + // total + ts, err1 := json.Marshal(NewCoins(NewCoin(defaultTokenIDFT, sdk.NewInt(1)))) + require.NoError(t, err1) + tm, err2 := json.Marshal(NewCoins(NewCoin(defaultTokenIDFT, sdk.NewInt(5)))) + require.NoError(t, err2) + tb, err3 := json.Marshal(NewCoins(NewCoin(defaultTokenIDFT, sdk.NewInt(4)))) + require.NoError(t, err3) + expected := fmt.Sprintf( + `{"contract_id":"%s","total_supply":%v,"total_mint":%v,"total_burn":%v}`, + defaultTokenIDFT, + string(ts), + string(tm), + string(tb), + ) + require.Equal(t, expected, supply.String()) +} + +func TestSupplyMarshalYAML(t *testing.T) { + supply := DefaultSupply(defaultContractID) + coins := NewCoins(NewCoin(defaultTokenIDFT, sdk.OneInt())) + supply = supply.Inflate(coins) + + bzCoins, err := json.Marshal(coins) + require.NoError(t, err) + + expected := fmt.Sprintf( + `{"contract_id":"%s","total_supply":%s,"total_mint":%s,"total_burn":%s}`, + defaultContractID, + string(bzCoins), + string(bzCoins), + []Coin{}, + ) + + require.Equal(t, expected, supply.String()) +} diff --git a/x/collection/internal/types/symbol.go b/x/collection/internal/types/symbol.go new file mode 100644 index 0000000000..fadd1edd88 --- /dev/null +++ b/x/collection/internal/types/symbol.go @@ -0,0 +1,40 @@ +package types + +import ( + "fmt" + "regexp" +) + +const ( + /* #nosec */ + reTokenIDString = `[a-f0-9]{16}` + /* #nosec */ + reTokenTypeString = `[a-f0-9]{8}` + /* #nosec */ + reTokenTypeFTString = `0[a-f0-9]{7}` + /* #nosec */ + reTokenTypeNFTString = `[a-f1-9][a-f0-9]{7}` + /* #nosec */ + reTokenIndexString = `[a-f0-9]{8}` +) + +var ( + reTokenID = regexp.MustCompile(fmt.Sprintf(`^%s$`, reTokenIDString)) + reTokenType = regexp.MustCompile(fmt.Sprintf(`^%s$`, reTokenTypeString)) + reTokenTypeFT = regexp.MustCompile(fmt.Sprintf(`^%s$`, reTokenTypeFTString)) + reTokenTypeNFT = regexp.MustCompile(fmt.Sprintf(`^%s$`, reTokenTypeNFTString)) + reTokenIndex = regexp.MustCompile(fmt.Sprintf(`^%s$`, reTokenIndexString)) +) + +func ValidateReg(symbol string, reg *regexp.Regexp) error { + if !reg.MatchString(symbol) { + return fmt.Errorf("symbol [%s] mismatched to [%s]", symbol, reg.String()) + } + return nil +} + +func ValidateTokenID(tokenID string) error { return ValidateReg(tokenID, reTokenID) } +func ValidateTokenType(tokenType string) error { return ValidateReg(tokenType, reTokenType) } +func ValidateTokenTypeFT(tokenType string) error { return ValidateReg(tokenType, reTokenTypeFT) } +func ValidateTokenTypeNFT(tokenType string) error { return ValidateReg(tokenType, reTokenTypeNFT) } +func ValidateTokenIndex(index string) error { return ValidateReg(index, reTokenIndex) } diff --git a/x/collection/internal/types/token.go b/x/collection/internal/types/token.go new file mode 100644 index 0000000000..10041dabc6 --- /dev/null +++ b/x/collection/internal/types/token.go @@ -0,0 +1,128 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type Token interface { + GetName() string + SetName(name string) + GetContractID() string + GetTokenID() string + GetTokenType() string + GetTokenIndex() string + String() string + GetMeta() string + SetMeta(meta string) +} + +type FT interface { + Token + GetMintable() bool + GetDecimals() sdk.Int +} + +type NFT interface { + Token + GetOwner() sdk.AccAddress + SetOwner(sdk.AccAddress) +} + +var _ Token = (*BaseNFT)(nil) + +type BaseNFT struct { + ContractID string `json:"contract_id"` + TokenID string `json:"token_id"` + Owner sdk.AccAddress `json:"owner"` + Name string `json:"name"` + Meta string `json:"meta"` +} + +func NewNFT(contractID, tokenID, name, meta string, owner sdk.AccAddress) NFT { + return &BaseNFT{ + ContractID: contractID, + TokenID: tokenID, + Owner: owner, + Name: name, + Meta: meta, + } +} +func (t BaseNFT) GetName() string { return t.Name } +func (t BaseNFT) GetContractID() string { return t.ContractID } +func (t BaseNFT) GetOwner() sdk.AccAddress { return t.Owner } +func (t BaseNFT) GetTokenID() string { return t.TokenID } +func (t BaseNFT) GetTokenType() string { return t.TokenID[:TokenTypeLength] } +func (t BaseNFT) GetTokenIndex() string { return t.TokenID[TokenTypeLength:] } +func (t *BaseNFT) SetName(name string) { + t.Name = name +} +func (t *BaseNFT) SetOwner(owner sdk.AccAddress) { + t.Owner = owner +} +func (t BaseNFT) String() string { + b, err := json.Marshal(t) + if err != nil { + panic(err) + } + return string(b) +} +func (t BaseNFT) GetMeta() string { return t.Meta } +func (t *BaseNFT) SetMeta(meta string) { + t.Meta = meta +} + +var _ Token = (*BaseFT)(nil) +var _ FT = (*BaseFT)(nil) + +type BaseFT struct { + ContractID string `json:"contract_id"` + TokenID string `json:"token_id"` + Decimals sdk.Int `json:"decimals"` + Mintable bool `json:"mintable"` + Name string `json:"name"` + Meta string `json:"meta"` +} + +func NewFT(contractID, tokenID, name, meta string, decimals sdk.Int, mintable bool) FT { + return &BaseFT{ + ContractID: contractID, + TokenID: tokenID, + Decimals: decimals, + Mintable: mintable, + Name: name, + Meta: meta, + } +} +func (t BaseFT) GetName() string { return t.Name } +func (t BaseFT) GetContractID() string { return t.ContractID } +func (t BaseFT) GetMintable() bool { return t.Mintable } +func (t BaseFT) GetDecimals() sdk.Int { return t.Decimals } +func (t BaseFT) GetTokenID() string { return t.TokenID } +func (t BaseFT) GetTokenType() string { return t.TokenID[:TokenTypeLength] } +func (t BaseFT) GetTokenIndex() string { return t.TokenID[TokenTypeLength:] } +func (t *BaseFT) SetName(name string) { + t.Name = name +} +func (t BaseFT) String() string { + b, err := json.Marshal(t) + if err != nil { + panic(err) + } + return string(b) +} +func (t BaseFT) GetMeta() string { return t.Meta } +func (t *BaseFT) SetMeta(meta string) { + t.Meta = meta +} + +type Tokens []Token + +func (ts Tokens) String() string { + b, err := json.Marshal(ts) + if err != nil { + panic(err) + } + return string(b) +} diff --git a/x/collection/internal/types/token_test.go b/x/collection/internal/types/token_test.go new file mode 100644 index 0000000000..2677317b31 --- /dev/null +++ b/x/collection/internal/types/token_test.go @@ -0,0 +1,77 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalFT(t *testing.T) { + // Given a FT + token := NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true) + var token2 BaseFT + + // When marshal and unmarshal the FT + bz, err := ModuleCdc.MarshalJSON(token) + require.NoError(t, err) + err = ModuleCdc.UnmarshalJSON(bz, &token2) + require.NoError(t, err) + + // Then the properties are same + r := require.New(t) + r.EqualValues(defaultName, token.GetName(), token2.GetName()) + r.Equal(defaultContractID, token.GetContractID(), token2.GetContractID()) + r.Equal(defaultTokenIDFT, token.GetTokenID(), token2.GetTokenID()) + r.Equal(defaultTokenIDFT[:TokenTypeLength], token.GetTokenType(), token2.GetTokenType()) + r.Equal(defaultTokenIDFT[TokenTypeLength:], token.GetTokenIndex(), token2.GetTokenIndex()) + r.Equal(int64(defaultDecimals), token.GetDecimals().Int64(), token2.GetDecimals().Int64()) + r.Equal(true, token.GetMintable(), token2.GetMintable()) + + r.Equal(`{"contract_id":"abcdef01","token_id":"0000000100000000","decimals":"6","mintable":true,"name":"name","meta":"{}"}`, token.String()) +} + +func TestUnmarshalNFT(t *testing.T) { + // Given a NFT + token := NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1) + var token2 BaseNFT + + // When marshal and unmarshal the FT + bz, err := ModuleCdc.MarshalJSON(token) + require.NoError(t, err) + err = ModuleCdc.UnmarshalJSON(bz, &token2) + require.NoError(t, err) + + // Then the properties are same + r := require.New(t) + r.Equal(defaultName, token.GetName(), token2.GetName()) + r.Equal(defaultContractID, token.GetContractID(), token2.GetContractID()) + r.Equal(defaultTokenID1, token.GetTokenID(), token2.GetTokenID()) + r.Equal(defaultTokenID1[:TokenTypeLength], token.GetTokenType(), token2.GetTokenType()) + r.Equal(defaultTokenID1[TokenTypeLength:], token.GetTokenIndex(), token2.GetTokenIndex()) + r.Equal(addr1, token.GetOwner(), token2.GetOwner()) +} + +func TestSetName(t *testing.T) { + // Given a FT, NFT + tokenFT := NewFT(defaultContractID, defaultTokenIDFT, defaultName, defaultMeta, sdk.NewInt(defaultDecimals), true) + tokenNFT := NewNFT(defaultContractID, defaultTokenID1, defaultName, defaultMeta, addr1) + + tokenFT.SetName("new_name") + tokenNFT.SetName("new_name") + tokenFT.SetMeta("new_meta") + tokenNFT.SetMeta("new_meta") + + // When change name, Then they are changed + require.Equal(t, "new_name", tokenFT.GetName()) + require.Equal(t, "new_name", tokenNFT.GetName()) + require.Equal(t, "new_meta", tokenFT.GetMeta()) + require.Equal(t, "new_meta", tokenNFT.GetMeta()) + + // Set empty name + tokenFT.SetName("") + tokenNFT.SetName("") + + require.Equal(t, "", tokenFT.GetName()) + require.Equal(t, "", tokenNFT.GetName()) +} diff --git a/x/collection/internal/types/token_type.go b/x/collection/internal/types/token_type.go new file mode 100644 index 0000000000..d51da1b48f --- /dev/null +++ b/x/collection/internal/types/token_type.go @@ -0,0 +1,69 @@ +package types + +import ( + "encoding/json" +) + +const ( + TokenTypeLength = 8 + SmallestAlphanum = "0" + FungibleFlag = SmallestAlphanum + ReservedEmpty = "00000000" + SmallestFTType = "00000001" + ReservedEmptyNFT = "10000000" + SmallestNFTType = "10000001" + SmallestTokenIndex = "00000001" +) + +type TokenType interface { + GetName() string + SetName(string) + GetMeta() string + SetMeta(string) + GetContractID() string + GetTokenType() string + String() string +} + +type BaseTokenType struct { + ContractID string `json:"contract_id"` + TokenType string `json:"token_type"` + Name string `json:"name"` + Meta string `json:"meta"` +} + +func NewBaseTokenType(contractID, tokenType, name, meta string) TokenType { + return &BaseTokenType{ + ContractID: contractID, + TokenType: tokenType, + Name: name, + Meta: meta, + } +} +func (t BaseTokenType) GetName() string { return t.Name } +func (t *BaseTokenType) SetName(name string) { + t.Name = name +} +func (t BaseTokenType) GetContractID() string { return t.ContractID } +func (t BaseTokenType) GetTokenType() string { return t.TokenType } +func (t BaseTokenType) String() string { + b, err := json.Marshal(t) + if err != nil { + panic(err) + } + return string(b) +} +func (t BaseTokenType) GetMeta() string { return t.Meta } +func (t *BaseTokenType) SetMeta(meta string) { + t.Meta = meta +} + +type TokenTypes []TokenType + +func (ts TokenTypes) String() string { + b, err := json.Marshal(ts) + if err != nil { + panic(err) + } + return string(b) +} diff --git a/x/collection/internal/types/token_type_test.go b/x/collection/internal/types/token_type_test.go new file mode 100644 index 0000000000..006e2fe09c --- /dev/null +++ b/x/collection/internal/types/token_type_test.go @@ -0,0 +1,39 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTokenType(t *testing.T) { + tokenType := NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta) + + require.Equal(t, `{"contract_id":"abcdef01","token_type":"10000001","name":"name","meta":"{}"}`, tokenType.String()) + + var tokenType2 TokenType + bz, err := ModuleCdc.MarshalJSON(tokenType) + require.NoError(t, err) + err = ModuleCdc.UnmarshalJSON(bz, &tokenType2) + require.NoError(t, err) + + require.Equal(t, defaultName, tokenType2.GetName()) + require.Equal(t, defaultContractID, tokenType2.GetContractID()) + require.Equal(t, defaultTokenType, tokenType2.GetTokenType()) + + require.Equal(t, tokenType.GetName(), tokenType2.GetName()) + require.Equal(t, tokenType.GetContractID(), tokenType2.GetContractID()) + require.Equal(t, tokenType.GetTokenType(), tokenType2.GetTokenType()) + + require.Equal(t, `{"contract_id":"abcdef01","token_type":"10000001","name":"name","meta":"{}"}`, tokenType.String()) + + tokenType3 := NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta) + tokenType3.SetName("testname") + require.Equal(t, defaultName, tokenType.GetName()) + require.Equal(t, "testname", tokenType3.GetName()) + + tokenType4 := NewBaseTokenType(defaultContractID, defaultTokenType, defaultName, defaultMeta) + tokenType4.SetMeta("testmeta") + require.Equal(t, defaultMeta, tokenType.GetMeta()) + require.Equal(t, "testmeta", tokenType4.GetMeta()) +} diff --git a/x/collection/internal/types/validators.go b/x/collection/internal/types/validators.go new file mode 100644 index 0000000000..42dd763106 --- /dev/null +++ b/x/collection/internal/types/validators.go @@ -0,0 +1,133 @@ +package types + +import ( + "unicode/utf8" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + MaxBaseImgURILength = 1000 + MaxTokenNameLength = 20 + MaxChangeFieldsCount = 100 + MaxTokenMetaLength = 1000 +) + +var ( + CollectionModifiableFields = ModifiableFields{ + AttributeKeyName: true, + AttributeKeyBaseImgURI: true, + AttributeKeyMeta: true, + } + TokenTypeModifiableFields = ModifiableFields{ + AttributeKeyName: true, + AttributeKeyMeta: true, + } + TokenModifiableFields = ModifiableFields{ + AttributeKeyName: true, + AttributeKeyMeta: true, + } +) + +type ModifiableFields map[string]bool + +func ValidateName(name string) bool { + return utf8.RuneCountInString(name) <= MaxTokenNameLength +} + +func ValidateBaseImgURI(baseImgURI string) bool { + return utf8.RuneCountInString(baseImgURI) <= MaxBaseImgURILength +} +func ValidateMeta(meta string) bool { + return utf8.RuneCountInString(meta) <= MaxTokenMetaLength +} + +type ChangesValidator struct { + modifiableFields ModifiableFields + handlers map[string]func(value string) error +} + +func NewChangesValidator() *ChangesValidator { + hs := make(map[string]func(value string) error) + hs[AttributeKeyName] = func(value string) error { + if !ValidateName(value) { + return sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", value, MaxTokenNameLength, utf8.RuneCountInString(value)) + } + return nil + } + hs[AttributeKeyBaseImgURI] = func(value string) error { + if !ValidateBaseImgURI(value) { + return sdkerrors.Wrapf(ErrInvalidBaseImgURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", value, MaxBaseImgURILength, utf8.RuneCountInString(value)) + } + return nil + } + hs[AttributeKeyMeta] = func(value string) error { + if !ValidateMeta(value) { + return sdkerrors.Wrapf(ErrInvalidMetaLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", value, MaxTokenMetaLength, utf8.RuneCountInString(value)) + } + return nil + } + return &ChangesValidator{ + handlers: hs, + } +} + +func (c *ChangesValidator) Validate(changes Changes) error { + if len(changes) == 0 { + return ErrEmptyChanges + } + + if len(changes) > MaxChangeFieldsCount { + return sdkerrors.Wrapf(ErrInvalidChangesFieldCount, "You can not change fields more than [%d] at once, current count: [%d]", MaxChangeFieldsCount, len(changes)) + } + + checkedFields := map[string]bool{} + for _, change := range changes { + if !c.modifiableFields[change.Field] { + return sdkerrors.Wrapf(ErrInvalidChangesField, "Field: %s", change.Field) + } + if checkedFields[change.Field] { + return sdkerrors.Wrapf(ErrDuplicateChangesField, "Field: %s", change.Field) + } + + validateHandler, ok := c.handlers[change.Field] + if !ok { + return sdkerrors.Wrapf(ErrInvalidChangesField, "Field: %s", change.Field) + } + + if err := validateHandler(change.Value); err != nil { + return err + } + checkedFields[change.Field] = true + } + return nil +} + +func (c *ChangesValidator) SetMode(tokenType, tokenIndex string) error { + if tokenType != "" { + if tokenIndex == "" { + c.forTokenType() + } else { + c.forToken() + } + } else { + if tokenIndex == "" { + c.forCollection() + } else { + return ErrTokenIndexWithoutType + } + } + return nil +} + +func (c *ChangesValidator) forCollection() { + c.modifiableFields = CollectionModifiableFields +} + +func (c *ChangesValidator) forTokenType() { + c.modifiableFields = TokenTypeModifiableFields +} + +func (c *ChangesValidator) forToken() { + c.modifiableFields = TokenModifiableFields +} diff --git a/x/collection/internal/types/validators_test.go b/x/collection/internal/types/validators_test.go new file mode 100644 index 0000000000..8a3e728189 --- /dev/null +++ b/x/collection/internal/types/validators_test.go @@ -0,0 +1,158 @@ +package types + +import ( + "strings" + "testing" + "unicode/utf8" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" +) + +var length1001String = strings.Repeat("Eng글자日本語はスゲ", 91) // 11 * 91 = 1001 + +func TestValidateName(t *testing.T) { + t.Log("Given valid name") + { + var length20String = strings.Repeat("Eng글자日本語はス", 2) // 10 * 2 = 20 + require.True(t, ValidateName(length20String)) + } + t.Log("Given invalid name") + { + var length21String = strings.Repeat("Eng글자日本", 3) // 7 * 3 = 21 + require.False(t, ValidateName(length21String)) + } +} + +func TestValidateBaseImgURI(t *testing.T) { + t.Log("Given valid base_img_uri") + { + var length990String = strings.Repeat("Eng글자日本語はスゲ", 90) // 11 * 90 = 990 + require.True(t, ValidateBaseImgURI(length990String)) + } + t.Log("Given invalid base_img_uri") + { + require.False(t, ValidateBaseImgURI(length1001String)) + } +} + +func TestValidateChangesForCollection(t *testing.T) { + // Given ChangesValidator for collection + validator := NewChangesValidator() + err := validator.SetMode("", "") + require.NoError(t, err) + + t.Log("Test with valid changes") + { + changes := NewChangesWithMap(map[string]string{ + "name": "new_name", + "base_img_uri": "new_base_uri", + }) + + require.Nil(t, validator.Validate(changes)) + } + t.Log("Test with empty changes") + { + changes := Changes{} + require.EqualError(t, validator.Validate(changes), ErrEmptyChanges.Error()) + } + t.Log("Test with base_img_uri too long") + { + length1001String := strings.Repeat("Eng글자日本語はスゲ", 91) // 11 * 91 = 1001 + changes := NewChangesWithMap(map[string]string{ + "name": "new_name", + "base_img_uri": length1001String, + }) + + require.EqualError( + t, + validator.Validate(changes), + sdkerrors.Wrapf(ErrInvalidBaseImgURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", length1001String, MaxBaseImgURILength, utf8.RuneCountInString(length1001String)).Error(), + ) + } + t.Log("Test with invalid changes field") + { + // Given changes with invalid fields + changes := NewChanges( + NewChange("invalid_field", "value"), + ) + + // Then error is occurred + require.EqualError(t, validator.Validate(changes), sdkerrors.Wrapf(ErrInvalidChangesField, "Field: invalid_field").Error()) + } + t.Log("Test with changes more than max") + { + // Given changes more than max + changeList := make([]Change, MaxChangeFieldsCount+1) + changes := Changes(changeList) + + // Then error is occurred + require.EqualError(t, validator.Validate(changes), sdkerrors.Wrapf(ErrInvalidChangesFieldCount, "You can not change fields more than [%d] at once, current count: [%d]", MaxChangeFieldsCount, len(changeList)).Error()) + } + t.Log("Test with duplicate fields") + { + // Given changes with duplicate fields + changes := NewChanges( + NewChange("name", "value"), + NewChange("name", "value2"), + ) + + // Then error is occurred + require.EqualError(t, validator.Validate(changes), sdkerrors.Wrapf(ErrDuplicateChangesField, "Field: name").Error()) + } +} + +func TestValidateChangesForTokenType(t *testing.T) { + // Given ChangesValidator for token type + validator := NewChangesValidator() + err := validator.SetMode(defaultTokenType, "") + require.NoError(t, err) + + t.Log("Test with valid changes") + { + changes := NewChangesWithMap(map[string]string{ + "name": "new_name", + }) + + require.Nil(t, validator.Validate(changes)) + } + t.Log("Test with base_img_uri") + { + changes := NewChangesWithMap(map[string]string{ + "name": "new_name", + "base_img_uri": "new_base_uri", + }) + + require.EqualError(t, validator.Validate(changes), sdkerrors.Wrap(ErrInvalidChangesField, "Field: base_img_uri").Error()) + } +} + +func TestValidateChangesForToken(t *testing.T) { + // Given ChangesValidator for token + validator := NewChangesValidator() + err := validator.SetMode(defaultTokenType, defaultTokenIndex) + require.NoError(t, err) + + t.Log("Test with valid changes") + { + changes := NewChangesWithMap(map[string]string{ + "name": "new_name", + }) + + require.Nil(t, validator.Validate(changes)) + } + t.Log("Test with base_img_uri") + { + changes := NewChangesWithMap(map[string]string{ + "name": "new_name", + "base_img_uri": "new_base_uri", + }) + + require.EqualError(t, validator.Validate(changes), sdkerrors.Wrap(ErrInvalidChangesField, "Field: base_img_uri").Error()) + } +} + +func TestValidateChangesForTokenWithoutType(t *testing.T) { + validator := NewChangesValidator() + require.EqualError(t, validator.SetMode("", defaultTokenIndex), ErrTokenIndexWithoutType.Error()) +} diff --git a/x/collection/module.go b/x/collection/module.go new file mode 100644 index 0000000000..77903edaf2 --- /dev/null +++ b/x/collection/module.go @@ -0,0 +1,155 @@ +package collection + +import ( + "encoding/json" + "math/rand" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/x/upgrade" + "github.com/line/lbm-sdk/v2/x/collection/internal/legacy" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/line/lbm-sdk/v2/x/collection/client/cli" + "github.com/line/lbm-sdk/v2/x/collection/client/rest" + "github.com/line/lbm-sdk/v2/x/collection/internal/handler" + "github.com/line/lbm-sdk/v2/x/collection/internal/keeper" + "github.com/line/lbm-sdk/v2/x/collection/internal/querier" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// app module basics object +type AppModuleBasic struct{} + +// module name +func (AppModuleBasic) Name() string { return ModuleName } + +// register module codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { RegisterCodec(cdc) } + +// default genesis state +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) +} + +// module validate genesis +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var data GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &data) + if err != nil { + return err + } + return ValidateGenesis(data) +} + +// register rest routes +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { + rest.RegisterRoutes(ctx, rtr) +} + +// get the root tx command of this module +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetTxCmd(cdc) +} + +// get the root query command of this module +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetQueryCmd(cdc) +} + +func (AppModuleBasic) GetUpgradeHandler(version string) upgrade.UpgradeHandler { + return legacy.UpgradeHandler(version) +} + +// ___________________________ +// app module +type AppModule struct { + AppModuleBasic + keeper keeper.Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper keeper.Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// module name +func (AppModule) Name() string { return ModuleName } + +// register invariants +// TODO: should this module need invariants? +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// module message route name +func (AppModule) Route() string { return RouterKey } + +// module handler +func (am AppModule) NewHandler() sdk.Handler { return handler.NewHandler(am.keeper) } + +// module querier route name +func (AppModule) QuerierRoute() string { return RouterKey } + +// module querier +func (am AppModule) NewQuerierHandler() sdk.Querier { + return querier.NewQuerier(am.keeper) +} + +// module init-genesis +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + ModuleCdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.keeper, genesisState) + return []abci.ValidatorUpdate{} +} + +// module export genesis +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return ModuleCdc.MustMarshalJSON(gs) +} + +// module begin-block +func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// module end-block +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} + +// ____________________________________________________________________________ + +// AppModuleSimulation functions + +func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + simState.GenState[ModuleName] = simState.Cdc.MustMarshalJSON(DefaultGenesisState()) +} + +func (AppModule) ProposalContents(module.SimulationState) []simulation.WeightedProposalContent { + return nil +} + +func (AppModule) RandomizedParams(*rand.Rand) []simulation.ParamChange { + return nil +} + +func (AppModule) RegisterStoreDecoder(sdk.StoreDecoderRegistry) { +} + +func (AppModule) WeightedOperations(module.SimulationState) []simulation.WeightedOperation { + return nil +} diff --git a/x/collection/spec/01_concept.md b/x/collection/spec/01_concept.md new file mode 100644 index 0000000000..bee5c528cf --- /dev/null +++ b/x/collection/spec/01_concept.md @@ -0,0 +1,2 @@ +# Concept +TBD diff --git a/x/collection/spec/02_keepers.md b/x/collection/spec/02_keepers.md new file mode 100644 index 0000000000..bddc31f1af --- /dev/null +++ b/x/collection/spec/02_keepers.md @@ -0,0 +1,2 @@ +# Keepers +TBD diff --git a/x/collection/spec/03_messages.md b/x/collection/spec/03_messages.md new file mode 100644 index 0000000000..2896b06048 --- /dev/null +++ b/x/collection/spec/03_messages.md @@ -0,0 +1,454 @@ +# Messages + +## MsgIssue + +**Issue token messages are to create a new token on Link Chain** +- The new contract id is generated while issuer issues and the issue permission is granted to the issuer +- An issuer who granted issue permission can issue collective tokens +- Mint permission is granted to the token issuer when the token is mintable +- The identifier for the collective token is defined by the concatenation of the contract_id and the token id + +### MsgCreate +```golang +type MsgCreateCollection struct { + Owner sdk.AccAddress `json:"owner"` + Name string `json:"name"` + Meta string `json:"meta"` + BaseImgURI string `json:"base_img_uri"` +} +``` + +### MsgIssueFT +```golang +type MsgIssueFT struct { + Owner sdk.AccAddress `json:"owner"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Name string `json:"name"` + Meta string `json:"meta"` + Amount sdk.Int `json:"amount"` + Mintable bool `json:"mintable"` + Decimals sdk.Int `json:"decimals"` +} +``` + + +### MsgIssueNFT +```golang +type MsgIssueNFT struct { + Owner sdk.AccAddress `json:"owner"` + ContractID string `json:"contract_id"` + Name string `json:"name"` + Meta string `json:"meta"` +} +``` + + +## Mint + +**Mint message is to increase the total supply of the token** +- Signer(From) of this message must have permission +- Minted token is added to the `To` account + +### MsgMintFT + +```golang +type MsgMintFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Amount Coins `json:"amount"` +} + +type Coin struct { + Denom string `json:"token_id"` + Amount sdk.Int `json:"amount"` +} + +type Coins []Coin +``` + + +### MsgMintNFT +```golang +type MintNFTParam struct { + Name string `json:"name"` + Meta string `json:"meta"` + TokenType string `json:"token_type"` +} + +type MsgMintNFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + MintNFTParams []MintNFTParam `json:"params"` +} +``` + +## Burn +**Burn message is to decrease the total supply of the token** +- Signer(From) of this message must have the amount of the tokens +- Token is subtracted from the `From` account + +### MsgBurnFT + +```golang +type MsgBurnFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + Amount Coins `json:"amount"` +} +``` + +### MsgBurnNFT +```golang +type MsgBurnNFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + TokenIDs []string `json:"token_ids"` +} +``` + +### MsgBurnFTFrom + +```golang +type MsgBurnFTFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + Amount Coins `json:"amount"` +} +``` + +### MsgBurnNFTFrom +```golang +type MsgBurnNFTFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + TokenIDs []string `json:"token_ids"` +} +``` + +## MsgGrantPermission + +```golang +type MsgGrantPermission struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Permission Permission `json:"permission"` +} +``` + +**Grant Permission is to give a permission to the `To` account** +- `From` account must has the permission + +## MsgRevokePermission + +```golang +type MsgRevokePermission struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + Permission Permission `json:"permission"` +} +``` + +**Revoke Permission is to dump a permission from the `From` account** +- `From` account must has the permission + + +## MsgTransferFT +```golang +type MsgTransferFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Amount Coins `json:"amount"` +} +``` + +**TransferFT message is to transfer a collective non-reserved fungible token** +- Signer of this message must have the amount of the tokens +- Token is subtracted from the `From` account +- Token is added to the `To` account + + +## MsgTransferNFT + +```golang +type MsgTransferNFT struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + TokenIDs []string `json:"token_ids"` +} +``` + +**TransferNFT message is to transfer a collective non-fungible token** +- Signer of this message must have the token +- Token is subtracted from the `From` account +- Token is added to the `To` account + + +## MsgTransferFTFrom + +```golang +type MsgTransferFTFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + To sdk.AccAddress `json:"to"` + Amount Coins `json:"amount"` +} +``` + +**TransferFTFrom message is for `Proxy` to transfer a collective non-reserved fungible token owned by `From`** +- Signer(`Proxy`) of this message must have been approved for the collection +- Token is subtracted from the `From` account +- Token is added to the `To` account + + +## MsgTransferNFTFrom + +```golang +type MsgTransferNFTFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + To sdk.AccAddress `json:"to"` + TokenIDs []string `json:"token_ids"` +} +``` + +**TransferNFT message is for `Proxy` to transfer a collective non-fungible token owned by `From`** +- Signer(`Proxy`) of this message must have been approved for the collection +- Token is subtracted from the `From` account +- Token is added to the `To` account + + +## MsgAttach + +```golang +type MsgAttach struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + ToTokenID string `json:"to_token_id"` + TokenID string `json:"token_id"` +} +``` + +**Attach message is to attach a non-fungible token to another non-fungible token** +- Signer(`From`) of this message must have the token +- The token having `TokenID` is attached to the token having `ToTokenID` +- If the owner of the `ToToken` is different with `From`, the owner of the Token is changed to the owner of `ToToken` +- Cannot attach a child token of some other to any token + + +## MsgDetach + +```golang +type MsgDetach struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + TokenID string `json:"token_id"` +} +``` + +**Detach message is to detach a non-fungible token from another parent token** +- Signer of this message must have the token +- Cannot detach a non-child token from any token + + +## MsgAttachFrom + +```golang +type MsgAttachFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + ToTokenID string `json:"to_token_id"` + TokenID string `json:"token_id"` +} +``` + +**Attach message is for a proxy to attach a non-fungible token to another non-fungible token** +- Signer(Proxy) of this message must have been approved by From having the token +- The token having TokenID is attached to the token having ToTokenID +- If the owner of the ToToken is different with From, the owner of the Token is changed to the owner of ToToken +- Cannot attach a child token of some other to any token + + +## MsgDetachFrom + +```golang +type MsgDetachFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + TokenID string `json:"token_id"` +} +``` + +**Detach message is for a proxy to detach a non-fungible token from another parent token** +- Signer(`Proxy`) of this message must have been approved by From having the token +- Cannot detach a non-child token from any token + + +## MsgApprove + +```golang +type MsgApprove struct { + Approver sdk.AccAddress `json:"approver"` + ContractID string `json:"contract_id"` + Proxy sdk.AccAddress `json:"proxy"` +} +``` + +**Approve message is to approve a proxy to transfer, attach/detach tokens of a collection** +- `Approver` is the signer + + +## MsgDisapprove + +```golang +type MsgDisapprove struct { + Approver sdk.AccAddress `json:"approver"` + ContractID string `json:"contract_id"` + Proxy sdk.AccAddress `json:"proxy"` +} +``` + +**Disapprove message is to withdraw proxy's approval for a collection** +- `Approver` is the signer + +## MsgModify + +```golang +type MsgModify struct { + Owner sdk.AccAddress `json:"owner"` + ContractID string `json:"contract_id"` + TokenType string `json:"token_type"` + TokenIndex string `json:"token_index"` + Changes linktype.Changes `json:"changes"` +} +``` + +**Modify message is to modify fields of collection, token type, CFT or CNFT** +- `Owner` is the signer + +# Syntax +| Message/Attributes | Tag | Type | +| ---- | ---- | ---- | +| Message | collection/MsgCreate | github.com/line/link/x/collection/internal/types.MsgCreateCollection | + | Attributes | owner | []uint8 | + | Attributes | name | string | + | Attributes | meta | string | + | Attributes | base_img_uri | string | +| Message | collection/MsgIssueFT | github.com/line/link/x/collection/internal/types.MsgIssueFT | + | Attributes | owner | []uint8 | + | Attributes | contract_id | string | + | Attributes | to | []uint8 | + | Attributes | name | string | + | Attributes | meta | string | + | Attributes | amount | github.com/cosmos/cosmos-sdk/types.Int | + | Attributes | mintable | bool | + | Attributes | decimals | github.com/cosmos/cosmos-sdk/types.Int | +| Message | collection/MsgIssueNFT | github.com/line/link/x/collection/internal/types.MsgIssueNFT | + | Attributes | owner | []uint8 | + | Attributes | contract_id | string | + | Attributes | name | string | + | Attributes | meta | string | +| Message | collection/MsgMintNFT | github.com/line/link/x/collection/internal/types.MsgMintNFT | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | to | []uint8 | + | Attributes | params | []github.com/line/link/x/collection/internal/types.MintNFTParam | +| Message | collection/MsgBurnNFT | github.com/line/link/x/collection/internal/types.MsgBurnNFT | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | token_ids | []string | +| Message | collection/MsgBurnNFTFrom | github.com/line/link/x/collection/internal/types.MsgBurnNFTFrom | + | Attributes | proxy | []uint8 | + | Attributes | contract_id | string | + | Attributes | from | []uint8 | + | Attributes | token_ids | []string | +| Message | collection/MsgModify | github.com/line/link/x/collection/internal/types.MsgModify | + | Attributes | owner | []uint8 | + | Attributes | contract_id | string | + | Attributes | token_type | string | + | Attributes | token_index | string | + | Attributes | changes | []github.com/line/link/types.Change | +| Message | collection/MsgMintFT | github.com/line/link/x/collection/internal/types.MsgMintFT | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | to | []uint8 | + | Attributes | amount | []github.com/line/link/x/collection/internal/types.Coin | +| Message | collection/MsgBurnFT | github.com/line/link/x/collection/internal/types.MsgBurnFT | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | amount | []github.com/line/link/x/collection/internal/types.Coin | +| Message | collection/MsgBurnFTFrom | github.com/line/link/x/collection/internal/types.MsgBurnFTFrom | + | Attributes | proxy | []uint8 | + | Attributes | contract_id | string | + | Attributes | from | []uint8 | + | Attributes | amount | []github.com/line/link/x/collection/internal/types.Coin | +| Message | collection/MsgGrantPermission | github.com/line/link/x/collection/internal/types.MsgGrantPermission | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | to | []uint8 | + | Attributes | permission | github.com/line/link/x/collection/internal/types.Permission | +| Message | collection/MsgRevokePermission | github.com/line/link/x/collection/internal/types.MsgRevokePermission | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | permission | github.com/line/link/x/collection/internal/types.Permission | +| Message | collection/MsgTransferFT | github.com/line/link/x/collection/internal/types.MsgTransferFT | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | to | []uint8 | + | Attributes | amount | []github.com/line/link/x/collection/internal/types.Coin | +| Message | collection/MsgTransferNFT | github.com/line/link/x/collection/internal/types.MsgTransferNFT | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | to | []uint8 | + | Attributes | token_ids | []string | +| Message | collection/MsgTransferFTFrom | github.com/line/link/x/collection/internal/types.MsgTransferFTFrom | + | Attributes | proxy | []uint8 | + | Attributes | contract_id | string | + | Attributes | from | []uint8 | + | Attributes | to | []uint8 | + | Attributes | amount | []github.com/line/link/x/collection/internal/types.Coin | +| Message | collection/MsgTransferNFTFrom | github.com/line/link/x/collection/internal/types.MsgTransferNFTFrom | + | Attributes | proxy | []uint8 | + | Attributes | contract_id | string | + | Attributes | from | []uint8 | + | Attributes | to | []uint8 | + | Attributes | token_ids | []string | +| Message | collection/MsgAttach | github.com/line/link/x/collection/internal/types.MsgAttach | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | to_token_id | string | + | Attributes | token_id | string | +| Message | collection/MsgDetach | github.com/line/link/x/collection/internal/types.MsgDetach | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | token_id | string | +| Message | collection/MsgAttachFrom | github.com/line/link/x/collection/internal/types.MsgAttachFrom | + | Attributes | proxy | []uint8 | + | Attributes | contract_id | string | + | Attributes | from | []uint8 | + | Attributes | to_token_id | string | + | Attributes | token_id | string | +| Message | collection/MsgDetachFrom | github.com/line/link/x/collection/internal/types.MsgDetachFrom | + | Attributes | proxy | []uint8 | + | Attributes | contract_id | string | + | Attributes | from | []uint8 | + | Attributes | token_id | string | +| Message | collection/MsgApprove | github.com/line/link/x/collection/internal/types.MsgApprove | + | Attributes | approver | []uint8 | + | Attributes | contract_id | string | + | Attributes | proxy | []uint8 | +| Message | collection/MsgDisapprove | github.com/line/link/x/collection/internal/types.MsgDisapprove | + | Attributes | approver | []uint8 | + | Attributes | contract_id | string | + | Attributes | proxy | []uint8 | diff --git a/x/collection/spec/04_events.md b/x/collection/spec/04_events.md new file mode 100644 index 0000000000..d19ccd2743 --- /dev/null +++ b/x/collection/spec/04_events.md @@ -0,0 +1,275 @@ +# Events +**Not fully documented yet** +The token module emits the following events: + + +### MsgCreate +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | collection | +| message | sender | {ownerAddress} | +| message | action | create_collection | +| grant_perm | to | {ownerAddress} | +| grant_perm | contract_id | {symbol} | +| grant_perm | perm | issue | +| grant_perm | perm | mint | +| grant_perm | perm | burn | +| grant_perm | perm | modify | +| create_collection| contract_id | {contractID} | +| create_collection| name | {name} | +| create_collection| owner | {ownerAddress} | + +### MsgIssueFT +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | collection | +| message | sender | {ownerAddress} | +| message | action | issue_ft | +| issue_ft | contract_id | {contractID} | +| issue_ft | name | {name} | +| issue_ft | token_id | {tokenID} | +| issue_ft | owner | {ownerAddress} | +| issue_ft | to | {toAddress} | +| issue_ft | amount | {amount} | +| issue_ft | mintable | {mintable} | +| issue_ft | decimals | {decimals} | + +### MsgMintFT +| Type | Attribute Key | Attribute Value | +|------------------|----------------|------------------------------| +| message | module | collection | +| message | sender | {ownerAddress} | +| message | action | mint_ft | +| mint_ft | contract_id | {contractID} | +| mint_ft | amount | {amount}{contractID}{tokenID}| +| mint_ft | from | {fromAddress} | +| mint_ft | to | {toAddress} | + +### MsgBurnFT +| Type | Attribute Key | Attribute Value | +|------------------|----------------|------------------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | burn_ft | +| burn_ft | contract_id | {contractID} | +| burn_ft | from | {fromAddress} | +| burn_ft | amount | {amount}{contractID}{tokenID}| + +### MsgBurnFTFrom +| Type | Attribute Key | Attribute Value | +|------------------|----------------|------------------------------| +| message | module | collection | +| message | sender | {proxyAddress} | +| message | action | burn_ft | +| burn_ft_from | contract_id | {contractID} | +| burn_ft_from | proxy | {proxyAddress} | +| burn_ft_from | from | {fromAddress} | +| burn_ft_from | amount | {amount}{contractID}{tokenID}| + +### MsgIssueNFT +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | issue_nft | +| issue_nft | contract_id | {contractID} | +| issue_nft | token_type | {tokentype} | + +### MsgMintNFT +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | mint_nft | +| mint_nft | contract_id | {contractID} | +| mint_nft | name | {name} | +| mint_nft | token_id | {tokenID} | +| mint_nft | from | {fromAddress} | +| mint_nft | to | {toAddress} | + +### MsgBurnNFT +| Type | Attribute Key | Attribute Value | +|--------------------|----------------|------------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | burn_nft | +| burn_nft | from | {fromAddress} | +| burn_nft | contract_id | {contractID} | +| burn_nft | token_id | {token_id} | +| operation_burn_nft | token_id | {token_id} | + +### MsgBurnNFTFrom +| Type | Attribute Key | Attribute Value | +|--------------------|----------------|------------------------| +| message | module | collection | +| message | sender | {proxyAddress} | +| message | action | burn_nft _from | +| burn_nft_from | contract_id | {contractID} | +| burn_nft_from | proxy | {proxyAddress} | +| burn_nft_from | from | {fromAddress} | +| burn_nft_from | token_id | {token_id} | +| operation_burn_nft | token_id | {token_id} | + +### MsgGrantPermission +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | grant_permission | +| grant_perm | from | {fromAddress} | +| grant_perm | to | {toAddress} | +| grant_perm | contract_id | {resource} | +| grant_perm | perm | issue/mint/burn/modify | + +### MsgRevokePermission +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | revoke_permission | +| revoke_perm | from | {fromAddress} | +| revoke_perm | contract_id | {resource} | +| revoke_perm | perm | issue/mint/burn/modify | + +### MsgTransferFT +| Type | Attribute Key | Attribute Value | +|------------------|----------------|-------------------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | transfer_ft | +| transfer_ft | contract_id | {contractID} | +| transfer_ft | from | {fromAddress} | +| transfer_ft | to | {toAddress} | +| transfer_ft | amount | {amount}{contractID}{tokenID} | + +### MsgTransferFTFrom +| Type | Attribute Key | Attribute Value | +|-------------------|----------------|-------------------------------| +| message | module | collection | +| message | sender | {proxyAddress} | +| message | action | transfer_ft_from | +| transfer_ft_from | contract_id | {contractID} | +| transfer_ft_from | proxy | {proxyAddress} | +| transfer_ft_from | from | {fromAddress} | +| transfer_ft_from | to | {toAddress} | +| transfer_ft_from | amount | {amount}{contractID}{tokenID} | + +### MsgTransferNFT +| Type | Attribute Key | Attribute Value | +|------------------------|----------------|-----------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | transfer_nft | +| transfer_nft | contract_id | {contractID} | +| transfer_nft | from | {fromAddress} | +| transfer_nft | to | {toAddress} | +| transfer_nft | token_id | {tokenID} | +| operation_transfer_nft | token_id | {tokenID} | + +### MsgTransferNFTFrom +| Type | Attribute Key | Attribute Value | +|------------------------|----------------|-----------------------| +| message | module | collection | +| message | sender | {proxyAddress} | +| message | action | transfer_nft_from | +| transfer_nft_from | contract_id | {contractID} | +| transfer_nft_from | proxy | {proxyAddress} | +| transfer_nft_from | from | {fromAddress} | +| transfer_nft_from | to | {toAddress} | +| transfer_nft_from | token_id | {tokenID} | +| operation_transfer_nft | token_id | {tokenID} | + +### MsgAttach +| Type | Attribute Key | Attribute Value | +|------------------------|-------------------|------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | attach | +| attach | contract_id | {contractID} | +| attach | from | {fromAddress} | +| attach | to_token_id | {toTokenID} | +| attach | token_id | {tokenID} | +| attach | old_root_token_id | {oldRootTokenID} | +| attach | new_root_token_id | {newRootTokenID} | +| operation_root_changed | token_id | {tokenID} | + +### MsgDetach +| Type | Attribute Key | Attribute Value | +|------------------------|-------------------|------------------| +| message | module | collection | +| message | sender | {fromAddress} | +| message | action | detach | +| detach | contract_id | {contractID} | +| detach | from | {fromAddress} | +| detach | from_token_id | {fromTokenID} | +| detach | token_id | {tokenID} | +| detach | old_root_token_id | {oldRootTokenID} | +| detach | new_root_token_id | {newRootTokenID} | +| operation_root_changed | token_id | {tokenID} | + +### MsgAttachFrom +| Type | Attribute Key | Attribute Value | +|------------------------|-------------------|------------------| +| message | module | collection | +| message | sender | {proxyAddress} | +| message | action | attach_from | +| attach_from | contract_id | {contractID} | +| attach_from | proxy | {proxyAddress} | +| attach_from | from | {fromAddress} | +| attach_from | to_token_id | {toTokenID} | +| attach_from | token_id | {tokenID} | +| attach_from | old_root_token_id | {oldRootTokenID} | +| attach_from | new_root_token_id | {newRootTokenID} | +| operation_root_changed | token_id | {tokenID} | + +### MsgDetachFrom +| Type | Attribute Key | Attribute Value | +|------------------------|-------------------|------------------| +| message | module | collection | +| message | sender | {proxyAddress} | +| message | action | detach_from | +| detach_from | contract_id | {contractID} | +| detach_from | proxy | {proxyAddress} | +| detach_from | from | {fromAddress} | +| detach_from | from_token_id | {fromTokenID} | +| detach_from | token_id | {tokenID} | +| detach_from | old_root_token_id | {oldRootTokenID} | +| detach_from | new_root_token_id | {newRootTokenID} | +| operation_root_changed | token_id | {tokenID} | + +### MsgApprove +| Type | Attribute Key | Attribute Value | +|--------------------|----------------|------------------------| +| message | module | collection | +| message | sender | {approverAddress} | +| message | action | approve_collection | +| approve_collection | contract_id | {contractID} | +| approve_collection | approver | {approverAddress} | +| approve_collection | proxy | {proxyAddress} | + +### MsgDisapprove +| Type | Attribute Key | Attribute Value | +|-----------------------|----------------|-----------------------| +| message | module | collection | +| message | sender | {approverAddress} | +| message | action | disapprove_collection | +| disapprove_collection | contract_id | {contractID} | +| disapprove_collection | approver | {approverAddress} | +| disapprove_collection | proxy | {proxyAddress} | + +### MsgModify +| Type | Attribute Key | Attribute Value | +|-----------------------|----------------|-----------------------| +| message | module | collection | +| message | sender | {ownerAddress} | +| message | action | modify_collection | +| message | action | modify_token_type | +| message | action | modify_token | +| modify_collection | contract_id | {contract_id} | +| modify_collection | {modifiedField}| {modifiedValue} | +| modify_token_type | contract_id | {contract_id} | +| modify_token_type | token_type | {token_type} | +| modify_token_type | {modifiedField}| {modifiedValue} | +| modify_token | contract_id | {contract_id} | +| modify_token | token_id | {token_id} | +| modify_token | {modifiedField}| {modifiedValue} | diff --git a/x/collection/spec/README.md b/x/collection/spec/README.md new file mode 100644 index 0000000000..e0c31c380d --- /dev/null +++ b/x/collection/spec/README.md @@ -0,0 +1,14 @@ +# Token module specification + + + +## Abstract + +This document specifies the token module of the Link Network. + +## Contents + +1. **[Concept](01_concept.md)** +2. **[Keepers](02_keepers.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** diff --git a/x/contract/alias.go b/x/contract/alias.go new file mode 100644 index 0000000000..90ba47dbf9 --- /dev/null +++ b/x/contract/alias.go @@ -0,0 +1,24 @@ +package contract + +import ( + "github.com/line/lbm-sdk/v2/x/contract/internal/keeper" + "github.com/line/lbm-sdk/v2/x/contract/internal/types" +) + +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey + SampleContractID = "abcde012" +) + +type ( + Msg = types.ContractMsg + Keeper = keeper.ContractKeeper + CtxKey = types.CtxKey +) + +var ( + ErrInvalidContractID = types.ErrInvalidContractID + ErrContractNotExist = types.ErrContractNotExist + NewContractKeeper = keeper.NewContractKeeper +) diff --git a/x/contract/internal/keeper/keeper.go b/x/contract/internal/keeper/keeper.go new file mode 100644 index 0000000000..39fc2651a2 --- /dev/null +++ b/x/contract/internal/keeper/keeper.go @@ -0,0 +1,106 @@ +package keeper + +import ( + "encoding/binary" + "fmt" + "hash/fnv" + "regexp" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/contract/internal/types" +) + +/*** + +Actual generated contractID values from genesis + +9be17165 +678c146a +3336b76f +fee15a74 +ca8bfd79 +9636a07e +61e14383 +2d8be688 +f936898d +c4e12c92 +... + +*/ + +const ( + IDRegExprString = "[a-f0-9]{8}" +) + +var ( + IDRegExpr = regexp.MustCompile(fmt.Sprintf("^%s$", IDRegExprString)) +) + +type ContractKeeper interface { + NewContractID(ctx sdk.Context) string + HasContractID(ctx sdk.Context, contractID string) bool + DeleteContractID(ctx sdk.Context, contractID string) +} + +func NewContractKeeper(cdc *codec.Codec, storeKey sdk.StoreKey) ContractKeeper { + return BaseContractKeeper{ + storeKey: storeKey, + cdc: cdc, + } +} + +type BaseContractKeeper struct { + storeKey sdk.StoreKey + cdc *codec.Codec +} + +var _ ContractKeeper = (*BaseContractKeeper)(nil) + +func VerifyContractID(contractID string) bool { + return IDRegExpr.MatchString(contractID) +} + +func (k BaseContractKeeper) NewContractID(ctx sdk.Context) string { + store := ctx.KVStore(k.storeKey) + + nextCount := uint64(0) + if store.Has(types.LastContractCountStoreKey()) { + b := store.Get(types.LastContractCountStoreKey()) + k.cdc.MustUnmarshalBinaryBare(b, &nextCount) + nextCount++ + } + var id string + hash := fnv.New32() + for ok := false; !ok; { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, nextCount) + _, err := hash.Write(b) + if err != nil { + panic("hash should not fail") + } + id = fmt.Sprintf("%x", hash.Sum32()) + if len(id) < 8 { + id = "00000000"[len(id):] + id + } + if store.Has(types.ContractIDStoreKey(id)) { + nextCount++ + } else { + ok = true + } + } + + store.Set(types.LastContractCountStoreKey(), k.cdc.MustMarshalBinaryBare(nextCount)) + store.Set(types.ContractIDStoreKey(id), k.cdc.MustMarshalBinaryBare(nextCount)) + return id +} + +func (k BaseContractKeeper) HasContractID(ctx sdk.Context, contractID string) bool { + store := ctx.KVStore(k.storeKey) + return store.Has(types.ContractIDStoreKey(contractID)) +} + +func (k BaseContractKeeper) DeleteContractID(ctx sdk.Context, contractID string) { + store := ctx.KVStore(k.storeKey) + store.Delete(types.ContractIDStoreKey(contractID)) +} diff --git a/x/contract/internal/keeper/keeper_test.go b/x/contract/internal/keeper/keeper_test.go new file mode 100644 index 0000000000..3cdd3c9d0c --- /dev/null +++ b/x/contract/internal/keeper/keeper_test.go @@ -0,0 +1,51 @@ +package keeper + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/contract/internal/types" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" +) + +type testInput struct { + cdc *codec.Codec + ctx sdk.Context + keeper ContractKeeper +} + +func newTestCodec() *codec.Codec { + cdc := codec.New() + sdk.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + return cdc +} + +func setupTestInput(t *testing.T) testInput { + keyContract := sdk.NewKVStoreKey(types.StoreKey) + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyContract, sdk.StoreTypeIAVL, db) + err := ms.LoadLatestVersion() + require.NoError(t, err) + + cdc := newTestCodec() + + keeper := NewContractKeeper(cdc, keyContract) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "test-chain-id"}, false, log.NewNopLogger()) + return testInput{cdc: cdc, ctx: ctx, keeper: keeper} +} + +func TestKeeper(t *testing.T) { + testInput := setupTestInput(t) + _, ctx, keeper := testInput.cdc, testInput.ctx, testInput.keeper + for i := 0; i < 10000; i++ { + contractID := keeper.NewContractID(ctx) + require.True(t, VerifyContractID(contractID)) + } +} diff --git a/x/contract/internal/types/errors.go b/x/contract/internal/types/errors.go new file mode 100644 index 0000000000..d1aa1dc912 --- /dev/null +++ b/x/contract/internal/types/errors.go @@ -0,0 +1,10 @@ +package types + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var ( + ErrInvalidContractID = sdkerrors.Register(ModuleName, 1, "invalid contractID") + ErrContractNotExist = sdkerrors.Register(ModuleName, 2, "contract does not exist") +) diff --git a/x/contract/internal/types/types.go b/x/contract/internal/types/types.go new file mode 100644 index 0000000000..92089cbbf9 --- /dev/null +++ b/x/contract/internal/types/types.go @@ -0,0 +1,31 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + ModuleName = "contract" + + StoreKey = ModuleName +) + +var ( + LastContractCountStoreKeyPrefix = []byte{0x01} + ContractIDStoreKeyPrefix = []byte{0x02} +) + +func LastContractCountStoreKey() []byte { + return LastContractCountStoreKeyPrefix +} + +func ContractIDStoreKey(contractID string) []byte { + return append(ContractIDStoreKeyPrefix, []byte(contractID)...) +} + +type ContractMsg interface { + sdk.Msg + GetContractID() string +} + +type CtxKey struct{} diff --git a/x/contract/validator.go b/x/contract/validator.go new file mode 100644 index 0000000000..5285289660 --- /dev/null +++ b/x/contract/validator.go @@ -0,0 +1,14 @@ +package contract + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract/internal/keeper" + "github.com/line/lbm-sdk/v2/x/contract/internal/types" +) + +func ValidateContractIDBasic(contract Msg) error { + if !keeper.VerifyContractID(contract.GetContractID()) { + return sdkerrors.Wrapf(types.ErrInvalidContractID, "ContractID: %s", contract.GetContractID()) + } + return nil +} diff --git a/x/genesis/alias.go b/x/genesis/alias.go new file mode 100644 index 0000000000..66609344c4 --- /dev/null +++ b/x/genesis/alias.go @@ -0,0 +1,21 @@ +package genesis + +import ( + "github.com/line/lbm-sdk/v2/x/genesis/internal/keeper" + "github.com/line/lbm-sdk/v2/x/genesis/internal/types" +) + +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey +) + +type ( + Keeper = keeper.Keeper +) + +var ( + ModuleCdc = types.ModuleCdc + NewKeeper = keeper.NewKeeper +) diff --git a/x/genesis/genesis.go b/x/genesis/genesis.go new file mode 100644 index 0000000000..4a66c333a0 --- /dev/null +++ b/x/genesis/genesis.go @@ -0,0 +1,28 @@ +package genesis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// nolint:golint +type GenesisState struct { + GenesisMessage string `json:"genesis_message"` +} + +func NewGenesisState(genesisMessage string) GenesisState { + return GenesisState{GenesisMessage: genesisMessage} +} + +func DefaultGenesisState() GenesisState { + return NewGenesisState("In the beginning God created the heavens and the earth.") +} + +func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { + keeper.SetGenesisMessage(ctx, data.GenesisMessage) +} + +func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + return NewGenesisState(keeper.GetGenesisMessage(ctx)) +} + +func ValidateGenesis(data GenesisState) error { return nil } diff --git a/x/genesis/internal/keeper/keeper.go b/x/genesis/internal/keeper/keeper.go new file mode 100644 index 0000000000..acd962cd4f --- /dev/null +++ b/x/genesis/internal/keeper/keeper.go @@ -0,0 +1,33 @@ +package keeper + +import ( + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/genesis/internal/types" +) + +func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey) Keeper { + return Keeper{ + storeKey: storeKey, + cdc: cdc, + } +} + +type Keeper struct { + storeKey sdk.StoreKey + cdc *codec.Codec +} + +func (k Keeper) SetGenesisMessage(ctx sdk.Context, genesisMessage string) { + store := ctx.KVStore(k.storeKey) + store.Set(types.GenesisKeyPrefix, []byte(genesisMessage)) +} + +func (k Keeper) GetGenesisMessage(ctx sdk.Context) string { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.GenesisKeyPrefix) + if bz == nil { + return "" + } + return string(bz) +} diff --git a/x/genesis/internal/types/codec.go b/x/genesis/internal/types/codec.go new file mode 100644 index 0000000000..9029d3aed9 --- /dev/null +++ b/x/genesis/internal/types/codec.go @@ -0,0 +1,10 @@ +package types + +import "github.com/cosmos/cosmos-sdk/codec" + +var ModuleCdc *codec.Codec + +func init() { + ModuleCdc = codec.New() + ModuleCdc.Seal() +} diff --git a/x/genesis/internal/types/types.go b/x/genesis/internal/types/types.go new file mode 100644 index 0000000000..ffde251b0e --- /dev/null +++ b/x/genesis/internal/types/types.go @@ -0,0 +1,11 @@ +package types + +const ( + ModuleName = "genesis" + StoreKey = ModuleName + RouterKey = ModuleName +) + +var ( + GenesisKeyPrefix = []byte{0x01} +) diff --git a/x/genesis/module.go b/x/genesis/module.go new file mode 100644 index 0000000000..7bd03009b7 --- /dev/null +++ b/x/genesis/module.go @@ -0,0 +1,111 @@ +package genesis + +import ( + "encoding/json" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/line/lbm-sdk/v2/x/genesis/internal/keeper" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// app module basics object +type AppModuleBasic struct{} + +// module name +func (AppModuleBasic) Name() string { return ModuleName } + +// register module codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {} + +// default genesis state +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) +} + +// module validate genesis +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var data GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &data) + if err != nil { + return err + } + return ValidateGenesis(data) +} + +// register rest routes +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { +} + +// get the root tx command of this module +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { return nil } + +// get the root query command of this module +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { return nil } + +// ___________________________ +// app module +type AppModule struct { + AppModuleBasic + keeper keeper.Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper keeper.Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// module name +func (AppModule) Name() string { return ModuleName } + +// register invariants +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// module message route name +func (AppModule) Route() string { return RouterKey } + +// module handler +func (am AppModule) NewHandler() sdk.Handler { return nil } + +// module querier route name +func (AppModule) QuerierRoute() string { return RouterKey } + +// module querier +func (am AppModule) NewQuerierHandler() sdk.Querier { return nil } + +// module init-genesis +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + ModuleCdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.keeper, genesisState) + return []abci.ValidatorUpdate{} +} + +// module export genesis +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return ModuleCdc.MustMarshalJSON(gs) +} + +// module begin-block +func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// module end-block +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/ibc/testing/chain.go b/x/ibc/testing/chain.go index 6760c8ef22..5f17ca0ff8 100644 --- a/x/ibc/testing/chain.go +++ b/x/ibc/testing/chain.go @@ -581,11 +581,9 @@ func (chain *TestChain) CreateTMClientHeader(chainID string, blockHeight int64, Commit: commit.ToProto(), } - if tmValSet != nil { - valSet, err = tmValSet.ToProto() - if err != nil { - panic(err) - } + valSet, err = tmValSet.ToProto() + if err != nil { + panic(err) } if tmTrustedVals != nil { diff --git a/x/token/alias.go b/x/token/alias.go new file mode 100644 index 0000000000..f306b5689a --- /dev/null +++ b/x/token/alias.go @@ -0,0 +1,63 @@ +package token + +import ( + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/querier" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey + RouterKey = types.RouterKey + EncodeRouterKey = types.EncodeRouterKey +) + +type ( + MsgIssue = types.MsgIssue + MsgTransfer = types.MsgTransfer + MsgMint = types.MsgMint + MsgBurn = types.MsgBurn + MsgModify = types.MsgModify + + Account = types.Account + Token = types.Token + Permissions = types.Permissions + Keeper = keeper.Keeper + Permission = types.Permission + + EncodeHandler = types.EncodeHandler + EncodeQuerier = types.EncodeQuerier +) + +var ( + NewMsgIssue = types.NewMsgIssue + NewMsgMint = types.NewMsgMint + NewMsgBurn = types.NewMsgBurn + NewMsgBurnFrom = types.NewMsgBurnFrom + NewMsgTransfer = types.NewMsgTransfer + NewMsgApprove = types.NewMsgApprove + NewMsgTransferFrom = types.NewMsgTransferFrom + NewMsgModify = types.NewMsgModify + NewChangesWithMap = types.NewChangesWithMap + NewMsgGrantPermission = types.NewMsgGrantPermission + NewMsgRevokePermission = types.NewMsgRevokePermission + ModuleCdc = types.ModuleCdc + RegisterCodec = types.RegisterCodec + NewToken = types.NewToken + NewKeeper = keeper.NewKeeper + NewQuerier = querier.NewQuerier + + NewMintPermission = types.NewMintPermission + NewBurnPermission = types.NewBurnPermission + NewModifyPermission = types.NewModifyPermission + + NewMsgEncodeHandler = keeper.NewMsgEncodeHandler + NewQueryEncoder = querier.NewQueryEncoder + + NewChanges = types.NewChanges + NewChange = types.NewChange + + ErrTokenNotExist = types.ErrTokenNotExist + ErrInsufficientBalance = types.ErrInsufficientBalance +) diff --git a/x/token/client/cli/query.go b/x/token/client/cli/query.go new file mode 100644 index 0000000000..7e23659dff --- /dev/null +++ b/x/token/client/cli/query.go @@ -0,0 +1,197 @@ +package cli + +import ( + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/line/lbm-sdk/v2/x/token/client/internal/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/spf13/cobra" +) + +func GetQueryCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Querying commands for the token module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + cmd.AddCommand( + GetTokenCmd(cdc), + GetBalanceCmd(cdc), + GetTotalCmd(cdc), + GetPermsCmd(cdc), + GetIsApprovedCmd(cdc), + GetApproversCmd(cdc), + ) + + return cmd +} + +func GetTokenCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "token [contract_id]", + Short: "Query token with its contract_id", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + token, height, err := retriever.GetToken(cliCtx, contractID) + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + + return cliCtx.PrintOutput(token) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetBalanceCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "balance [contract_id] [addr]", + Short: "Query balance of the account", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + addr, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + supply, height, err := retriever.GetAccountBalance(cliCtx, contractID, addr) + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(supply) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetTotalCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "total [supply|mint|burn] [contract_id] ", + Short: "Query total supply/mint/burn of token", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + target := args[0] + contractID := args[1] + + supply, height, err := retriever.GetTotal(cliCtx, contractID, target) + + if err != nil { + return err + } + + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(supply) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetPermsCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "perm [addr] [contract_id]", + Short: "Get Permission of the Account", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + addr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + contractID := args[1] + pms, height, err := retriever.GetAccountPermission(cliCtx, contractID, addr) + if err != nil { + return err + } + cliCtx = cliCtx.WithHeight(height) + return cliCtx.PrintOutput(pms) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetIsApprovedCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "approved [contract_id] [proxy] [approver]", + Short: "Query whether a proxy is approved by approver on a token", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + + proxy, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + approver, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + approved, height, err := retriever.IsApproved(cliCtx, contractID, proxy, approver) + if err != nil { + return err + } + + return cliCtx.WithHeight(height).PrintOutput(approved) + }, + } + + return flags.GetCommands(cmd)[0] +} + +func GetApproversCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "approvers [contract_id] [proxy]", + Short: "Query approvers by the proxy", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + retriever := clienttypes.NewRetriever(cliCtx) + + contractID := args[0] + + proxy, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + approvers, height, err := retriever.GetApprovers(cliCtx, contractID, proxy) + if err != nil { + return err + } + + return cliCtx.WithHeight(height).PrintOutput(approvers) + }, + } + + return flags.GetCommands(cmd)[0] +} diff --git a/x/token/client/cli/tx.go b/x/token/client/cli/tx.go new file mode 100644 index 0000000000..580ff0bde5 --- /dev/null +++ b/x/token/client/cli/tx.go @@ -0,0 +1,364 @@ +package cli + +import ( + "bufio" + "errors" + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/spf13/viper" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" +) + +var ( + flagTotalSupply = "total-supply" + flagDecimals = "decimals" + flagMintable = "mintable" + flagMeta = "meta" + flagImageURI = "image-uri" +) + +const ( + DefaultDecimals = 8 + DefaultTotalSupply = 1 +) + +// GetTxCmd returns the transaction commands for this module +func GetTxCmd(cdc *codec.Codec) *cobra.Command { + txCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Token transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + txCmd.AddCommand( + IssueTxCmd(cdc), + TransferTxCmd(cdc), + MintTxCmd(cdc), + BurnTxCmd(cdc), + GrantPermTxCmd(cdc), + RevokePermTxCmd(cdc), + ModifyTokenCmd(cdc), + TransferFromTxCmd(cdc), + ApproveTokenTxCmd(cdc), + BurnFromTxCmd(cdc), + ) + return txCmd +} + +func IssueTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "issue [from_key_or_address] [to] [name] [symbol]", + Short: "Create and sign an issue token tx", + Long: ` +[Issue a token command] +To query or send the token, you should remember the contract id + + +[Fungible Token] +linkcli tx token issue [from_key_or_address] [to] [name] [symbol] +--decimals=[decimals] +--mintable=[mintable] +--total-supply=[initial amount of the token] +--meta=[metadata for the token] +--image-uri=[image uri for the token] +`, + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + from := cliCtx.FromAddress + to, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + name := args[2] + symbol := args[3] + supply := viper.GetInt64(flagTotalSupply) + decimals := viper.GetInt64(flagDecimals) + mintable := viper.GetBool(flagMintable) + meta := viper.GetString(flagMeta) + imageURI := viper.GetString(flagImageURI) + + if decimals < 0 || decimals > 18 { + return errors.New("invalid decimals. 0 <= decimals <= 18") + } + + msg := types.NewMsgIssue(from, to, name, symbol, meta, imageURI, sdk.NewInt(supply), sdk.NewInt(decimals), mintable) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().Int64(flagTotalSupply, DefaultTotalSupply, "total supply") + cmd.Flags().Int64(flagDecimals, DefaultDecimals, "set decimals") + cmd.Flags().Bool(flagMintable, false, "set mintable") + cmd.Flags().String(flagMeta, "", "set meta") + cmd.Flags().String(flagImageURI, "", "set img-uri") + + return flags.PostCommands(cmd)[0] +} + +func TransferTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "transfer [from_key_or_address] [to_address] [contract_id] [amount]", + Short: "Create and sign a tx transferring non-reserved fungible tokens", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + to, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return err + } + + amount, ok := sdk.NewIntFromString(args[3]) + if !ok { + return sdkerrors.Wrap(types.ErrInvalidAmount, args[3]) + } + + msg := types.NewMsgTransfer(cliCtx.GetFromAddress(), to, args[2], amount) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd = flags.PostCommands(cmd)[0] + + return cmd +} + +func MintTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "mint [from_key_or_address] [contract_id] [to] [amount]", + Short: "Create and sign a mint token tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + to, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + amount, err := strconv.Atoi(args[3]) + if err != nil { + return err + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgMint(cliCtx.GetFromAddress(), contractID, to, sdk.NewInt(int64(amount))) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func BurnTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "burn [from_key_or_address] [contract_id] [amount]", + Short: "Create and sign a burn token tx", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + amount, ok := sdk.NewIntFromString(args[2]) + if !ok { + return sdkerrors.Wrap(types.ErrInvalidAmount, args[4]) + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgBurn(cliCtx.GetFromAddress(), contractID, amount) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func GrantPermTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "grant [from_key_or_address] [contract_id] [to] [action]", + Short: "Create and sign a grant permission for token tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + to, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + perm := types.Permission(args[3]) + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgGrantPermission(cliCtx.GetFromAddress(), contractID, to, perm) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func RevokePermTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "revoke [from_key_or_address] [contract_id] [action]", + Short: "Create and sign a revoke permission for token tx", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + perm := types.Permission(args[2]) + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgRevokePermission(cliCtx.GetFromAddress(), contractID, perm) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func ModifyTokenCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "modify [owner_address] [contract_id] [field] [new_value]", + Short: "Create and sign a modify token tx", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + contractID := args[1] + field := args[2] + newValue := args[3] + + msg := types.NewMsgModify( + cliCtx.FromAddress, + contractID, + types.NewChanges(types.NewChange(field, newValue)), + ) + err := msg.ValidateBasic() + if err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} + +func TransferFromTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "transfer-from [proxy_key_or_address] [contract_id] [from_address] [to_address] [amount]", + Short: "Create and sign a tx transferring tokens by approved proxy", + Args: cobra.ExactArgs(5), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + from, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + to, err := sdk.AccAddressFromBech32(args[3]) + if err != nil { + return err + } + + amount, ok := sdk.NewIntFromString(args[4]) + if !ok { + return sdkerrors.Wrap(types.ErrInvalidAmount, args[4]) + } + + msg := types.NewMsgTransferFrom(cliCtx.GetFromAddress(), contractID, from, to, amount) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd = flags.PostCommands(cmd)[0] + + return cmd +} + +func ApproveTokenTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "approve [approver_key_or_address] [contract_id] [proxy_address]", + Short: "Create and sign a tx approve all token operations of a token to a proxy", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + + contractID := args[1] + + proxy, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + + msg := types.NewMsgApprove(cliCtx.GetFromAddress(), contractID, proxy) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} +func BurnFromTxCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "burn-from [proxy_key_or_address] [contract_id] [from_address] [amount]", + Short: "Create and sign a burn token tx by approved proxy", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) + contractID := args[1] + from, err := sdk.AccAddressFromBech32(args[2]) + if err != nil { + return err + } + amount, ok := sdk.NewIntFromString(args[3]) + if !ok { + return errors.New("invalid amount") + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.NewMsgBurnFrom(cliCtx.GetFromAddress(), contractID, from, amount) + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + return flags.PostCommands(cmd)[0] +} diff --git a/x/token/client/internal/types/retriever.go b/x/token/client/internal/types/retriever.go new file mode 100644 index 0000000000..6fa335cf0d --- /dev/null +++ b/x/token/client/internal/types/retriever.go @@ -0,0 +1,124 @@ +package types + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +type Retriever struct { + querier types.NodeQuerier +} + +func NewRetriever(querier types.NodeQuerier) Retriever { + return Retriever{querier: querier} +} + +func (r Retriever) query(path, contractID string, data []byte) ([]byte, int64, error) { + return r.querier.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", types.QuerierRoute, path, contractID), data) +} + +func (r Retriever) GetAccountPermission(ctx context.CLIContext, contractID string, addr sdk.AccAddress) (types.Permissions, int64, error) { + var pms types.Permissions + bs, err := ctx.Codec.MarshalJSON(types.NewQueryContractIDAccAddressParams(addr)) + if err != nil { + return pms, 0, err + } + + res, height, err := r.query(types.QueryPerms, contractID, bs) + if err != nil { + return pms, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &pms); err != nil { + return pms, height, err + } + + return pms, height, nil +} + +func (r Retriever) GetAccountBalance(ctx context.CLIContext, contractID string, addr sdk.AccAddress) (sdk.Int, int64, error) { + var supply sdk.Int + bs, err := ctx.Codec.MarshalJSON(types.NewQueryContractIDAccAddressParams(addr)) + if err != nil { + return supply, 0, err + } + + res, height, err := r.query(types.QueryBalance, contractID, bs) + if err != nil { + return supply, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &supply); err != nil { + return supply, height, err + } + + return supply, height, nil +} +func (r Retriever) GetTotal(ctx context.CLIContext, contractID string, target string) (sdk.Int, int64, error) { + var total sdk.Int + + res, height, err := r.query(target, contractID, nil) + if err != nil { + return total, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &total); err != nil { + return total, height, err + } + + return total, height, nil +} + +func (r Retriever) GetToken(ctx context.CLIContext, contractID string) (types.Token, int64, error) { + var token types.Token + + res, height, err := r.query(types.QueryTokens, contractID, nil) + if err != nil { + return token, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &token); err != nil { + return token, height, err + } + return token, height, nil +} + +func (r Retriever) IsApproved(ctx context.CLIContext, contractID string, proxy sdk.AccAddress, approver sdk.AccAddress) (approved bool, height int64, err error) { + bs, err := types.ModuleCdc.MarshalJSON(types.NewQueryIsApprovedParams(proxy, approver)) + if err != nil { + return false, 0, err + } + + res, height, err := r.query(types.QueryIsApproved, contractID, bs) + if err != nil { + return false, 0, err + } + + err = ctx.Codec.UnmarshalJSON(res, &approved) + if err != nil { + return false, 0, err + } + + return approved, height, nil +} + +func (r Retriever) GetApprovers(ctx context.CLIContext, contractID string, proxy sdk.AccAddress) (accAdds []sdk.AccAddress, height int64, err error) { + bs, err := ctx.Codec.MarshalJSON(types.NewQueryApproverParams(proxy)) + if err != nil { + return accAdds, 0, err + } + + res, height, err := r.query(types.QueryApprovers, contractID, bs) + if err != nil { + return accAdds, height, err + } + + if err := ctx.Codec.UnmarshalJSON(res, &accAdds); err != nil { + return accAdds, height, err + } + + return accAdds, height, nil +} diff --git a/x/token/client/rest/query.go b/x/token/client/rest/query.go new file mode 100644 index 0000000000..74f0e713d5 --- /dev/null +++ b/x/token/client/rest/query.go @@ -0,0 +1,206 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/line/lbm-sdk/v2/x/token/client/internal/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/types/rest" +) + +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc("/token/{contract_id}/supply", QueryTotalRequestHandlerFn(cliCtx, types.QuerySupply)).Methods("GET") + r.HandleFunc("/token/{contract_id}/mint", QueryTotalRequestHandlerFn(cliCtx, types.QueryMint)).Methods("GET") + r.HandleFunc("/token/{contract_id}/burn", QueryTotalRequestHandlerFn(cliCtx, types.QueryBurn)).Methods("GET") + r.HandleFunc("/token/{contract_id}/accounts/{address}/balance", QueryBalanceRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/token/{contract_id}/accounts/{address}/permissions", QueryPermRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/token/{contract_id}/token", QueryTokenRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/token/{contract_id}/accounts/{address}/proxies/{approver}", QueryIsApprovedRequestHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc("/token/{contract_id}/accounts/{address}/approvers", QueryApproversRequestHandlerFn(cliCtx)).Methods("GET") +} + +func QueryTokenRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + token, height, err := retriever.GetToken(cliCtx, contractID) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, token) + } +} + +func QueryBalanceRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + addr, err := sdk.AccAddressFromBech32(vars["address"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + supply, height, err := retriever.GetAccountBalance(cliCtx, contractID, addr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, supply) + } +} + +func QueryTotalRequestHandlerFn(cliCtx context.CLIContext, target string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + total, height, err := retriever.GetTotal(cliCtx, contractID, target) + + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, total) + } +} + +func QueryPermRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + addr, err := sdk.AccAddressFromBech32(vars["address"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("address cannot parsed: %s", err)) + return + } + contractID := vars["contract_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + pms, height, err := retriever.GetAccountPermission(cliCtx, contractID, addr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, pms) + } +} + +func QueryIsApprovedRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + + proxy, err := sdk.AccAddressFromBech32(vars["address"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("proxy[%s] cannot parsed: %s", proxy.String(), err)) + return + } + + approver, err := sdk.AccAddressFromBech32(vars["approver"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("approver[%s] cannot parsed: %s", approver.String(), err)) + return + } + + contractID := vars["contract_id"] + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + approved, height, err := retriever.IsApproved(cliCtx, contractID, proxy, approver) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, approved) + } +} + +func QueryApproversRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + vars := mux.Vars(r) + contractID := vars["contract_id"] + + proxy, err := sdk.AccAddressFromBech32(vars["address"]) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("proxy[%s] cannot parsed: %s", proxy.String(), err)) + return + } + + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + retriever := clienttypes.NewRetriever(cliCtx) + + approvers, height, err := retriever.GetApprovers(cliCtx, contractID, proxy) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + + rest.PostProcessResponse(w, cliCtx, approvers) + } +} diff --git a/x/token/genesis.go b/x/token/genesis.go new file mode 100644 index 0000000000..4c8d2b80a0 --- /dev/null +++ b/x/token/genesis.go @@ -0,0 +1,28 @@ +package token + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type GenesisState struct { + Tokens []Token `json:"tokens"` + // TODO: approvals +} + +func NewGenesisState(tokens []Token) GenesisState { + return GenesisState{Tokens: tokens} +} + +func DefaultGenesisState() GenesisState { return NewGenesisState(nil) } + +func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState) { + // TODO: fill it with permission +} + +func ExportGenesis(ctx sdk.Context, keeper Keeper) GenesisState { + return NewGenesisState(keeper.GetAllTokens(ctx)) +} + +func ValidateGenesis(data GenesisState) error { return nil } + +// TODO: validate diff --git a/x/token/internal/handler/handler.go b/x/token/internal/handler/handler.go new file mode 100644 index 0000000000..c01c33e8fc --- /dev/null +++ b/x/token/internal/handler/handler.go @@ -0,0 +1,61 @@ +package handler + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func NewHandler(keeper keeper.Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + if msg, ok := msg.(contract.Msg); ok { + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, msg.GetContractID())) + err := handleMsgContract(ctx, keeper, msg) + if err != nil { + return nil, err + } + } + if _, ok := msg.(types.MsgIssue); ok { + contractID := keeper.NewContractID(ctx) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, contractID)) + } + if ctx.Context().Value(contract.CtxKey{}) == nil { + panic("contract id does not set") + } + switch msg := msg.(type) { + case types.MsgIssue: + return handleMsgIssue(ctx, keeper, msg) + case types.MsgMint: + return handleMsgMint(ctx, keeper, msg) + case types.MsgBurn: + return handleMsgBurn(ctx, keeper, msg) + case types.MsgBurnFrom: + return handleMsgBurnFrom(ctx, keeper, msg) + case types.MsgTransfer: + return handleMsgTransfer(ctx, keeper, msg) + case types.MsgGrantPermission: + return handleMsgGrant(ctx, keeper, msg) + case types.MsgRevokePermission: + return handleMsgRevoke(ctx, keeper, msg) + case types.MsgModify: + return handleMsgModify(ctx, keeper, msg) + case types.MsgTransferFrom: + return handleMsgTransferFrom(ctx, keeper, msg) + case types.MsgApprove: + return handleMsgApprove(ctx, keeper, msg) + default: + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized Msg type: %T", msg) + } + } +} +func handleMsgContract(ctx sdk.Context, keeper keeper.Keeper, msg contract.Msg) error { + if !keeper.HasContractID(ctx) { + return sdkerrors.Wrapf(contract.ErrContractNotExist, "contract id: %s", msg.GetContractID()) + } + return nil +} diff --git a/x/token/internal/handler/handler_test.go b/x/token/internal/handler/handler_test.go new file mode 100644 index 0000000000..1eb155b23d --- /dev/null +++ b/x/token/internal/handler/handler_test.go @@ -0,0 +1,67 @@ +package handler + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/contract" + testCommon "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +var ( + ms store.CommitMultiStore + ctx sdk.Context + k testCommon.Keeper +) + +func setup() { + println("setup") + ctx, ms, k = testCommon.TestKeeper() +} + +func TestMain(m *testing.M) { + setup() + ret := m.Run() + os.Exit(ret) +} + +func cacheKeeper() (sdk.Context, sdk.Handler) { + msCache := ms.CacheMultiStore() + ctx = ctx.WithMultiStore(msCache) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID)) + return ctx, NewHandler(k) +} + +func verifyEventFunc(t *testing.T, expected sdk.Events, actual sdk.Events) { + require.Equal(t, sdk.StringifyEvents(expected.ToABCIEvents()).String(), sdk.StringifyEvents(actual.ToABCIEvents()).String()) +} + +const ( + defaultName = "name" + defaultContractID = "9be17165" + defaultSymbol = "BTC" + defaultImageURI = "image-uri" + defaultMeta = "{}" + defaultDecimals = 6 + defaultAmount = 1000 + defaultCoin = "link" +) + +var ( + addr1 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) +) + +func TestHandlerUnrecognized(t *testing.T) { + ctx, h := cacheKeeper() + + _, err := h(ctx, sdk.NewTestMsg()) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "unrecognized Msg type")) +} diff --git a/x/token/internal/handler/issue.go b/x/token/internal/handler/issue.go new file mode 100644 index 0000000000..ea3ba2aa0f --- /dev/null +++ b/x/token/internal/handler/issue.go @@ -0,0 +1,29 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func handleMsgIssue(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgIssue) (*sdk.Result, error) { + contractI := ctx.Context().Value(contract.CtxKey{}) + if contractI == nil { + panic("contract id does not set") + } + token := types.NewToken(contractI.(string), msg.Name, msg.Symbol, msg.Meta, msg.ImageURI, msg.Decimals, msg.Mintable) + err := keeper.IssueToken(ctx, token, msg.Amount, msg.Owner, msg.To) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/token/internal/handler/issue_test.go b/x/token/internal/handler/issue_test.go new file mode 100644 index 0000000000..528c462408 --- /dev/null +++ b/x/token/internal/handler/issue_test.go @@ -0,0 +1,63 @@ +package handler + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func GetMadeContractID(events sdk.Events) string { + for _, event := range events.ToABCIEvents() { + for _, attr := range event.Attributes { + if string(attr.Key) == types.AttributeKeyContractID { + return string(attr.Value) + } + } + } + return "" +} + +func TestHandleMsgIssue(t *testing.T) { + ctx, h := cacheKeeper() + + t.Log("Issue Token") + { + msg := types.NewMsgIssue(addr1, addr1, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + contractID := GetMadeContractID(res.Events) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "token")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", types.ModifyAction)), + sdk.NewEvent("issue", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("issue", sdk.NewAttribute("name", defaultName)), + sdk.NewEvent("issue", sdk.NewAttribute("symbol", defaultSymbol)), + sdk.NewEvent("issue", sdk.NewAttribute("owner", addr1.String())), + sdk.NewEvent("issue", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("issue", sdk.NewAttribute("amount", sdk.NewInt(defaultAmount).String())), + sdk.NewEvent("issue", sdk.NewAttribute("mintable", "true")), + sdk.NewEvent("issue", sdk.NewAttribute("decimals", sdk.NewInt(defaultDecimals).String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "mint")), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", "burn")), + } + verifyEventFunc(t, e, res.Events) + } + + t.Log("Issue Token Again Expect Success") + { + msg := types.NewMsgIssue(addr1, addr1, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + _, err := h(ctx, msg) + require.NoError(t, err) + } +} diff --git a/x/token/internal/handler/mintburn.go b/x/token/internal/handler/mintburn.go new file mode 100644 index 0000000000..72a98e99b3 --- /dev/null +++ b/x/token/internal/handler/mintburn.go @@ -0,0 +1,56 @@ +// nolint:dupl +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func handleMsgMint(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgMint) (*sdk.Result, error) { + err := keeper.MintToken(ctx, msg.Amount, msg.From, msg.To) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgBurn(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgBurn) (*sdk.Result, error) { + err := keeper.BurnToken(ctx, msg.Amount, msg.From) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgBurnFrom(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgBurnFrom) (*sdk.Result, error) { + err := keeper.BurnTokenFrom(ctx, msg.Proxy, msg.From, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Proxy.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/token/internal/handler/mintburn_test.go b/x/token/internal/handler/mintburn_test.go new file mode 100644 index 0000000000..e3485c6189 --- /dev/null +++ b/x/token/internal/handler/mintburn_test.go @@ -0,0 +1,139 @@ +package handler + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestHandleMsgMint(t *testing.T) { + ctx, h := cacheKeeper() + + t.Log("Prepare Token Issued") + { + k.NewContractID(ctx) + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + err := k.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1) + require.NoError(t, err) + } + + t.Log("Burn Tokens") + { + msg := types.NewMsgMint(addr1, defaultContractID, addr1, sdk.NewInt(defaultAmount)) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "token")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("mint", sdk.NewAttribute("contract_id", defaultContractID)), + sdk.NewEvent("mint", sdk.NewAttribute("amount", sdk.NewInt(defaultAmount).String())), + sdk.NewEvent("mint", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("mint", sdk.NewAttribute("to", addr1.String())), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleMsgBurn(t *testing.T) { + ctx, h := cacheKeeper() + + t.Log("Prepare Token Issued") + { + k.NewContractID(ctx) + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + err := k.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1) + require.NoError(t, err) + } + + t.Log("Mint Tokens") + { + msg := types.NewMsgBurn(addr1, defaultContractID, sdk.NewInt(defaultAmount)) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "token")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("burn", sdk.NewAttribute("contract_id", defaultContractID)), + sdk.NewEvent("burn", sdk.NewAttribute("amount", sdk.NewInt(defaultAmount).String())), + sdk.NewEvent("burn", sdk.NewAttribute("from", addr1.String())), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleMsgBurnFTFrom(t *testing.T) { + ctx, h := cacheKeeper() + + t.Log("Prepare Token Issued") + { + k.NewContractID(ctx) + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + err := k.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1) + require.NoError(t, err) + } + + t.Log("fail to burn when addr2 is not approved ") + { + burnMsg := types.NewMsgBurnFrom(addr2, defaultContractID, addr1, sdk.NewInt(100)) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + + t.Log("Approve addr2") + { + msgApprove := types.NewMsgApprove(addr1, defaultContractID, addr2) + _, err := h(ctx, msgApprove) + require.NoError(t, err) + } + + t.Log("give permission to addr2") + { + permission := types.NewBurnPermission() + msg := types.NewMsgGrantPermission(addr1, defaultContractID, addr2, permission) + require.NoError(t, msg.ValidateBasic()) + _, err := h(ctx, msg) + require.NoError(t, err) + } + + t.Log("Error when invalid user") + { + burnMsg := types.NewMsgBurnFrom(addr2, defaultContractID, addr2, sdk.NewInt(100)) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + + t.Log("fail to burn over the being supplied") + { + burnMsg := types.NewMsgBurnFrom(addr2, defaultContractID, addr1, sdk.NewInt(1001)) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + + t.Log("fail to burn when invalid ContractID") + { + burnMsg := types.NewMsgBurnFrom(addr2, "abcd11234", addr1, sdk.NewInt(1000)) + _, err := h(ctx, burnMsg) + require.Error(t, err) + } + + t.Log("Succeed to burn") + { + burnMsg := types.NewMsgBurnFrom(addr2, defaultContractID, addr1, sdk.NewInt(100)) + res, err := h(ctx, burnMsg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "token")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("burn_from", sdk.NewAttribute("contract_id", defaultContractID)), + sdk.NewEvent("burn_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("burn_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("burn_from", sdk.NewAttribute("amount", sdk.NewInt(100).String())), + } + verifyEventFunc(t, e, res.Events) + } +} diff --git a/x/token/internal/handler/modify.go b/x/token/internal/handler/modify.go new file mode 100644 index 0000000000..0f5167040c --- /dev/null +++ b/x/token/internal/handler/modify.go @@ -0,0 +1,23 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func handleMsgModify(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgModify) (*sdk.Result, error) { + err := keeper.ModifyToken(ctx, msg.Owner, msg.Changes) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/token/internal/handler/modify_test.go b/x/token/internal/handler/modify_test.go new file mode 100644 index 0000000000..1750398303 --- /dev/null +++ b/x/token/internal/handler/modify_test.go @@ -0,0 +1,68 @@ +package handler + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestHandleMsgModify(t *testing.T) { + ctx, h := cacheKeeper() + + contractID := contract.SampleContractID + const ( + modifiedTokenName = "modifiedTokenName" + modifiedImgURI = "modifiedImgURI" + modifiedMeta = "modifiedMeta" + ) + // Given MsgModify + msg := types.NewMsgModify(addr1, contractID, types.NewChanges( + types.NewChange("name", modifiedTokenName), + types.NewChange("img_uri", modifiedImgURI), + types.NewChange("meta", modifiedMeta), + )) + + t.Log("Test with nonexistent token") + { + // When handle MsgModify + _, err := h(ctx, msg) + + // Then response is error + require.Error(t, err) + } + + t.Log("Test modify token") + { + // Given issued token + res, err := h(ctx, types.NewMsgIssue(addr1, addr1, defaultName, defaultContractID, defaultMeta, defaultImageURI, + sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true)) + require.NoError(t, err) + contractID := GetMadeContractID(res.Events) + + msg := types.NewMsgModify(addr1, contractID, types.NewChanges( + types.NewChange("name", modifiedTokenName), + types.NewChange("img_uri", modifiedImgURI), + types.NewChange("meta", modifiedMeta), + )) + + // When handle MsgModify + res, err = h(ctx, msg) + + // Then response is success + require.NoError(t, err) + // And events are returned + expectedEvents := sdk.Events{ + sdk.NewEvent(types.EventTypeModifyToken, sdk.NewAttribute(types.AttributeKeyContractID, defaultContractID)), + sdk.NewEvent(types.EventTypeModifyToken, sdk.NewAttribute("name", modifiedTokenName)), + sdk.NewEvent(types.EventTypeModifyToken, sdk.NewAttribute("img_uri", modifiedImgURI)), + sdk.NewEvent(types.EventTypeModifyToken, sdk.NewAttribute("meta", modifiedMeta)), + sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory)), + sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeySender, msg.Owner.String())), + } + verifyEventFunc(t, expectedEvents, res.Events) + } +} diff --git a/x/token/internal/handler/perm.go b/x/token/internal/handler/perm.go new file mode 100644 index 0000000000..b3d007b7b6 --- /dev/null +++ b/x/token/internal/handler/perm.go @@ -0,0 +1,40 @@ +// nolint:dupl +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func handleMsgGrant(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgGrantPermission) (*sdk.Result, error) { + err := keeper.GrantPermission(ctx, msg.From, msg.To, msg.Permission) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgRevoke(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgRevokePermission) (*sdk.Result, error) { + err := keeper.RevokePermission(ctx, msg.From, msg.Permission) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + }) + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/token/internal/handler/perm_test.go b/x/token/internal/handler/perm_test.go new file mode 100644 index 0000000000..db8e2e2c20 --- /dev/null +++ b/x/token/internal/handler/perm_test.go @@ -0,0 +1,79 @@ +package handler + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestHandleMsgGrant(t *testing.T) { + ctx, h := cacheKeeper() + + t.Log("Prepare Token Issued") + { + k.NewContractID(ctx) + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + err := k.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1) + require.NoError(t, err) + } + + permission := types.NewMintPermission() + t.Log("Invalid contract id") + { + msg := types.NewMsgGrantPermission(addr1, "1234567890", addr2, permission) + require.Error(t, msg.ValidateBasic()) + } + t.Log("Grant Permission") + { + msg := types.NewMsgGrantPermission(addr1, defaultContractID, addr2, permission) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "token")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("grant_perm", sdk.NewAttribute("contract_id", defaultContractID)), + sdk.NewEvent("grant_perm", sdk.NewAttribute("perm", permission.String())), + } + verifyEventFunc(t, e, res.Events) + } +} + +func TestHandleMsgRevoke(t *testing.T) { + ctx, h := cacheKeeper() + + t.Log("Prepare Token Issued") + { + k.NewContractID(ctx) + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + err := k.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1) + require.NoError(t, err) + } + + permission := types.NewMintPermission() + t.Log("Invalid contract id") + { + msg := types.NewMsgRevokePermission(addr1, "1234567890", permission) + require.Error(t, msg.ValidateBasic()) + } + t.Log("Revoke Permission") + { + msg := types.NewMsgRevokePermission(addr1, defaultContractID, permission) + require.NoError(t, msg.ValidateBasic()) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "token")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("contract_id", defaultContractID)), + sdk.NewEvent("revoke_perm", sdk.NewAttribute("perm", permission.String())), + } + verifyEventFunc(t, e, res.Events) + } +} diff --git a/x/token/internal/handler/proxy.go b/x/token/internal/handler/proxy.go new file mode 100644 index 0000000000..b07b7933e6 --- /dev/null +++ b/x/token/internal/handler/proxy.go @@ -0,0 +1,24 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func handleMsgApprove(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgApprove) (*sdk.Result, error) { + err := keeper.SetApproved(ctx, msg.Proxy, msg.Approver) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Approver.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/token/internal/handler/proxy_test.go b/x/token/internal/handler/proxy_test.go new file mode 100644 index 0000000000..d3865b1a72 --- /dev/null +++ b/x/token/internal/handler/proxy_test.go @@ -0,0 +1,51 @@ +package handler + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestHandleApprove(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + msgIssue := types.NewMsgIssue(addr1, addr1, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + res, err := h(ctx, msgIssue) + require.NoError(t, err) + + contractID = GetMadeContractID(res.Events) + + msgMint := types.NewMsgMint(addr1, contractID, addr1, sdk.NewInt(defaultAmount)) + _, err = h(ctx, msgMint) + require.NoError(t, err) + } + + msg := types.NewMsgTransferFrom(addr2, contractID, addr1, addr2, sdk.NewInt(defaultAmount)) + _, err := h(ctx, msg) + require.Error(t, err) + + { + msgApprove := types.NewMsgApprove(addr1, contractID, addr2) + _, err := h(ctx, msgApprove) + require.NoError(t, err) + } + + msg = types.NewMsgTransferFrom(addr2, contractID, addr1, addr2, sdk.NewInt(defaultAmount)) + res, err := h(ctx, msg) + require.NoError(t, err) + + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "token")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("transfer_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("transfer_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("transfer_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("transfer_from", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("transfer_from", sdk.NewAttribute("amount", sdk.NewInt(defaultAmount).String())), + } + verifyEventFunc(t, e, res.Events) +} diff --git a/x/token/internal/handler/transfer.go b/x/token/internal/handler/transfer.go new file mode 100644 index 0000000000..acd3529a9e --- /dev/null +++ b/x/token/internal/handler/transfer.go @@ -0,0 +1,41 @@ +package handler + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func handleMsgTransfer(ctx sdk.Context, k keeper.Keeper, msg types.MsgTransfer) (*sdk.Result, error) { + err := k.Transfer(ctx, msg.From, msg.To, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.From.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} + +func handleMsgTransferFrom(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgTransferFrom) (*sdk.Result, error) { + err := keeper.TransferFrom(ctx, msg.Proxy, msg.From, msg.To, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Proxy.String()), + ), + ) + + return &sdk.Result{Events: ctx.EventManager().Events()}, nil +} diff --git a/x/token/internal/handler/transfer_test.go b/x/token/internal/handler/transfer_test.go new file mode 100644 index 0000000000..8aee1d7c61 --- /dev/null +++ b/x/token/internal/handler/transfer_test.go @@ -0,0 +1,78 @@ +package handler + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" + + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestHandleMsgTransfer(t *testing.T) { + ctx, h := cacheKeeper() + + t.Log("Prepare Token Issued") + { + k.NewContractID(ctx) + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + err := k.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1) + require.NoError(t, err) + } + + t.Log("Transfer Token") + { + msg := types.NewMsgTransfer(addr1, addr2, defaultContractID, sdk.NewInt(10)) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "token")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr1.String())), + sdk.NewEvent("transfer", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("transfer", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("transfer", sdk.NewAttribute("contract_id", defaultContractID)), + sdk.NewEvent("transfer", sdk.NewAttribute("amount", sdk.NewInt(10).String())), + } + verifyEventFunc(t, e, res.Events) + } + t.Log("Transfer Coin. Expect Fail") + { + msg := types.NewMsgTransfer(addr1, addr2, defaultCoin, sdk.NewInt(10)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(contract.ErrInvalidContractID, "ContractID: %s", defaultCoin).Error()) + _, err := h(ctx, msg) + require.Error(t, err) + } +} + +func TestHandleTransferFrom(t *testing.T) { + ctx, h := cacheKeeper() + + var contractID string + { + msgIssue := types.NewMsgIssue(addr1, addr1, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultAmount), sdk.NewInt(defaultDecimals), true) + res, err := h(ctx, msgIssue) + require.NoError(t, err) + + contractID = GetMadeContractID(res.Events) + + msgApprove := types.NewMsgApprove(addr1, contractID, addr2) + _, err = h(ctx, msgApprove) + require.NoError(t, err) + } + + msg := types.NewMsgTransferFrom(addr2, contractID, addr1, addr2, sdk.NewInt(defaultAmount)) + res, err := h(ctx, msg) + require.NoError(t, err) + e := sdk.Events{ + sdk.NewEvent("message", sdk.NewAttribute("module", "token")), + sdk.NewEvent("message", sdk.NewAttribute("sender", addr2.String())), + sdk.NewEvent("transfer_from", sdk.NewAttribute("contract_id", contractID)), + sdk.NewEvent("transfer_from", sdk.NewAttribute("proxy", addr2.String())), + sdk.NewEvent("transfer_from", sdk.NewAttribute("from", addr1.String())), + sdk.NewEvent("transfer_from", sdk.NewAttribute("to", addr2.String())), + sdk.NewEvent("transfer_from", sdk.NewAttribute("amount", sdk.NewInt(defaultAmount).String())), + } + verifyEventFunc(t, e, res.Events) +} diff --git a/x/token/internal/keeper/account.go b/x/token/internal/keeper/account.go new file mode 100644 index 0000000000..02a9fabc4a --- /dev/null +++ b/x/token/internal/keeper/account.go @@ -0,0 +1,79 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +type AccountKeeper interface { + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) + GetOrNewAccount(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) + GetAccount(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) + SetAccount(ctx sdk.Context, acc types.Account) error + UpdateAccount(ctx sdk.Context, acc types.Account) error +} + +func (k Keeper) NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) { + acc = types.NewBaseAccountWithAddress(k.getContractID(ctx), addr) + if err = k.SetAccount(ctx, acc); err != nil { + return nil, err + } + return acc, nil +} + +func (k Keeper) SetAccount(ctx sdk.Context, acc types.Account) error { + store := ctx.KVStore(k.storeKey) + accKey := types.AccountKey(acc.GetContractID(), acc.GetAddress()) + if store.Has(accKey) { + return sdkerrors.Wrap(types.ErrAccountExist, acc.GetAddress().String()) + } + store.Set(accKey, k.cdc.MustMarshalBinaryBare(acc)) + + // Set Account if not exists yet + account := k.accountKeeper.GetAccount(ctx, acc.GetAddress()) + if account == nil { + account = k.accountKeeper.NewAccountWithAddress(ctx, acc.GetAddress()) + k.accountKeeper.SetAccount(ctx, account) + } + return nil +} + +func (k Keeper) UpdateAccount(ctx sdk.Context, acc types.Account) error { + store := ctx.KVStore(k.storeKey) + accKey := types.AccountKey(acc.GetContractID(), acc.GetAddress()) + if !store.Has(accKey) { + return sdkerrors.Wrap(types.ErrAccountNotExist, acc.GetAddress().String()) + } + store.Set(accKey, k.cdc.MustMarshalBinaryBare(acc)) + return nil +} + +func (k Keeper) GetOrNewAccount(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) { + acc, err = k.GetAccount(ctx, addr) + if err != nil { + acc, err = k.NewAccountWithAddress(ctx, addr) + if err != nil { + return nil, err + } + } + return acc, nil +} + +func (k Keeper) GetAccount(ctx sdk.Context, addr sdk.AccAddress) (acc types.Account, err error) { + store := ctx.KVStore(k.storeKey) + accKey := types.AccountKey(k.getContractID(ctx), addr) + if !store.Has(accKey) { + return nil, sdkerrors.Wrap(types.ErrAccountNotExist, addr.String()) + } + bz := store.Get(accKey) + return k.mustDecodeAccount(bz), nil +} + +func (k Keeper) mustDecodeAccount(bz []byte) (acc types.Account) { + err := k.cdc.UnmarshalBinaryBare(bz, &acc) + if err != nil { + panic(err) + } + return +} diff --git a/x/token/internal/keeper/account_test.go b/x/token/internal/keeper/account_test.go new file mode 100644 index 0000000000..118ccf2e68 --- /dev/null +++ b/x/token/internal/keeper/account_test.go @@ -0,0 +1,94 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func verifyAccountFunc(t *testing.T, expected types.Account, actual types.Account) { + require.Equal(t, expected.GetContractID(), actual.GetContractID()) + require.Equal(t, expected.GetAddress(), actual.GetAddress()) + require.Equal(t, expected.GetBalance(), actual.GetBalance()) +} + +func TestKeeper_SetAccount(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + expected := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, expected)) + } + t.Log("Compare Account") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.AccountKey(expected.GetContractID(), addr1)) + actual := keeper.mustDecodeAccount(bz) + verifyAccountFunc(t, expected, actual) + } +} + +func TestKeeper_GetAccount(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Account") + expected := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.AccountKey(expected.GetContractID(), addr1), keeper.cdc.MustMarshalBinaryBare(expected)) + } + t.Log("Get Account") + { + actual, err := keeper.GetAccount(ctx, addr1) + require.NoError(t, err) + verifyAccountFunc(t, expected, actual) + } +} + +func TestKeeper_UpdateAccount(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + acc := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Update Account") + var expected types.Account + expected = types.NewBaseAccountWithAddress(defaultContractID, addr1) + expected = expected.SetBalance(sdk.OneInt()) + { + require.NoError(t, keeper.UpdateAccount(ctx, expected)) + } + t.Log("Compare Account") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.AccountKey(acc.GetContractID(), addr1)) + actual := keeper.mustDecodeAccount(bz) + verifyAccountFunc(t, expected, actual) + } +} + +func TestKeeper_GetOrNewAccount(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Account") + expected := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.AccountKey(expected.GetContractID(), addr1), keeper.cdc.MustMarshalBinaryBare(expected)) + } + t.Log("Get Account addr1") + { + actual, err := keeper.GetOrNewAccount(ctx, addr1) + require.NoError(t, err) + verifyAccountFunc(t, expected, actual) + } + + expected = types.NewBaseAccountWithAddress(defaultContractID, addr2) + t.Log("Get Account addr2") + { + actual, err := keeper.GetOrNewAccount(ctx, addr2) + require.NoError(t, err) + verifyAccountFunc(t, expected, actual) + } +} diff --git a/x/token/internal/keeper/bank.go b/x/token/internal/keeper/bank.go new file mode 100644 index 0000000000..9f1bff8e31 --- /dev/null +++ b/x/token/internal/keeper/bank.go @@ -0,0 +1,107 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +// For the Token module +type BankKeeper interface { + GetBalance(ctx sdk.Context, addr sdk.AccAddress) sdk.Int + SetBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) error + HasBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) bool + + SubtractBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) (sdk.Int, error) + AddBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) (sdk.Int, error) + Send(ctx sdk.Context, from, to sdk.AccAddress, amt sdk.Int) error +} + +var _ BankKeeper = (*Keeper)(nil) + +func (k Keeper) GetBalance(ctx sdk.Context, addr sdk.AccAddress) sdk.Int { + acc, err := k.GetAccount(ctx, addr) + if err != nil { + return sdk.ZeroInt() + } + return acc.GetBalance() +} + +func (k Keeper) SetBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) error { + acc, err := k.GetAccount(ctx, addr) + if err != nil { + return err + } + acc = acc.SetBalance(amt) + err = k.UpdateAccount(ctx, acc) + if err != nil { + return err + } + return nil +} + +func (k Keeper) HasBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) bool { + return k.GetBalance(ctx, addr).GTE(amt) +} + +func (k Keeper) Send(ctx sdk.Context, from, to sdk.AccAddress, amt sdk.Int) error { + if amt.IsNegative() { + return sdkerrors.Wrap(types.ErrInvalidAmount, "send amount must be positive") + } + + _, err := k.SubtractBalance(ctx, from, amt) + if err != nil { + return err + } + + _, err = k.AddBalance(ctx, to, amt) + if err != nil { + return err + } + return nil +} + +func (k Keeper) SubtractBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) (sdk.Int, error) { + return k.subtractBalance(ctx, addr, amt) +} +func (k Keeper) subtractBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) (sdk.Int, error) { + acc, err := k.GetAccount(ctx, addr) + if err != nil { + return sdk.ZeroInt(), sdkerrors.Wrapf(types.ErrInsufficientBalance, fmt.Sprintf("insufficient account funds for token [%s]; 0 < %s", k.getContractID(ctx), amt)) + } + oldBalance := acc.GetBalance() + newBalance := oldBalance.Sub(amt) + if newBalance.IsNegative() { + return amt, sdkerrors.Wrapf(types.ErrInsufficientBalance, "insufficient account funds for token [%s]; %s < %s", k.getContractID(ctx), oldBalance, amt) + } + acc = acc.SetBalance(newBalance) + err = k.UpdateAccount(ctx, acc) + if err != nil { + return amt, err + } + return newBalance, nil +} + +func (k Keeper) AddBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) (sdk.Int, error) { + return k.addBalance(ctx, addr, amt) +} + +func (k Keeper) addBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Int) (sdk.Int, error) { + acc, err := k.GetOrNewAccount(ctx, addr) + if err != nil { + return amt, err + } + oldBalance := acc.GetBalance() + newBalance := oldBalance.Add(amt) + if newBalance.IsNegative() { + return amt, sdkerrors.Wrapf(types.ErrInsufficientBalance, "insufficient account funds for token [%s]; %s < %s", k.getContractID(ctx), oldBalance, amt) + } + acc = acc.SetBalance(newBalance) + err = k.UpdateAccount(ctx, acc) + if err != nil { + return amt, err + } + return newBalance, nil +} diff --git a/x/token/internal/keeper/bank_test.go b/x/token/internal/keeper/bank_test.go new file mode 100644 index 0000000000..195f3d8fcb --- /dev/null +++ b/x/token/internal/keeper/bank_test.go @@ -0,0 +1,127 @@ +package keeper + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func TestKeeper_GetBalance(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + acc := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Get Balance") + { + balance := keeper.GetBalance(ctx, addr1) + require.Equal(t, balance.Int64(), acc.GetBalance().Int64()) + } +} + +func TestKeeper_HasBalance(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + var acc types.Account + acc = types.NewBaseAccountWithAddress(defaultContractID, addr1) + acc = acc.SetBalance(sdk.NewInt(defaultAmount)) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Has Balance") + { + require.True(t, keeper.HasBalance(ctx, addr1, sdk.NewInt(defaultAmount))) + } +} + +func TestKeeper_SetBalance(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + acc := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Set Balance") + { + require.NoError(t, keeper.SetBalance(ctx, addr1, sdk.NewInt(defaultAmount))) + } + t.Log("Get Balance") + { + balance := keeper.GetBalance(ctx, addr1) + require.Equal(t, sdk.NewInt(defaultAmount).Int64(), balance.Int64()) + } +} + +func TestKeeper_AddBalance(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + acc := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Add Balance") + { + added, err := keeper.AddBalance(ctx, addr1, sdk.NewInt(defaultAmount)) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount).Int64(), added.Int64()) + } + t.Log("Get Balance") + { + balance := keeper.GetBalance(ctx, addr1) + require.Equal(t, sdk.NewInt(defaultAmount).Int64(), balance.Int64()) + } +} + +func TestKeeper_SubtractBalance(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + acc := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Set Balance") + { + require.NoError(t, keeper.SetBalance(ctx, addr1, sdk.NewInt(defaultAmount))) + } + t.Log("Subtract Balance") + { + sub, err := keeper.SubtractBalance(ctx, addr1, sdk.NewInt(defaultAmount)) + require.NoError(t, err) + require.Equal(t, sdk.ZeroInt().Int64(), sub.Int64()) + } + t.Log("Get Balance") + { + balance := keeper.GetBalance(ctx, addr1) + require.Equal(t, sdk.ZeroInt().Int64(), balance.Int64()) + } +} + +func TestKeeper_SendToken(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Account") + acc := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Set Balance") + { + require.NoError(t, keeper.SetBalance(ctx, addr1, sdk.NewInt(defaultAmount))) + } + t.Log("Send Balance") + { + require.NoError(t, keeper.Send(ctx, addr1, addr2, sdk.NewInt(defaultAmount))) + } + { + require.EqualError(t, keeper.Send(ctx, addr3, addr2, sdk.NewInt(1)), sdkerrors.Wrapf(types.ErrInsufficientBalance, "insufficient account funds for token [9be17165]; 0 < 1").Error()) + } + t.Log("Get Balance") + { + require.Equal(t, sdk.ZeroInt().Int64(), keeper.GetBalance(ctx, addr1).Int64()) + require.Equal(t, sdk.NewInt(defaultAmount).Int64(), keeper.GetBalance(ctx, addr2).Int64()) + } +} diff --git a/x/token/internal/keeper/blacklist.go b/x/token/internal/keeper/blacklist.go new file mode 100644 index 0000000000..368430b070 --- /dev/null +++ b/x/token/internal/keeper/blacklist.go @@ -0,0 +1,19 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func (k Keeper) SetBlackList(ctx sdk.Context, addr sdk.AccAddress, action string) { + store := ctx.KVStore(k.storeKey) + + // value is just a key w/o the module prefix + v := addr.String() + ":" + action + store.Set(types.BlacklistKey(addr, action), []byte(v)) +} + +func (k Keeper) IsBlacklisted(ctx sdk.Context, addr sdk.AccAddress, action string) bool { + store := ctx.KVStore(k.storeKey) + return store.Has(types.BlacklistKey(addr, action)) +} diff --git a/x/token/internal/keeper/blacklist_test.go b/x/token/internal/keeper/blacklist_test.go new file mode 100644 index 0000000000..95b07672f4 --- /dev/null +++ b/x/token/internal/keeper/blacklist_test.go @@ -0,0 +1,21 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestKeeper_SetBlackList(t *testing.T) { + ctx := cacheKeeper() + keeper.SetBlackList(ctx, addr1, "every") + require.True(t, keeper.IsBlacklisted(ctx, addr1, "every")) +} + +func TestKeeper_IsBlacklisted(t *testing.T) { + ctx := cacheKeeper() + keeper.SetBlackList(ctx, addr1, "every") + require.True(t, keeper.IsBlacklisted(ctx, addr1, "every")) + require.False(t, keeper.IsBlacklisted(ctx, addr2, "every")) + require.False(t, keeper.IsBlacklisted(ctx, addr1, "every2")) +} diff --git a/x/token/internal/keeper/burn.go b/x/token/internal/keeper/burn.go new file mode 100644 index 0000000000..a5130ab3ba --- /dev/null +++ b/x/token/internal/keeper/burn.go @@ -0,0 +1,70 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func (k Keeper) BurnToken(ctx sdk.Context, amount sdk.Int, from sdk.AccAddress) error { + err := k.isBurnable(ctx, from, from, amount) + if err != nil { + return err + } + + err = k.BurnSupply(ctx, from, amount) + if err != nil { + return err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeBurnToken, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + ), + }) + return nil +} + +func (k Keeper) BurnTokenFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, amount sdk.Int) error { + if !k.IsApproved(ctx, proxy, from) { + return sdkerrors.Wrapf(types.ErrTokenNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), from.String(), k.getContractID(ctx)) + } + + err := k.isBurnable(ctx, proxy, from, amount) + if err != nil { + return err + } + + err = k.BurnSupply(ctx, from, amount) + if err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeBurnTokenFrom, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + ), + }) + return nil +} + +func (k Keeper) isBurnable(ctx sdk.Context, permissionOwner, tokenOwner sdk.AccAddress, amount sdk.Int) error { + if !k.HasBalance(ctx, tokenOwner, amount) { + return sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "%v has not enough coins for %v", tokenOwner.String(), amount) + } + if !amount.IsPositive() { + return sdkerrors.Wrap(types.ErrInvalidAmount, amount.String()) + } + + perm := types.NewBurnPermission() + if !k.HasPermission(ctx, permissionOwner, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", permissionOwner.String(), perm.String()) + } + return nil +} diff --git a/x/token/internal/keeper/burn_test.go b/x/token/internal/keeper/burn_test.go new file mode 100644 index 0000000000..6e4027a875 --- /dev/null +++ b/x/token/internal/keeper/burn_test.go @@ -0,0 +1,101 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper_BurnTokens(t *testing.T) { + ctx := cacheKeeper() + t.Log("Issue Token") + { + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + require.NoError(t, keeper.IssueToken(ctx, token, sdk.NewInt(defaultAmount+defaultAmount), addr1, addr1)) + } + t.Log("TotalSupply supply") + { + supply, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, int64(defaultAmount+defaultAmount), supply.Int64()) + } + t.Log("Balance of Account") + { + supply := keeper.GetBalance(ctx, addr1) + require.Equal(t, int64(defaultAmount+defaultAmount), supply.Int64()) + } + + t.Log("Burn Tokens by addr1") + { + err := keeper.BurnToken(ctx, sdk.NewInt(defaultAmount), addr1) + require.NoError(t, err) + } + t.Log("Burn 0 Token by addr1") + { + err := keeper.BurnToken(ctx, sdk.NewInt(0), addr1) + require.Error(t, err) + } + t.Log("TotalSupply supply") + { + supply, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.Equal(t, int64(defaultAmount), supply.Int64()) + require.NoError(t, err) + } + t.Log("Balance of Account 1") + { + supply := keeper.GetBalance(ctx, addr1) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } +} + +func TestKeeper_BurnTokenFrom(t *testing.T) { + ctx := cacheKeeper() + t.Log("Issue Token") + { + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + require.NoError(t, keeper.IssueToken(ctx, token, sdk.NewInt(defaultAmount+defaultAmount), addr1, addr1)) + } + t.Log("set permission for proxy") + { + keeper.AddPermission(ctx, addr2, types.NewBurnPermission()) + } + t.Log("approve") + { + require.NoError(t, keeper.SetApproved(ctx, addr2, addr1)) + } + t.Log("Burn Tokens by proxy") + { + err := keeper.BurnTokenFrom(ctx, addr2, addr1, sdk.NewInt(defaultAmount)) + require.NoError(t, err) + } + t.Log("check Balance of Account 1") + { + supply := keeper.GetBalance(ctx, addr1) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } +} + +func TestKeeper_BurnTokensWithoutPermissions(t *testing.T) { + ctx := cacheKeeper() + t.Log("Issue Token") + { + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + require.NoError(t, keeper.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1)) + } + + t.Log("Transfer Enough Token") + { + err := keeper.Transfer(ctx, addr1, addr2, sdk.NewInt(defaultAmount)) + require.NoError(t, err) + } + + t.Log("Burn Tokens by addr2. Expect Fail") + { + err := keeper.BurnToken(ctx, sdk.NewInt(defaultAmount), addr2) + require.Error(t, err) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr2.String(), types.NewBurnPermission().String()).Error()) + } +} diff --git a/x/token/internal/keeper/hooks.go b/x/token/internal/keeper/hooks.go new file mode 100644 index 0000000000..a4df903b9f --- /dev/null +++ b/x/token/internal/keeper/hooks.go @@ -0,0 +1,11 @@ +package keeper + +// Hooks wrapper struct for safety box keeper +type Hooks struct { + k Keeper +} + +// Return the wrapper struct +func (k Keeper) Hooks() *Hooks { + return &Hooks{k} +} diff --git a/x/token/internal/keeper/issue.go b/x/token/internal/keeper/issue.go new file mode 100644 index 0000000000..92f08edd30 --- /dev/null +++ b/x/token/internal/keeper/issue.go @@ -0,0 +1,70 @@ +package keeper + +import ( + "strconv" + "unicode/utf8" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func (k Keeper) IssueToken(ctx sdk.Context, token types.Token, amount sdk.Int, owner, to sdk.AccAddress) error { + if !types.ValidateImageURI(token.GetImageURI()) { + return sdkerrors.Wrapf(types.ErrInvalidImageURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", token.GetImageURI(), types.MaxImageURILength, utf8.RuneCountInString(token.GetImageURI())) + } + err := k.SetToken(ctx, token) + if err != nil { + return err + } + + err = k.MintSupply(ctx, to, amount) + if err != nil { + return err + } + + modifyTokenURIPermission := types.NewModifyPermission() + k.AddPermission(ctx, owner, modifyTokenURIPermission) + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeIssueToken, + sdk.NewAttribute(types.AttributeKeyContractID, token.GetContractID()), + sdk.NewAttribute(types.AttributeKeyName, token.GetName()), + sdk.NewAttribute(types.AttributeKeySymbol, token.GetSymbol()), + sdk.NewAttribute(types.AttributeKeyOwner, owner.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + sdk.NewAttribute(types.AttributeKeyMintable, strconv.FormatBool(token.GetMintable())), + sdk.NewAttribute(types.AttributeKeyDecimals, token.GetDecimals().String()), + ), + sdk.NewEvent( + types.EventTypeGrantPermToken, + sdk.NewAttribute(types.AttributeKeyTo, owner.String()), + sdk.NewAttribute(types.AttributeKeyContractID, token.GetContractID()), + sdk.NewAttribute(types.AttributeKeyPerm, modifyTokenURIPermission.String()), + ), + }) + + if token.GetMintable() { + mintPerm := types.NewMintPermission() + k.AddPermission(ctx, owner, mintPerm) + burnPerm := types.NewBurnPermission() + k.AddPermission(ctx, owner, burnPerm) + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeGrantPermToken, + sdk.NewAttribute(types.AttributeKeyTo, owner.String()), + sdk.NewAttribute(types.AttributeKeyContractID, token.GetContractID()), + sdk.NewAttribute(types.AttributeKeyPerm, mintPerm.String()), + ), + sdk.NewEvent( + types.EventTypeGrantPermToken, + sdk.NewAttribute(types.AttributeKeyTo, owner.String()), + sdk.NewAttribute(types.AttributeKeyContractID, token.GetContractID()), + sdk.NewAttribute(types.AttributeKeyPerm, burnPerm.String()), + ), + }) + } + + return nil +} diff --git a/x/token/internal/keeper/issue_test.go b/x/token/internal/keeper/issue_test.go new file mode 100644 index 0000000000..d7e1baddfb --- /dev/null +++ b/x/token/internal/keeper/issue_test.go @@ -0,0 +1,82 @@ +package keeper + +import ( + "strings" + "testing" + "unicode/utf8" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper_IssueToken(t *testing.T) { + ctx := cacheKeeper() + t.Log("Issue Token") + expected := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + { + require.NoError(t, keeper.IssueToken(ctx, expected, sdk.NewInt(defaultAmount), addr1, addr1)) + } + t.Log("Get Token") + { + actual, err := keeper.GetToken(ctx) + require.NoError(t, err) + verifyTokenFunc(t, expected, actual) + } + t.Log("Permission") + { + require.True(t, keeper.HasPermission(ctx, addr1, types.NewModifyPermission())) + require.True(t, keeper.HasPermission(ctx, addr1, types.NewMintPermission())) + require.True(t, keeper.HasPermission(ctx, addr1, types.NewBurnPermission())) + } + t.Log("Permission only addr1 has the permissions") + { + require.False(t, keeper.HasPermission(ctx, addr2, types.NewModifyPermission())) + require.False(t, keeper.HasPermission(ctx, addr2, types.NewMintPermission())) + require.False(t, keeper.HasPermission(ctx, addr2, types.NewBurnPermission())) + } + t.Log("TotalSupply supply") + { + supply, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } + t.Log("Balance of Account") + { + supply := keeper.GetBalance(ctx, addr1) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } +} + +func TestKeeper_IssueTokenNotMintable(t *testing.T) { + ctx := cacheKeeper() + t.Log("Issue a Token Not Mintable") + expected := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), false) + { + require.NoError(t, keeper.IssueToken(ctx, expected, sdk.NewInt(defaultAmount), addr1, addr1)) + } + { + actual, err := keeper.GetToken(ctx) + require.NoError(t, err) + verifyTokenFunc(t, expected, actual) + } + t.Log("Permission only addr1 has no mint/burn permissions") + { + require.True(t, keeper.HasPermission(ctx, addr1, types.NewModifyPermission())) + require.False(t, keeper.HasPermission(ctx, addr1, types.NewMintPermission())) + require.False(t, keeper.HasPermission(ctx, addr1, types.NewBurnPermission())) + } +} + +func TestKeeper_IssueTokenTooLongTokenURI(t *testing.T) { + ctx := cacheKeeper() + + length1001String := strings.Repeat("Eng글자日本語はスゲ", 91) // 11 * 91 = 1001 + + t.Log("issue a token with too long token uri") + { + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, length1001String, sdk.NewInt(defaultDecimals), true) + require.EqualError(t, keeper.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1), sdkerrors.Wrapf(types.ErrInvalidImageURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", length1001String, types.MaxImageURILength, utf8.RuneCountInString(length1001String)).Error()) + } +} diff --git a/x/token/internal/keeper/keeper.go b/x/token/internal/keeper/keeper.go new file mode 100644 index 0000000000..0a602c1783 --- /dev/null +++ b/x/token/internal/keeper/keeper.go @@ -0,0 +1,57 @@ +package keeper + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/tendermint/tendermint/libs/log" +) + +type Keeper struct { + accountKeeper types.AccountKeeper + storeKey sdk.StoreKey + contractKeeper contract.Keeper + cdc *codec.Codec +} + +func NewKeeper(cdc *codec.Codec, accountKeeper types.AccountKeeper, contractKeeper contract.Keeper, storeKey sdk.StoreKey) Keeper { + return Keeper{ + accountKeeper: accountKeeper, + storeKey: storeKey, + contractKeeper: contractKeeper, + cdc: cdc, + } +} + +func (k Keeper) NewContractID(ctx sdk.Context) string { + return k.contractKeeper.NewContractID(ctx) +} +func (k Keeper) HasContractID(ctx sdk.Context) bool { + return k.contractKeeper.HasContractID(ctx, k.getContractID(ctx)) +} +func (k Keeper) getContractID(ctx sdk.Context) string { + contractI := ctx.Context().Value(contract.CtxKey{}) + if contractI == nil { + panic("contract id does not set on the context") + } + return contractI.(string) +} + +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +func (k Keeper) UnmarshalJSON(bz []byte, ptr interface{}) error { + return k.cdc.UnmarshalJSON(bz, ptr) +} + +func (k Keeper) MarshalJSON(o interface{}) ([]byte, error) { + return k.cdc.MarshalJSON(o) +} + +func (k Keeper) MarshalJSONIndent(o interface{}) ([]byte, error) { + return k.cdc.MarshalJSONIndent(o, "", " ") +} diff --git a/x/token/internal/keeper/keeper_test.go b/x/token/internal/keeper/keeper_test.go new file mode 100644 index 0000000000..56689dad01 --- /dev/null +++ b/x/token/internal/keeper/keeper_test.go @@ -0,0 +1,88 @@ +package keeper + +import ( + "context" + "os" + "testing" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +const ( + defaultName = "name" + defaultSymbol = "BTC" + defaultContractID = "9be17165" + anotherContractID = "56171eb9" + defaultMeta = "{}" + defaultImageURI = "image-uri" + defaultDecimals = 6 + defaultAmount = 1000 +) + +var ( + addr1 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr3 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) +) + +var ( + ms store.CommitMultiStore + ctx sdk.Context + keeper Keeper +) + +func setup() { + println("setup") + ctx, ms, keeper = TestKeeper() +} + +func TestMain(m *testing.M) { + setup() + ret := m.Run() + os.Exit(ret) +} + +func cacheKeeper() sdk.Context { + msCache := ms.CacheMultiStore() + ctx = ctx.WithMultiStore(msCache) + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID)) + return ctx +} + +func verifyTokenFunc(t *testing.T, expected types.Token, actual types.Token) { + require.Equal(t, expected.GetContractID(), actual.GetContractID()) + require.Equal(t, expected.GetName(), actual.GetName()) + require.Equal(t, expected.GetImageURI(), actual.GetImageURI()) + require.Equal(t, expected.GetDecimals(), actual.GetDecimals()) + require.Equal(t, expected.GetMintable(), actual.GetMintable()) +} + +func TestKeeper_MarshalJSONLogger(t *testing.T) { + ctx := cacheKeeper() + dummy := struct { + Key string + Value string + }{ + Key: "key", + Value: "value", + } + bz, err := keeper.MarshalJSON(dummy) + require.NoError(t, err) + + dummy2 := struct { + Key string + Value string + }{} + + err = keeper.UnmarshalJSON(bz, &dummy2) + require.NoError(t, err) + require.Equal(t, dummy.Key, dummy2.Key) + require.Equal(t, dummy.Value, dummy2.Value) + logger := keeper.Logger(ctx) + logger.Info("test", dummy, dummy2) +} diff --git a/x/token/internal/keeper/mint.go b/x/token/internal/keeper/mint.go new file mode 100644 index 0000000000..73b5f69be0 --- /dev/null +++ b/x/token/internal/keeper/mint.go @@ -0,0 +1,45 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func (k Keeper) MintToken(ctx sdk.Context, amount sdk.Int, from, to sdk.AccAddress) error { + token, err := k.GetToken(ctx) + if err != nil { + return err + } + if err := k.isMintable(ctx, token, from, amount); err != nil { + return err + } + err = k.MintSupply(ctx, to, amount) + if err != nil { + return err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeMintToken, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + ), + }) + return nil +} + +func (k Keeper) isMintable(ctx sdk.Context, token types.Token, from sdk.AccAddress, amount sdk.Int) error { + if !token.GetMintable() { + return sdkerrors.Wrapf(types.ErrTokenNotMintable, "ContractID: %s", token.GetContractID()) + } + if !amount.IsPositive() { + return sdkerrors.Wrap(types.ErrInvalidAmount, amount.String()) + } + perm := types.NewMintPermission() + if !k.HasPermission(ctx, from, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", from.String(), perm.String()) + } + return nil +} diff --git a/x/token/internal/keeper/mint_test.go b/x/token/internal/keeper/mint_test.go new file mode 100644 index 0000000000..5904bb154b --- /dev/null +++ b/x/token/internal/keeper/mint_test.go @@ -0,0 +1,89 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper_MintTokens(t *testing.T) { + ctx := cacheKeeper() + t.Log("Issue Token") + { + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + require.NoError(t, keeper.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1)) + } + t.Log("TotalSupply supply") + { + supply, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } + t.Log("Balance of Account") + { + supply := keeper.GetBalance(ctx, addr1) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } + + t.Log("Mint Tokens addr1 -> addr1") + { + err := keeper.MintToken(ctx, sdk.NewInt(defaultAmount), addr1, addr1) + require.NoError(t, err) + } + t.Log("Mint 0 Token") + { + err := keeper.MintToken(ctx, sdk.NewInt(0), addr1, addr1) + require.Error(t, err) + } + t.Log("TotalSupply supply") + { + supply, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.Equal(t, int64(defaultAmount+defaultAmount), supply.Int64()) + require.NoError(t, err) + } + t.Log("Balance of Account 1") + { + supply := keeper.GetBalance(ctx, addr1) + require.Equal(t, int64(defaultAmount+defaultAmount), supply.Int64()) + } + t.Log("Mint Tokens addr1 -> addr2") + { + err := keeper.MintToken(ctx, sdk.NewInt(defaultAmount), addr1, addr2) + require.NoError(t, err) + } + t.Log("TotalSupply supply") + { + supply, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.Equal(t, int64(defaultAmount+defaultAmount+defaultAmount), supply.Int64()) + require.NoError(t, err) + } + t.Log("Balance of Account 1") + { + supply := keeper.GetBalance(ctx, addr1) + require.Equal(t, int64(defaultAmount+defaultAmount), supply.Int64()) + } + t.Log("Balance of Account 2") + { + supply := keeper.GetBalance(ctx, addr2) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } +} + +func TestKeeper_MintTokensWithoutPermissions(t *testing.T) { + ctx := cacheKeeper() + t.Log("Issue Token") + { + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + require.NoError(t, keeper.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1)) + } + + t.Log("Mint Tokens by addr2. Expect Fail") + { + err := keeper.MintToken(ctx, sdk.NewInt(defaultAmount), addr2, addr2) + require.Error(t, err) + require.EqualError(t, err, sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr2.String(), types.NewMintPermission().String()).Error()) + } +} diff --git a/x/token/internal/keeper/modify.go b/x/token/internal/keeper/modify.go new file mode 100644 index 0000000000..a9ef10e14e --- /dev/null +++ b/x/token/internal/keeper/modify.go @@ -0,0 +1,49 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func (k Keeper) ModifyToken(ctx sdk.Context, owner sdk.AccAddress, changes types.Changes) error { + token, err := k.GetToken(ctx) + if err != nil { + return err + } + + tokenModifyPerm := types.NewModifyPermission() + if !k.HasPermission(ctx, owner, tokenModifyPerm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", owner.String(), tokenModifyPerm.String()) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeModifyToken, + sdk.NewAttribute(types.AttributeKeyContractID, token.GetContractID()), + ), + }) + + for _, change := range changes { + switch change.Field { + case types.AttributeKeyName: + token.SetName(change.Value) + case types.AttributeKeyMeta: + token.SetMeta(change.Value) + case types.AttributeKeyImageURI: + token.SetImageURI(change.Value) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeModifyToken, + sdk.NewAttribute(change.Field, change.Value), + ), + }) + } + err = k.UpdateToken(ctx, token) + if err != nil { + return err + } + return nil +} diff --git a/x/token/internal/keeper/modify_test.go b/x/token/internal/keeper/modify_test.go new file mode 100644 index 0000000000..adc5092964 --- /dev/null +++ b/x/token/internal/keeper/modify_test.go @@ -0,0 +1,69 @@ +package keeper + +import ( + "context" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestModifyTokenName(t *testing.T) { + const modifiedTokenName = "modifiedTokenName" + const modifiedMeta = "modifiedMeta" + const modifiedImageURI = "modifiedImageURI" + changes := types.NewChanges( + types.NewChange("name", modifiedTokenName), + types.NewChange("meta", modifiedMeta), + types.NewChange("img_uri", modifiedImageURI), + ) + + ctx := cacheKeeper() + token := aToken(defaultContractID) + tokenWithoutPerm := aToken(defaultContractID + "2") + modifyPermission := types.NewModifyPermission() + + // Given Token And Permission + require.NoError(t, keeper.SetToken(ctx, token)) + keeper.AddPermission(ctx, addr1, modifyPermission) + + t.Log("Test to modify token") + { + // When modify token name + require.NoError(t, keeper.ModifyToken(ctx, addr1, changes)) + + // Then token name is modified + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.TokenKey(token.GetContractID())) + actual := keeper.mustDecodeToken(bz) + require.Equal(t, modifiedTokenName, actual.GetName()) + } + t.Log("Test with nonexistent contract") + { + // Given nonexistent contractID + nonExistentcontractID := "abcd1234" + + // When modify token name with invalid contractID, Then error is occurred + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, nonExistentcontractID)) + require.EqualError(t, keeper.ModifyToken(ctx2, addr1, changes), + sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s", nonExistentcontractID).Error()) + } + t.Log("Test without permission") + { + // Given Token without Permission + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, defaultContractID+"2")) + require.NoError(t, keeper.SetToken(ctx2, tokenWithoutPerm)) + invalidPerm := types.NewModifyPermission() + + // When modify token name with invalid permission, Then error is occurred + require.EqualError(t, keeper.ModifyToken(ctx2, addr1, changes), + sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr1.String(), invalidPerm.String()).Error()) + } +} + +func aToken(contractID string) types.Token { + return types.NewToken(contractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) +} diff --git a/x/token/internal/keeper/msg_encoder.go b/x/token/internal/keeper/msg_encoder.go new file mode 100644 index 0000000000..8d723f0b4d --- /dev/null +++ b/x/token/internal/keeper/msg_encoder.go @@ -0,0 +1,134 @@ +package keeper + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/line/lbm-sdk/v2/x/wasm" +) + +func NewMsgEncodeHandler(tokenKeeper Keeper) wasm.EncodeHandler { + return func(jsonMsg json.RawMessage) ([]sdk.Msg, error) { + var wasmCustomMsg types.WasmCustomMsg + err := json.Unmarshal(jsonMsg, &wasmCustomMsg) + if err != nil { + return nil, err + } + switch types.MsgRoute(wasmCustomMsg.Route) { + case types.RIssue: + return handleMsgIssue(wasmCustomMsg.Data) + case types.RTransfer: + return handleMsgTransfer(wasmCustomMsg.Data) + case types.RTransferFrom: + return handleMsgTransferFrom(wasmCustomMsg.Data) + case types.RMint: + return handleMsgMint(wasmCustomMsg.Data) + case types.RBurn: + return handleMsgBurn(wasmCustomMsg.Data) + case types.RBurnFrom: + return handleMsgBurnFrom(wasmCustomMsg.Data) + case types.RGrantPerm: + return handleMsgGrantPerm(wasmCustomMsg.Data) + case types.RRevokePerm: + return handleMsgRevokePerm(wasmCustomMsg.Data) + case types.RModify: + return handleMsgModify(wasmCustomMsg.Data) + case types.RApprove: + return handleMsgApprove(wasmCustomMsg.Data) + default: + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized Msg route: %T", wasmCustomMsg.Route) + } + } +} + +func handleMsgIssue(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgIssue + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgTransfer(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgTransfer + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgTransferFrom(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgTransferFrom + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgMint(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgMint + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgBurn(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgBurn + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgBurnFrom(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgBurnFrom + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgGrantPerm(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgGrantPermission + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgRevokePerm(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgRevokePermission + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgModify(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgModify + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} + +func handleMsgApprove(msgData json.RawMessage) ([]sdk.Msg, error) { + var msg types.MsgApprove + err := json.Unmarshal(msgData, &msg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{msg}, nil +} diff --git a/x/token/internal/keeper/msg_encoder_test.go b/x/token/internal/keeper/msg_encoder_test.go new file mode 100644 index 0000000000..5f3b19c5d8 --- /dev/null +++ b/x/token/internal/keeper/msg_encoder_test.go @@ -0,0 +1,154 @@ +package keeper + +import ( + "encoding/json" + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Encode(t *testing.T) { + encodeHandler := NewMsgEncodeHandler(keeper) + jsonMsg := json.RawMessage(`{"foo": 123}`) + + testContractID := "test_contract_id" + issue := fmt.Sprintf(`{"route":"issue", "data":{"owner":"%s","to":"%s","name":"TestToken1","symbol":"TT1","img_uri":"","meta":"","amount":"1000","mintable":true,"decimals":"18"}}`, addr1.String(), addr2.String()) + issueMsg := json.RawMessage(issue) + transfer := fmt.Sprintf(`{"route":"transfer", "data":{"from":"%s", "contract_id":"%s", "to":"%s", "amount":"100"}}`, addr1.String(), testContractID, addr2.String()) + transferMsg := json.RawMessage(transfer) + transferFrom := fmt.Sprintf(`{"route":"transfer_from", "data":{"proxy":"%s", "from":"%s", "contract_id":"%s", "to":"%s", "amount":"100"}}`, addr3.String(), addr1.String(), testContractID, addr2.String()) + transferFromMsg := json.RawMessage(transferFrom) + mint := fmt.Sprintf(`{"route":"mint", "data":{"from":"%s", "contract_id":"%s", "to":"%s", "amount":"100"}}`, addr1.String(), testContractID, addr2.String()) + mintMsg := json.RawMessage(mint) + burn := fmt.Sprintf(`{"route":"burn", "data":{"from":"%s", "contract_id":"%s", "amount":"5"}}`, addr1.String(), testContractID) + burnMsg := json.RawMessage(burn) + burnFrom := fmt.Sprintf(`{"route":"burn_from", "data":{"proxy":"%s", "from":"%s", "contract_id":"%s", "amount":"5"}}`, addr2.String(), addr1.String(), testContractID) + burnFromMsg := json.RawMessage(burnFrom) + + grantPermission := fmt.Sprintf(`{"route":"grant_perm", "data":{"from":"%s", "contract_id":"%s", "to":"%s", "permission":"mint"}}`, addr1.String(), testContractID, addr2.String()) + grantPermissionMsg := json.RawMessage(grantPermission) + revokePermission := fmt.Sprintf(`{"route":"revoke_perm", "data":{"from":"%s", "contract_id":"%s", "permission":"mint"}}`, addr1.String(), testContractID) + revokePermissionMsg := json.RawMessage(revokePermission) + modify := fmt.Sprintf(`{"route":"modify","data":{"owner":"%s","contract_id":"%s","changes":[{"field":"meta","value":"update_meta"}]}}`, addr1.String(), testContractID) + modifyMsg := json.RawMessage(modify) + approver := fmt.Sprintf(`{"route":"approve", "data":{"approver":"%s", "contract_id":"%s", "proxy":"%s"}}`, addr1.String(), testContractID, addr2.String()) + approverMsg := json.RawMessage(approver) + + changes := types.NewChanges(types.NewChange("meta", "update_meta")) + + cases := map[string]struct { + input json.RawMessage + // set if valid + output []sdk.Msg + // set if invalid + isError bool + }{ + "issue token": { + input: issueMsg, + output: []sdk.Msg{ + types.MsgIssue{ + Owner: addr1, + To: addr2, + Name: "TestToken1", + Symbol: "TT1", + ImageURI: "", + Meta: "", + Amount: sdk.NewInt(1000), + Mintable: true, + Decimals: sdk.NewInt(18), + }, + }, + }, + "transfer token": { + input: transferMsg, + output: []sdk.Msg{ + types.MsgTransfer{ + From: addr1, + ContractID: testContractID, + To: addr2, + Amount: sdk.NewInt(100), + }, + }, + }, + "transfer from token": { + input: transferFromMsg, + output: []sdk.Msg{ + types.MsgTransferFrom{ + Proxy: addr3, + From: addr1, + ContractID: testContractID, + To: addr2, + Amount: sdk.NewInt(100), + }, + }, + }, + "mint token": { + input: mintMsg, + output: []sdk.Msg{ + types.MsgMint{ + From: addr1, + ContractID: testContractID, + To: addr2, + Amount: sdk.NewInt(100), + }, + }, + }, + "burn token": { + input: burnMsg, + output: []sdk.Msg{ + types.NewMsgBurn(addr1, testContractID, sdk.NewInt(5)), + }, + }, + "burn from token": { + input: burnFromMsg, + output: []sdk.Msg{ + types.NewMsgBurnFrom(addr2, testContractID, addr1, sdk.NewInt(5)), + }, + }, + "grant permission": { + input: grantPermissionMsg, + output: []sdk.Msg{ + types.NewMsgGrantPermission(addr1, testContractID, addr2, types.Permission("mint")), + }, + }, + "revoke permission": { + input: revokePermissionMsg, + output: []sdk.Msg{ + types.NewMsgRevokePermission(addr1, testContractID, types.Permission("mint")), + }, + }, + "modify token": { + input: modifyMsg, + output: []sdk.Msg{ + types.NewMsgModify(addr1, testContractID, changes), + }, + }, + "approve": { + input: approverMsg, + output: []sdk.Msg{ + types.NewMsgApprove(addr1, testContractID, addr2), + }, + }, + "unknown custom msg": { + input: jsonMsg, + isError: true, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + res, err := encodeHandler(tc.input) + if tc.isError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.output, res) + } + }) + } +} diff --git a/x/token/internal/keeper/perm.go b/x/token/internal/keeper/perm.go new file mode 100644 index 0000000000..802dde55d9 --- /dev/null +++ b/x/token/internal/keeper/perm.go @@ -0,0 +1,91 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func (k Keeper) AddPermission(ctx sdk.Context, addr sdk.AccAddress, perm types.Permission) { + accPerm := k.getAccountPermission(ctx, addr) + accPerm.AddPermission(perm) + k.setAccountPermission(ctx, accPerm) +} + +func (k Keeper) HasPermission(ctx sdk.Context, addr sdk.AccAddress, p types.Permission) bool { + accPerm := k.getAccountPermission(ctx, addr) + return accPerm.HasPermission(p) +} + +func (k Keeper) GetPermissions(ctx sdk.Context, addr sdk.AccAddress) types.Permissions { + accPerm := k.getAccountPermission(ctx, addr) + return accPerm.GetPermissions() +} + +func (k Keeper) RevokePermission(ctx sdk.Context, addr sdk.AccAddress, perm types.Permission) error { + if !k.HasPermission(ctx, addr, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", addr.String(), perm.String()) + } + accPerm := k.getAccountPermission(ctx, addr) + accPerm.RemovePermission(perm) + k.setAccountPermission(ctx, accPerm) + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeRevokePermToken, + sdk.NewAttribute(types.AttributeKeyFrom, addr.String()), + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyPerm, perm.String()), + ), + }) + return nil +} + +func (k Keeper) GrantPermission(ctx sdk.Context, from, to sdk.AccAddress, perm types.Permission) error { + if !k.HasPermission(ctx, from, perm) { + return sdkerrors.Wrapf(types.ErrTokenNoPermission, "Account: %s, Permission: %s", from.String(), perm.String()) + } + k.AddPermission(ctx, to, perm) + + // Set Account if not exists yet + account := k.accountKeeper.GetAccount(ctx, to) + if account == nil { + account = k.accountKeeper.NewAccountWithAddress(ctx, to) + k.accountKeeper.SetAccount(ctx, account) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeGrantPermToken, + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyPerm, perm.String()), + ), + }) + + return nil +} + +func (k Keeper) getAccountPermission(ctx sdk.Context, addr sdk.AccAddress) (accPerm types.AccountPermissionI) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.PermKey(k.getContractID(ctx), addr)) + if bz != nil { + accPerm = k.mustDecodeAccountPermission(bz) + return accPerm + } + return types.NewAccountPermission(addr) +} + +func (k Keeper) setAccountPermission(ctx sdk.Context, accPerm types.AccountPermissionI) { + store := ctx.KVStore(k.storeKey) + store.Set(types.PermKey(k.getContractID(ctx), accPerm.GetAddress()), k.cdc.MustMarshalBinaryBare(accPerm)) +} + +func (k Keeper) mustDecodeAccountPermission(bz []byte) (accPerm types.AccountPermissionI) { + err := k.cdc.UnmarshalBinaryBare(bz, &accPerm) + if err != nil { + panic(err) + } + return +} diff --git a/x/token/internal/keeper/perm_test.go b/x/token/internal/keeper/perm_test.go new file mode 100644 index 0000000000..df0b5b71c1 --- /dev/null +++ b/x/token/internal/keeper/perm_test.go @@ -0,0 +1,94 @@ +package keeper + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func preparePermissions(ctx sdk.Context, t *testing.T) types.Permissions { + expected := types.Permissions{ + types.NewMintPermission(), + types.NewBurnPermission(), + } + t.Log("Prepare Permissions") + { + for _, perm := range expected { + keeper.AddPermission(ctx, addr1, perm) + } + } + return expected +} + +func TestKeeper_GetPermissions(t *testing.T) { + ctx = cacheKeeper() + expected := preparePermissions(ctx, t) + + t.Log("Compare Permissions") + { + actual := keeper.GetPermissions(ctx, addr1) + for index := range actual { + require.Equal(t, expected[index].String(), actual[index].String()) + } + } +} + +func TestKeeper_AddPermission(t *testing.T) { + ctx = cacheKeeper() + keeper.AddPermission(ctx, addr1, types.NewMintPermission()) + keeper.AddPermission(ctx, addr1, types.NewBurnPermission()) + keeper.AddPermission(ctx, addr1, types.NewModifyPermission()) + require.Equal(t, 3, len(keeper.GetPermissions(ctx, addr1))) +} + +func TestKeeper_GrantPermission(t *testing.T) { + ctx = cacheKeeper() + expected := preparePermissions(ctx, t) + t.Log("Grant Permissions addr1 -> addr2") + { + for _, perm := range expected { + err := keeper.GrantPermission(ctx, addr1, addr2, perm) + require.NoError(t, err) + } + } + t.Log("Grant Permission. addr1 has not the permission") + { + err := keeper.GrantPermission(ctx, addr1, addr2, types.NewModifyPermission()) + require.Error(t, err) + } +} + +func TestKeeper_RevokePermission(t *testing.T) { + ctx = cacheKeeper() + expected := preparePermissions(ctx, t) + t.Log("Revoke Permissions addr1") + { + for _, perm := range expected { + err := keeper.RevokePermission(ctx, addr1, perm) + require.NoError(t, err) + } + } + t.Log("Revoke Permission. addr1 has not the permission") + { + err := keeper.RevokePermission(ctx, addr1, types.NewModifyPermission()) + require.Error(t, err) + } +} + +func TestKeeper_HasPermission(t *testing.T) { + ctx = cacheKeeper() + expected := preparePermissions(ctx, t) + t.Log("Has Permissions addr1") + { + for _, perm := range expected { + require.True(t, keeper.HasPermission(ctx, addr1, perm)) + } + } + t.Log("Revoke Permission. addr1 has not the permission") + { + require.False(t, keeper.HasPermission(ctx, addr1, types.NewModifyPermission())) + } +} diff --git a/x/token/internal/keeper/proxy.go b/x/token/internal/keeper/proxy.go new file mode 100644 index 0000000000..89f71fa734 --- /dev/null +++ b/x/token/internal/keeper/proxy.go @@ -0,0 +1,89 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +var ( + ApprovedValue = []byte{0x01} +) + +type ProxyKeeper interface { + IsApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) bool + SetApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) error + DeleteApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) error +} + +func (k Keeper) IsApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) bool { + store := ctx.KVStore(k.storeKey) + approvedKey := types.TokenApprovedKey(k.getContractID(ctx), proxy, approver) + return store.Has(approvedKey) +} + +func (k Keeper) SetApproved(ctx sdk.Context, proxy sdk.AccAddress, approver sdk.AccAddress) error { + store := ctx.KVStore(k.storeKey) + if !store.Has(types.TokenKey(k.getContractID(ctx))) { + return sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s", k.getContractID(ctx)) + } + + approvedKey := types.TokenApprovedKey(k.getContractID(ctx), proxy, approver) + if store.Has(approvedKey) { + return sdkerrors.Wrapf(types.ErrTokenAlreadyApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), approver.String(), k.getContractID(ctx)) + } + store.Set(approvedKey, ApprovedValue) + + // Set Account if not exists yet + account := k.accountKeeper.GetAccount(ctx, proxy) + if account == nil { + account = k.accountKeeper.NewAccountWithAddress(ctx, proxy) + k.accountKeeper.SetAccount(ctx, account) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeApproveToken, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyApprover, approver.String()), + ), + }) + + return nil +} + +func (k Keeper) GetApprovers(ctx sdk.Context, proxy sdk.AccAddress) (accAds []sdk.AccAddress, err error) { + _, err = k.GetToken(ctx) + if err != nil { + return nil, err + } + k.iterateApprovers(ctx, proxy, false, func(address sdk.AccAddress) bool { + accAds = append(accAds, address) + return false + }) + return accAds, nil +} + +func (k Keeper) iterateApprovers(ctx sdk.Context, prefix sdk.AccAddress, reverse bool, process func(accAd sdk.AccAddress) bool) { + store := ctx.KVStore(k.storeKey) + prefixKey := types.TokenApproversKey(k.getContractID(ctx), prefix) + var iter sdk.Iterator + if reverse { + iter = sdk.KVStoreReversePrefixIterator(store, prefixKey) + } else { + iter = sdk.KVStorePrefixIterator(store, prefixKey) + } + defer iter.Close() + for { + if !iter.Valid() { + return + } + bz := iter.Key() + approver := bz[len(prefixKey):] + if process(approver) { + return + } + iter.Next() + } +} diff --git a/x/token/internal/keeper/proxy_test.go b/x/token/internal/keeper/proxy_test.go new file mode 100644 index 0000000000..cee62e4a3e --- /dev/null +++ b/x/token/internal/keeper/proxy_test.go @@ -0,0 +1,50 @@ +package keeper + +import ( + "context" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestApproveScenario(t *testing.T) { + ctx := cacheKeeper() + + // prepare token + someToken := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + require.NoError(t, keeper.IssueToken(ctx, someToken, sdk.NewInt(defaultAmount), addr1, addr1)) + + // approve test + anotherCtx := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, anotherContractID)) + require.EqualError(t, keeper.SetApproved(anotherCtx, addr3, addr1), sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s", anotherContractID).Error()) + require.NoError(t, keeper.SetApproved(ctx, addr3, addr1)) + require.EqualError(t, keeper.SetApproved(ctx, addr3, addr1), sdkerrors.Wrapf(types.ErrTokenAlreadyApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr3.String(), addr1.String(), defaultContractID).Error()) + + // transfer_from test + require.EqualError(t, keeper.TransferFrom(ctx, addr2, addr1, addr2, sdk.NewInt(10)), sdkerrors.Wrapf(types.ErrTokenNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", addr2.String(), addr1.String(), defaultContractID).Error()) + require.NoError(t, keeper.TransferFrom(ctx, addr3, addr1, addr2, sdk.NewInt(10))) + + t.Log("add one more approver fo test") + { + require.NoError(t, keeper.SetApproved(ctx, addr3, addr2)) + } + + t.Log("succeed to GetApprovers") + { + approvers, err := keeper.GetApprovers(ctx, addr3) + require.NoError(t, err) + require.True(t, len(approvers) == 2) + require.True(t, types.IsAddressContains(approvers, addr1)) + require.True(t, types.IsAddressContains(approvers, addr2)) + } + + t.Log("fail to GetApprovres") + { + _, err := keeper.GetApprovers(anotherCtx, addr3) + require.Error(t, err, "") + } +} diff --git a/x/token/internal/keeper/supply.go b/x/token/internal/keeper/supply.go new file mode 100644 index 0000000000..82cae5b786 --- /dev/null +++ b/x/token/internal/keeper/supply.go @@ -0,0 +1,112 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +type SupplyKeeper interface { + GetTotalInt(ctx sdk.Context, target string) (sdk.Int, error) + MintSupply(ctx sdk.Context, to sdk.AccAddress, amount sdk.Int) error + BurnSupply(ctx sdk.Context, from sdk.AccAddress, amount sdk.Int) error +} + +var _ SupplyKeeper = (*Keeper)(nil) + +func (k Keeper) GetTotalInt(ctx sdk.Context, target string) (sdk.Int, error) { + supply, err := k.getSupply(ctx) + if err != nil { + return sdk.ZeroInt(), err + } + + switch target { + case types.QuerySupply: + return supply.GetTotalSupply(), nil + case types.QueryBurn: + return supply.GetTotalBurn(), nil + case types.QueryMint: + return supply.GetTotalMint(), nil + default: + return sdk.ZeroInt(), sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid request target to query total %s", target) + } +} + +func (k Keeper) getSupply(ctx sdk.Context) (supply types.Supply, err error) { + if _, err := k.GetToken(ctx); err != nil { + return nil, err + } + store := ctx.KVStore(k.storeKey) + b := store.Get(types.SupplyKey(k.getContractID(ctx))) + if b == nil { + panic("stored supply should not have been nil") + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &supply) + return +} + +func (k Keeper) setSupply(ctx sdk.Context, supply types.Supply) { + if k.getContractID(ctx) != supply.GetContractID() { + panic("cannot set supply with different contract id") + } + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(supply) + store.Set(types.SupplyKey(k.getContractID(ctx)), b) +} + +func (k Keeper) MintSupply(ctx sdk.Context, to sdk.AccAddress, amount sdk.Int) (err error) { + defer func() { + // to recover from overflows + if r := recover(); r != nil { + err = types.WrapIfOverflowPanic(r) + } + }() + + _, err = k.addBalance(ctx, to, amount) + if err != nil { + return err + } + + supply, err := k.getSupply(ctx) + if err != nil { + return err + } + oldSupplyAmount := supply.GetTotalSupply() + newSupplyAmount := oldSupplyAmount.Add(amount) + if newSupplyAmount.IsNegative() { + return sdkerrors.Wrapf(types.ErrInsufficientSupply, "insufficient supply for token [%s]; %s < %s", k.getContractID(ctx), oldSupplyAmount, amount) + } + supply = supply.Inflate(amount) + k.setSupply(ctx, supply) + + return nil +} + +func (k Keeper) BurnSupply(ctx sdk.Context, from sdk.AccAddress, amount sdk.Int) (err error) { + defer func() { + // to recover from overflows + // however, it will return insufficient fund error instead of panicking in the case + if r := recover(); r != nil { + err = types.WrapIfOverflowPanic(r) + } + }() + + _, err = k.subtractBalance(ctx, from, amount) + if err != nil { + return err + } + + supply, err := k.getSupply(ctx) + if err != nil { + return err + } + oldSupplyAmount := supply.GetTotalSupply() + newSupplyAmount := oldSupplyAmount.Sub(amount) + if newSupplyAmount.IsNegative() { + return sdkerrors.Wrapf(types.ErrInsufficientSupply, "insufficient supply for token [%s]; %s < %s", k.getContractID(ctx), oldSupplyAmount, amount) + } + supply = supply.Deflate(amount) + k.setSupply(ctx, supply) + + return nil +} diff --git a/x/token/internal/keeper/supply_test.go b/x/token/internal/keeper/supply_test.go new file mode 100644 index 0000000000..f48269c04c --- /dev/null +++ b/x/token/internal/keeper/supply_test.go @@ -0,0 +1,219 @@ +package keeper + +import ( + "testing" + + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func verifySupplyFunc(t *testing.T, expected types.Supply, actual types.Supply) { + require.Equal(t, expected.GetContractID(), actual.GetContractID()) + require.Equal(t, expected.GetTotalSupply().Int64(), actual.GetTotalSupply().Int64()) +} + +func TestKeeper_GetTotalInt(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Supply and Token") + expected := types.DefaultSupply(defaultContractID) + { + store := ctx.KVStore(keeper.storeKey) + b := keeper.cdc.MustMarshalBinaryLengthPrefixed(expected) + store.Set(types.SupplyKey(expected.GetContractID()), b) + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + store.Set(types.TokenKey(expected.GetContractID()), keeper.cdc.MustMarshalBinaryBare(token)) + } + t.Log("Get Supply") + { + actual, err := keeper.getSupply(ctx) + require.NoError(t, err) + verifySupplyFunc(t, expected, actual) + } + t.Log("Get Total Supply Int") + { + actual, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, expected.GetTotalSupply().Int64(), actual.Int64()) + } + t.Log("Get Total Mint Int") + { + actual, err := keeper.GetTotalInt(ctx, types.QueryMint) + require.NoError(t, err) + require.Equal(t, expected.GetTotalMint().Int64(), actual.Int64()) + } + t.Log("Get Total Burn Int") + { + actual, err := keeper.GetTotalInt(ctx, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, expected.GetTotalBurn().Int64(), actual.Int64()) + } +} + +func TestKeeper_MintSupply(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Token") + { + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + require.NoError(t, keeper.SetToken(ctx, token)) + } + t.Log("Set Account") + acc := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Mint Supply") + { + require.NoError(t, keeper.MintSupply(ctx, addr1, sdk.NewInt(defaultAmount))) + } + t.Log("Get Balance") + { + balance := keeper.GetBalance(ctx, addr1) + require.Equal(t, sdk.NewInt(defaultAmount).Int64(), balance.Int64()) + } + t.Log("Get Total Supply Int") + { + actual, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount), actual) + } + t.Log("Get Total Mint Int") + { + actual, err := keeper.GetTotalInt(ctx, types.QueryMint) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount), actual) + } + t.Log("Get Total Burn Int") + { + actual, err := keeper.GetTotalInt(ctx, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, sdk.ZeroInt(), actual) + } +} + +func TestKeeper_BurnSupply(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Token") + { + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + require.NoError(t, keeper.SetToken(ctx, token)) + } + t.Log("Set Account") + acc := types.NewBaseAccountWithAddress(defaultContractID, addr1) + { + require.NoError(t, keeper.SetAccount(ctx, acc)) + } + t.Log("Set Balance And Supply") + { + require.NoError(t, keeper.SetBalance(ctx, addr1, sdk.NewInt(defaultAmount))) + keeper.setSupply(ctx, types.DefaultSupply(defaultContractID).SetTotalSupply(sdk.NewInt(defaultAmount))) + } + t.Log("Burn Supply") + { + require.NoError(t, keeper.BurnSupply(ctx, addr1, sdk.NewInt(defaultAmount))) + } + t.Log("Get Balance") + { + balance := keeper.GetBalance(ctx, addr1) + require.Equal(t, sdk.ZeroInt().Int64(), balance.Int64()) + } + t.Log("Get Total Supply Int") + { + actual, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, sdk.ZeroInt(), actual) + } + t.Log("Get Total Mint Int") + { + actual, err := keeper.GetTotalInt(ctx, types.QueryMint) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount), actual) + } + t.Log("Get Total Burn Int") + { + actual, err := keeper.GetTotalInt(ctx, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, sdk.NewInt(defaultAmount), actual) + } +} + +func TestKeeper_Handle_Overflows(t *testing.T) { + ctx := cacheKeeper() + + t.Log("Prepare Supply and Token") + expected := types.DefaultSupply(defaultContractID) + { + store := ctx.KVStore(keeper.storeKey) + b := keeper.cdc.MustMarshalBinaryLengthPrefixed(expected) + store.Set(types.SupplyKey(expected.GetContractID()), b) + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultImageURI, defaultMeta, sdk.NewInt(defaultDecimals), true) + store.Set(types.TokenKey(expected.GetContractID()), keeper.cdc.MustMarshalBinaryBare(token)) + } + + // int64 is the set of all signed 64-bit integers. + // Range: -9223372036854775808 through 9223372036854775807. + + // Int wraps integer with 256 bit range bound + // Checks overflow, underflow and division by zero + // Exists in range from -(2^maxBitLen-1) to 2^maxBitLen-1 + + t.Log("Set supply less than the overflow limit") + maxInt64Supply := sdk.NewInt(9223372036854775807) + + initialSupply := maxInt64Supply.Mul(maxInt64Supply).Mul(maxInt64Supply).Mul(maxInt64Supply) + newSupply := types.NewSupply(defaultContractID, initialSupply) + keeper.setSupply(ctx, newSupply) + + ts, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalSupply(), ts) + + tm, err := keeper.GetTotalInt(ctx, types.QueryMint) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalMint(), tm) + + tb, err := keeper.GetTotalInt(ctx, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalBurn(), tb) + + // inflate over the overflow limit + t.Log("Inflate the supply over the overflow limit") + addToOverflow := initialSupply.Mul(sdk.NewInt(8)) + err = keeper.MintSupply(ctx, addr1, addToOverflow) + require.Equal(t, types.ErrSupplyOverflow, err) + + // should have not changed + t.Log("Totals have not changed") + ts, err = keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalSupply(), ts) + + tm, err = keeper.GetTotalInt(ctx, types.QueryMint) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalMint(), tm) + + tb, err = keeper.GetTotalInt(ctx, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalBurn(), tb) + + // deflate below the overflow limit - it will return insufficient fund instead of panicking + t.Log("Deflate the supply below the overflow limit - will return insufficient fund instead of panicking") + subToOverflow := initialSupply.Mul(sdk.NewInt(8)) + err = keeper.BurnSupply(ctx, addr1, subToOverflow) + require.True(t, types.ErrInsufficientSupply.Is(err)) + + // should have not changed + t.Log("Totals have not changed") + ts, err = keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalSupply(), ts) + + tm, err = keeper.GetTotalInt(ctx, types.QueryMint) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalMint(), tm) + + tb, err = keeper.GetTotalInt(ctx, types.QueryBurn) + require.NoError(t, err) + require.Equal(t, newSupply.GetTotalBurn(), tb) +} diff --git a/x/token/internal/keeper/test_common.go b/x/token/internal/keeper/test_common.go new file mode 100644 index 0000000000..4463b8dbfb --- /dev/null +++ b/x/token/internal/keeper/test_common.go @@ -0,0 +1,49 @@ +package keeper + +import ( + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestKeeper() (sdk.Context, store.CommitMultiStore, Keeper) { + keyAuth := sdk.NewKVStoreKey(auth.StoreKey) + keyParams := sdk.NewKVStoreKey(params.StoreKey) + tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) + keyToken := sdk.NewKVStoreKey(types.StoreKey) + keyContract := sdk.NewKVStoreKey(contract.StoreKey) + + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyAuth, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyToken, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyContract, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + if err := ms.LoadLatestVersion(); err != nil { + panic(err) + } + + cdc := codec.New() + types.RegisterCodec(cdc) + auth.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + cdc.Seal() + + paramsKeeper := params.NewKeeper(cdc, keyParams, tkeyParams) + authSubspace := paramsKeeper.Subspace(auth.DefaultParamspace) + + // add keepers + accountKeeper := auth.NewAccountKeeper(cdc, keyAuth, authSubspace, auth.ProtoBaseAccount) + keeper := NewKeeper(cdc, accountKeeper, contract.NewContractKeeper(cdc, keyContract), keyToken) + ctx := sdk.NewContext(ms, abci.Header{ChainID: "test-chain-id"}, false, log.NewNopLogger()) + + return ctx, ms, keeper +} diff --git a/x/token/internal/keeper/token.go b/x/token/internal/keeper/token.go new file mode 100644 index 0000000000..91c03c3aaa --- /dev/null +++ b/x/token/internal/keeper/token.go @@ -0,0 +1,79 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func (k Keeper) GetToken(ctx sdk.Context) (types.Token, error) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.TokenKey(k.getContractID(ctx))) + if bz == nil { + return nil, sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s", k.getContractID(ctx)) + } + return k.mustDecodeToken(bz), nil +} + +func (k Keeper) SetToken(ctx sdk.Context, token types.Token) error { + if k.getContractID(ctx) != token.GetContractID() { + panic("cannot set token with different contract id") + } + store := ctx.KVStore(k.storeKey) + tokenKey := types.TokenKey(k.getContractID(ctx)) + if store.Has(tokenKey) { + return sdkerrors.Wrapf(types.ErrTokenExist, "ContractID: %s", k.getContractID(ctx)) + } + store.Set(tokenKey, k.cdc.MustMarshalBinaryBare(token)) + + k.setSupply(ctx, types.DefaultSupply(token.GetContractID())) + + return nil +} + +func (k Keeper) UpdateToken(ctx sdk.Context, token types.Token) error { + if k.getContractID(ctx) != token.GetContractID() { + panic("cannot update token with different contract id") + } + store := ctx.KVStore(k.storeKey) + tokenKey := types.TokenKey(k.getContractID(ctx)) + if !store.Has(tokenKey) { + return sdkerrors.Wrapf(types.ErrTokenNotExist, "ContractID: %s", k.getContractID(ctx)) + } + store.Set(tokenKey, k.cdc.MustMarshalBinaryBare(token)) + return nil +} + +func (k Keeper) GetAllTokens(ctx sdk.Context) (tokens types.Tokens) { + appendToken := func(token types.Token) (stop bool) { + tokens = append(tokens, token) + return false + } + k.iterateTokens(ctx, "", appendToken) + return tokens +} + +func (k Keeper) iterateTokens(ctx sdk.Context, prefix string, process func(types.Token) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, types.TokenKey(prefix)) + defer iter.Close() + for { + if !iter.Valid() { + return + } + val := iter.Value() + token := k.mustDecodeToken(val) + if process(token) { + return + } + iter.Next() + } +} + +func (k Keeper) mustDecodeToken(tokenByte []byte) (token types.Token) { + err := k.cdc.UnmarshalBinaryBare(tokenByte, &token) + if err != nil { + panic(err) + } + return token +} diff --git a/x/token/internal/keeper/token_test.go b/x/token/internal/keeper/token_test.go new file mode 100644 index 0000000000..81b079933f --- /dev/null +++ b/x/token/internal/keeper/token_test.go @@ -0,0 +1,86 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper_GetToken(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Token") + expected := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + { + store := ctx.KVStore(keeper.storeKey) + store.Set(types.TokenKey(expected.GetContractID()), keeper.cdc.MustMarshalBinaryBare(expected)) + } + t.Log("Get Token") + { + actual, err := keeper.GetToken(ctx) + require.NoError(t, err) + verifyTokenFunc(t, expected, actual) + } +} + +func TestKeeper_SetToken(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Token") + expected := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + { + require.NoError(t, keeper.SetToken(ctx, expected)) + } + t.Log("Compare Token") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.TokenKey(expected.GetContractID())) + actual := keeper.mustDecodeToken(bz) + verifyTokenFunc(t, expected, actual) + } +} + +func TestKeeper_UpdateToken(t *testing.T) { + ctx := cacheKeeper() + t.Log("Set Token") + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + { + require.NoError(t, keeper.SetToken(ctx, token)) + } + t.Log("Update Token") + expected := types.NewToken(token.GetContractID(), "modifiedname", "BTC", "{}", "modifiedtokenuri", sdk.NewInt(defaultDecimals), true) + { + require.NoError(t, keeper.UpdateToken(ctx, expected)) + } + t.Log("Compare Token") + { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(types.TokenKey(token.GetContractID())) + actual := keeper.mustDecodeToken(bz) + verifyTokenFunc(t, expected, actual) + } +} + +func TestKeeper_GetAllTokens(t *testing.T) { + ctx := cacheKeeper() + t.Log("Prepare Tokens") + expected := types.Tokens{ + types.NewToken(defaultContractID+"1", defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true), + types.NewToken(defaultContractID+"2", defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true), + types.NewToken(defaultContractID+"3", defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true), + types.NewToken(defaultContractID+"4", defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true), + } + { + store := ctx.KVStore(keeper.storeKey) + for _, t := range expected { + store.Set(types.TokenKey(t.GetContractID()), keeper.cdc.MustMarshalBinaryBare(t)) + } + } + t.Log("Compare Tokens") + { + actual := keeper.GetAllTokens(ctx) + for index := range expected { + verifyTokenFunc(t, expected[index], actual[index]) + } + } +} diff --git a/x/token/internal/keeper/transfer.go b/x/token/internal/keeper/transfer.go new file mode 100644 index 0000000000..0473e9c067 --- /dev/null +++ b/x/token/internal/keeper/transfer.go @@ -0,0 +1,54 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/coin" + "github.com/line/lbm-sdk/v2/x/token/internal/types" +) + +func (k Keeper) Transfer(ctx sdk.Context, from sdk.AccAddress, to sdk.AccAddress, amount sdk.Int) error { + // reject if to address is blacklisted (safety box addresses) + if k.IsBlacklisted(ctx, to, coin.ActionTransferTo) { + return sdkerrors.Wrapf(coin.ErrCanNotTransferToBlacklisted, "Addr: %s", to.String()) + } + + err := k.Send(ctx, from, to, amount) + if err != nil { + return err + } + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransfer, + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + ), + }) + + return nil +} + +func (k Keeper) TransferFrom(ctx sdk.Context, proxy sdk.AccAddress, from sdk.AccAddress, to sdk.AccAddress, amount sdk.Int) error { + if !k.IsApproved(ctx, proxy, from) { + return sdkerrors.Wrapf(types.ErrTokenNotApproved, "Proxy: %s, Approver: %s, ContractID: %s", proxy.String(), from.String(), k.getContractID(ctx)) + } + + if err := k.Send(ctx, from, to, amount); err != nil { + return err + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTransferFrom, + sdk.NewAttribute(types.AttributeKeyContractID, k.getContractID(ctx)), + sdk.NewAttribute(types.AttributeKeyProxy, proxy.String()), + sdk.NewAttribute(types.AttributeKeyFrom, from.String()), + sdk.NewAttribute(types.AttributeKeyTo, to.String()), + sdk.NewAttribute(types.AttributeKeyAmount, amount.String()), + ), + }) + + return nil +} diff --git a/x/token/internal/keeper/transfer_test.go b/x/token/internal/keeper/transfer_test.go new file mode 100644 index 0000000000..826758ebff --- /dev/null +++ b/x/token/internal/keeper/transfer_test.go @@ -0,0 +1,56 @@ +package keeper + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" +) + +func TestKeeper_Transfer(t *testing.T) { + ctx := cacheKeeper() + t.Log("Issue Token") + { + token := types.NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + require.NoError(t, keeper.IssueToken(ctx, token, sdk.NewInt(defaultAmount), addr1, addr1)) + } + t.Log("TotalSupply supply") + { + supply, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } + t.Log("Balance of Account 1") + { + supply := keeper.GetBalance(ctx, addr1) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } + t.Log("Balance of Account 2") + { + supply := keeper.GetBalance(ctx, addr2) + require.Equal(t, int64(0), supply.Int64()) + } + t.Log("Transfer Token") + { + err := keeper.Transfer(ctx, addr1, addr2, sdk.NewInt(defaultAmount)) + require.NoError(t, err) + } + t.Log("TotalSupply supply") + { + supply, err := keeper.GetTotalInt(ctx, types.QuerySupply) + require.NoError(t, err) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } + t.Log("Balance of Account 1") + { + supply := keeper.GetBalance(ctx, addr1) + require.Equal(t, int64(0), supply.Int64()) + } + t.Log("Balance of Account 2") + { + supply := keeper.GetBalance(ctx, addr2) + require.Equal(t, int64(defaultAmount), supply.Int64()) + } +} diff --git a/x/token/internal/legacy/upgrade.go b/x/token/internal/legacy/upgrade.go new file mode 100644 index 0000000000..177ded52c9 --- /dev/null +++ b/x/token/internal/legacy/upgrade.go @@ -0,0 +1,13 @@ +package legacy + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/upgrade" +) + +func UpgradeHandler(version string) upgrade.UpgradeHandler { + // XXX: return handler for the migration version + return func(ctx sdk.Context, plan upgrade.Plan) { + + } +} diff --git a/x/token/internal/querier/querier.go b/x/token/internal/querier/querier.go new file mode 100644 index 0000000000..eea6a7396d --- /dev/null +++ b/x/token/internal/querier/querier.go @@ -0,0 +1,150 @@ +package querier + +import ( + "context" + + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + abci "github.com/tendermint/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// creates a querier for token REST endpoints +func NewQuerier(keeper keeper.Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) { + if len(path) >= 2 { + ctx = ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, path[1])) + } + switch path[0] { + case types.QueryPerms: + return queryAccountPermission(ctx, req, keeper) + case types.QueryTokens: + return queryTokens(ctx, req, keeper) + case types.QueryBalance: + return queryBalance(ctx, req, keeper) + case types.QueryMint: + return queryTotal(ctx, req, keeper, types.QueryMint) + case types.QueryBurn: + return queryTotal(ctx, req, keeper, types.QueryBurn) + case types.QuerySupply: + return queryTotal(ctx, req, keeper, types.QuerySupply) + case types.QueryIsApproved: + return queryIsApproved(ctx, req, keeper) + case types.QueryApprovers: + return queryApprovers(ctx, req, keeper) + default: + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown token query endpoint") + } + } +} + +func queryBalance(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + if len(req.Data) == 0 { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "data is nil") + } + var params types.QueryContractIDAccAddressParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + supply := keeper.GetBalance(ctx, params.Addr) + bz, err := keeper.MarshalJSONIndent(supply) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil +} + +func queryAccountPermission(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + if len(req.Data) == 0 { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "data is nil") + } + var params types.QueryContractIDAccAddressParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + pms := keeper.GetPermissions(ctx, params.Addr) + + bz, err := keeper.MarshalJSONIndent(pms) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil +} + +func queryTokens(ctx sdk.Context, _ abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + if ctx.Context().Value(contract.CtxKey{}) == nil { + tokens := keeper.GetAllTokens(ctx) + + bz, err := keeper.MarshalJSONIndent(tokens) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil + } + token, err := keeper.GetToken(ctx) + if err != nil { + return nil, err + } + + bz, err2 := keeper.MarshalJSONIndent(token) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryTotal(ctx sdk.Context, _ abci.RequestQuery, keeper keeper.Keeper, target string) ([]byte, error) { + total, err := keeper.GetTotalInt(ctx, target) + if err != nil { + return nil, err + } + + bz, err2 := keeper.MarshalJSONIndent(total) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} + +func queryIsApproved(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryIsApprovedParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + approved := keeper.IsApproved(ctx, params.Proxy, params.Approver) + + bz, err := keeper.MarshalJSONIndent(approved) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return bz, nil +} + +func queryApprovers(ctx sdk.Context, req abci.RequestQuery, keeper keeper.Keeper) ([]byte, error) { + var params types.QueryProxyParams + if err := keeper.UnmarshalJSON(req.Data, ¶ms); err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + + approvers, err := keeper.GetApprovers(ctx, params.Proxy) + if err != nil { + return nil, err + } + + bz, err2 := keeper.MarshalJSONIndent(approvers) + if err2 != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err2.Error()) + } + + return bz, nil +} diff --git a/x/token/internal/querier/querier_encoder.go b/x/token/internal/querier/querier_encoder.go new file mode 100644 index 0000000000..db8f784617 --- /dev/null +++ b/x/token/internal/querier/querier_encoder.go @@ -0,0 +1,154 @@ +package querier + +import ( + "encoding/json" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/line/lbm-sdk/v2/x/wasm" +) + +func NewQueryEncoder(tokenQuerier sdk.Querier) wasm.EncodeQuerier { + return func(ctx sdk.Context, jsonQuerier json.RawMessage) ([]byte, error) { + var customQuerier types.WasmCustomQuerier + err := json.Unmarshal(jsonQuerier, &customQuerier) + if err != nil { + return nil, err + } + switch customQuerier.Route { + case types.QueryTokens: + return handleQueryToken(ctx, tokenQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryBalance: + return handleQueryBalance(ctx, tokenQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QuerySupply: + return handleQueryTotal(ctx, tokenQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryMint: + return handleQueryTotal(ctx, tokenQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryBurn: + return handleQueryTotal(ctx, tokenQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryPerms: + return handleQueryPerms(ctx, tokenQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryIsApproved: + return handleQueryIsApproved(ctx, tokenQuerier, []string{customQuerier.Route}, customQuerier.Data) + case types.QueryApprovers: + return handleQueryApprovers(ctx, tokenQuerier, []string{customQuerier.Route}, customQuerier.Data) + default: + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized Msg route: %T", customQuerier.Route) + } + } +} + +func handleQueryToken(ctx sdk.Context, tokenQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryTokenWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + req := makeRequestQuery(nil) + + contractID := wrapper.TokenParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return tokenQuerier(ctx, path, req) +} + +func handleQueryBalance(ctx sdk.Context, tokenQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryBalanceWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + + req := makeRequestQuery(types.QueryContractIDAccAddressParams{ + Addr: wrapper.BalanceParam.Address, + }) + + contractID := wrapper.BalanceParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return tokenQuerier(ctx, path, req) +} + +func handleQueryTotal(ctx sdk.Context, tokenQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryTotalWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + req := makeRequestQuery(nil) + + contractID := wrapper.TotalParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return tokenQuerier(ctx, path, req) +} + +func handleQueryPerms(ctx sdk.Context, tokenQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryPermWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + + req := makeRequestQuery(types.QueryContractIDAccAddressParams{ + Addr: wrapper.PermParam.Address, + }) + + contractID := wrapper.PermParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return tokenQuerier(ctx, path, req) +} + +func handleQueryIsApproved(ctx sdk.Context, tokenQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryIsApprovedWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + + req := makeRequestQuery(types.QueryIsApprovedParams{ + Proxy: wrapper.IsApprovedParam.Proxy, + Approver: wrapper.IsApprovedParam.Approver, + }) + + contractID := wrapper.IsApprovedParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return tokenQuerier(ctx, path, req) +} + +func handleQueryApprovers(ctx sdk.Context, tokenQuerier sdk.Querier, path []string, msgData json.RawMessage) ([]byte, error) { + var wrapper types.QueryApproversWrapper + err := json.Unmarshal(msgData, &wrapper) + if err != nil { + return nil, err + } + + req := makeRequestQuery(types.QueryProxyParams{ + Proxy: wrapper.ApproversParam.Proxy, + }) + + contractID := wrapper.ApproversParam.ContractID + if contractID != "" { + path = append(path, contractID) + } + return tokenQuerier(ctx, path, req) +} + +func makeRequestQuery(params interface{}) abci.RequestQuery { + req := abci.RequestQuery{ + Path: "", + Data: []byte(string(codec.MustMarshalJSONIndent(types.ModuleCdc, params))), + } + return req +} diff --git a/x/token/internal/querier/querier_encoder_test.go b/x/token/internal/querier/querier_encoder_test.go new file mode 100644 index 0000000000..a118591b0d --- /dev/null +++ b/x/token/internal/querier/querier_encoder_test.go @@ -0,0 +1,143 @@ +package querier + +import ( + "encoding/json" + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/line/lbm-sdk/v2/x/wasm" + "github.com/stretchr/testify/require" +) + +var ( + tokenQueryEncoder wasm.EncodeQuerier +) + +func setupQueryEncoder() { + tokenQuerier := NewQuerier(tkeeper) + + tokenQueryEncoder = NewQueryEncoder(tokenQuerier) +} + +func encodeQuery(t *testing.T, jsonQuerier json.RawMessage, result interface{}) error { + res, err := tokenQueryEncoder(ctx, jsonQuerier) + if len(res) > 0 { + require.NoError(t, tkeeper.UnmarshalJSON(res, result)) + } + return err +} + +func TestNewQuerier_encodeQueryTokens(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"tokens","data":{"token_param":{"contract_id":"%s"}}}`, contractID) + + var token types.Token + err := encodeQuery(t, json.RawMessage(jsonQuerier), &token) + require.NoError(t, err) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetName(), tokenName) + require.Equal(t, token.GetSymbol(), tokenSymbol) + require.Equal(t, token.GetImageURI(), tokenImageURL) +} + +func TestNewQuerier_encodeQueryAccountPermission(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"perms","data":{"perm_param":{"contract_id":"%s","address":"%s"}}}`, contractID, addr1) + + var perms types.Permissions + err := encodeQuery(t, json.RawMessage(jsonQuerier), &perms) + require.NoError(t, err) + require.Equal(t, len(perms), 3) + require.Equal(t, perms[0].String(), "modify") + require.Equal(t, perms[1].String(), "mint") + require.Equal(t, perms[2].String(), "burn") +} + +func TestNewQuerier_encodeQueryBalance(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"balance","data":{"balance_param":{"contract_id":"%s","address":"%s"}}}`, contractID, addr1) + + var balance sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &balance) + require.NoError(t, err) + require.Equal(t, balance.Int64(), int64(tokenAmount-tokenBurned)) +} + +func TestNewQuerier_encodeQueryTotalSupply(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"%s","data":{"total_param":{"contract_id":"%s"}}}`, types.QuerySupply, contractID) + + var supply sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &supply) + require.NoError(t, err) + require.Equal(t, supply.Int64(), int64(tokenAmount-tokenBurned)) +} + +func TestNewQuerier_encodeQueryTotalMint(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"%s","data":{"total_param":{"contract_id":"%s"}}}`, types.QueryMint, contractID) + + var supply sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &supply) + require.NoError(t, err) + require.Equal(t, supply.Int64(), int64(tokenAmount)) +} + +func TestNewQuerier_encodeQueryTotalBurn(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"%s","data":{"total_param":{"contract_id":"%s"}}}`, types.QueryBurn, contractID) + + var supply sdk.Int + err := encodeQuery(t, json.RawMessage(jsonQuerier), &supply) + require.NoError(t, err) + require.Equal(t, supply.Int64(), int64(tokenBurned)) +} + +func TestNewQuerier_encodeQueryIsApproved_true(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"approved","data":{"is_approved_param":{"proxy":"%s", "contract_id":"%s","approver":"%s"}}}`, addr1, contractID, addr2) + + var approved bool + err := encodeQuery(t, json.RawMessage(jsonQuerier), &approved) + require.NoError(t, err) + require.True(t, approved) +} + +func TestNewQuerier_encodeQueryApprovers(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintf(`{"route":"approvers","data":{"approvers_param":{"proxy":"%s", "contract_id":"%s"}}}`, addr1, contractID) + + var approvers []sdk.AccAddress + err := encodeQuery(t, json.RawMessage(jsonQuerier), &approvers) + require.NoError(t, err) + require.Equal(t, 2, len(approvers)) + require.True(t, types.IsAddressContains(approvers, addr3)) + require.True(t, types.IsAddressContains(approvers, addr2)) + + var acAdEmpty []sdk.AccAddress + jsonQuerier = fmt.Sprintf(`{"route":"approvers","data":{"approvers_param":{"proxy":"%s", "contract_id":"%s"}}}`, addr2, contractID) + + err = encodeQuery(t, json.RawMessage(jsonQuerier), &acAdEmpty) + require.NoError(t, err) + require.Empty(t, acAdEmpty) +} + +func TestNewQuerier_invalidEncode(t *testing.T) { + prepare(t) + setupQueryEncoder() + jsonQuerier := fmt.Sprintln(`{"route":"noquery","data":{"query_invalid_param":""}}`) + + err := encodeQuery(t, json.RawMessage(jsonQuerier), nil) + require.EqualError(t, err, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized Msg route: %T", "noquery").Error()) +} diff --git a/x/token/internal/querier/querier_test.go b/x/token/internal/querier/querier_test.go new file mode 100644 index 0000000000..7d04f91ea2 --- /dev/null +++ b/x/token/internal/querier/querier_test.go @@ -0,0 +1,193 @@ +package querier + +import ( + "context" + "testing" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/types" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +const ( + contractID = "9be17165" + tokenName = "linko token" + tokenSymbol = "LINKO" + tokenImageURL = "url" + tokenAmount = 1000 + tokenBurned = 10 + tokenMeta = "{}" +) + +var ( + ms store.CommitMultiStore + ctx sdk.Context + tkeeper keeper.Keeper + addr1 sdk.AccAddress + addr2 sdk.AccAddress + addr3 sdk.AccAddress +) + +func prepare(t *testing.T) { + ctx, ms, tkeeper = keeper.TestKeeper() + msCache := ms.CacheMultiStore() + ctx = ctx.WithMultiStore(msCache) + + addr1 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr3 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + + // prepare token + ctx2 := ctx.WithContext(context.WithValue(ctx.Context(), contract.CtxKey{}, contractID)) + require.NoError(t, tkeeper.IssueToken(ctx2, types.NewToken(contractID, tokenName, tokenSymbol, tokenMeta, tokenImageURL, sdk.NewInt(1), true), sdk.NewInt(tokenAmount), addr1, addr1)) + require.NoError(t, tkeeper.BurnToken(ctx2, sdk.NewInt(tokenBurned), addr1)) + + require.NoError(t, tkeeper.GrantPermission(ctx2, addr1, addr2, types.NewBurnPermission())) + require.NoError(t, tkeeper.SetApproved(ctx2, addr1, addr2)) + + // prepare one more approver for test proxy + require.NoError(t, tkeeper.SetApproved(ctx2, addr1, addr3)) +} + +func query(t *testing.T, params interface{}, query string, result interface{}) { + req := abci.RequestQuery{ + Path: "", + Data: []byte(string(codec.MustMarshalJSONIndent(types.ModuleCdc, params))), + } + if params == nil { + req.Data = nil + } + path := []string{query} + if contractID != "" { + path = append(path, contractID) + } + querier := NewQuerier(tkeeper) + res, err := querier(ctx, path, req) + require.NoError(t, err) + if len(res) > 0 { + require.NoError(t, tkeeper.UnmarshalJSON(res, result)) + } +} + +func TestNewQuerier_queryAccountPermission(t *testing.T) { + prepare(t) + + params := types.NewQueryContractIDAccAddressParams(addr1) + var perms types.Permissions + query(t, params, types.QueryPerms, &perms) + require.Equal(t, len(perms), 3) + require.Equal(t, perms[0].String(), "modify") + require.Equal(t, perms[1].String(), "mint") + require.Equal(t, perms[2].String(), "burn") +} + +func TestNewQuerier_queryTokens_one(t *testing.T) { + prepare(t) + + var token types.Token + query(t, nil, types.QueryTokens, &token) + require.Equal(t, token.GetContractID(), contractID) + require.Equal(t, token.GetName(), tokenName) + require.Equal(t, token.GetSymbol(), tokenSymbol) + require.Equal(t, token.GetImageURI(), tokenImageURL) +} + +func TestNewQuerier_queryBalance(t *testing.T) { + prepare(t) + + params := types.QueryContractIDAccAddressParams{ + Addr: addr1, + } + var balance sdk.Int + query(t, params, types.QueryBalance, &balance) + require.Equal(t, balance.Int64(), int64(tokenAmount-tokenBurned)) +} + +func TestNewQuerier_queryTotalSupply(t *testing.T) { + prepare(t) + + var supply sdk.Int + query(t, nil, types.QuerySupply, &supply) + require.Equal(t, supply.Int64(), int64(tokenAmount-tokenBurned)) +} + +func TestNewQuerier_queryTotalMint(t *testing.T) { + prepare(t) + + var supply sdk.Int + query(t, nil, types.QueryMint, &supply) + require.Equal(t, supply.Int64(), int64(tokenAmount)) +} + +func TestNewQuerier_queryTotalBurn(t *testing.T) { + prepare(t) + + var supply sdk.Int + query(t, nil, types.QueryBurn, &supply) + require.Equal(t, supply.Int64(), int64(tokenBurned)) +} + +func TestNewQuerier_invalid(t *testing.T) { + prepare(t) + params := types.QueryContractIDAccAddressParams{ + Addr: addr1, + } + querier := NewQuerier(tkeeper) + path := []string{"noquery", contractID} + req := abci.RequestQuery{ + Path: "", + Data: []byte(string(codec.MustMarshalJSONIndent(types.ModuleCdc, params))), + } + _, err := querier(ctx, path, req) + require.EqualError(t, err, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown token query endpoint").Error()) +} + +func TestNewQuerier_queryIsApproved_true(t *testing.T) { + prepare(t) + + params := types.QueryIsApprovedParams{ + Proxy: addr1, + Approver: addr2, + } + var approved bool + query(t, params, types.QueryIsApproved, &approved) + require.True(t, approved) +} + +func TestNewQuerier_queryIsApproved_false(t *testing.T) { + prepare(t) + + params := types.QueryIsApprovedParams{ + Proxy: addr2, + Approver: addr1, + } + var approved bool + query(t, params, types.QueryIsApproved, &approved) + require.False(t, approved) +} + +func TestNewQuerier_queryApprovers(t *testing.T) { + prepare(t) + params := types.QueryProxyParams{ + Proxy: addr1, + } + var approvers []sdk.AccAddress + query(t, params, types.QueryApprovers, &approvers) + require.Equal(t, 2, len(approvers)) + require.True(t, types.IsAddressContains(approvers, addr3)) + require.True(t, types.IsAddressContains(approvers, addr2)) + + var acAdEmpty []sdk.AccAddress + paramsEmpty := types.QueryProxyParams{ + Proxy: addr2, + } + query(t, paramsEmpty, types.QueryApprovers, &acAdEmpty) + require.Empty(t, acAdEmpty) +} diff --git a/x/token/internal/types/account.go b/x/token/internal/types/account.go new file mode 100644 index 0000000000..3c3ddc8776 --- /dev/null +++ b/x/token/internal/types/account.go @@ -0,0 +1,56 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type TokenID string + +type Account interface { + GetAddress() sdk.AccAddress + GetContractID() string + GetBalance() sdk.Int + SetBalance(amount sdk.Int) Account + String() string +} + +type BaseAccount struct { + ContractID string `json:"contract_id"` + Address sdk.AccAddress `json:"address"` + Amount sdk.Int `json:"amount"` +} + +func NewBaseAccountWithAddress(contractID string, addr sdk.AccAddress) *BaseAccount { + return &BaseAccount{ + ContractID: contractID, + Address: addr, + Amount: sdk.ZeroInt(), + } +} + +func (acc BaseAccount) GetContractID() string { + return acc.ContractID +} + +func (acc BaseAccount) String() string { + b, err := json.Marshal(acc) + if err != nil { + panic(err) + } + return string(b) +} + +func (acc BaseAccount) GetAddress() sdk.AccAddress { + return acc.Address +} + +func (acc BaseAccount) GetBalance() sdk.Int { + return acc.Amount +} + +func (acc BaseAccount) SetBalance(amount sdk.Int) Account { + acc.Amount = amount + return acc +} diff --git a/x/token/internal/types/account_test.go b/x/token/internal/types/account_test.go new file mode 100644 index 0000000000..dfecb83ab7 --- /dev/null +++ b/x/token/internal/types/account_test.go @@ -0,0 +1,23 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestAccount(t *testing.T) { + var acc Account + acc = NewBaseAccountWithAddress(defaultContractID, addr1) + + require.Equal(t, defaultContractID, acc.GetContractID()) + require.Equal(t, addr1, acc.GetAddress()) + require.Equal(t, sdk.ZeroInt(), acc.GetBalance()) + + acc = acc.SetBalance(sdk.OneInt()) + + require.Equal(t, sdk.OneInt(), acc.GetBalance()) + + require.True(t, len(acc.String()) > 0) +} diff --git a/x/token/internal/types/codec.go b/x/token/internal/types/codec.go new file mode 100644 index 0000000000..20f5c39a99 --- /dev/null +++ b/x/token/internal/types/codec.go @@ -0,0 +1,35 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +var ModuleCdc *codec.Codec + +func init() { + ModuleCdc = codec.New() + RegisterCodec(ModuleCdc) + ModuleCdc.Seal() +} + +// RegisterCodec registers concrete types on the Amino codec +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(MsgIssue{}, "token/MsgIssue", nil) + cdc.RegisterConcrete(MsgModify{}, "token/MsgModify", nil) + cdc.RegisterConcrete(MsgMint{}, "token/MsgMint", nil) + cdc.RegisterConcrete(MsgBurn{}, "token/MsgBurn", nil) + cdc.RegisterConcrete(MsgGrantPermission{}, "token/MsgGrantPermission", nil) + cdc.RegisterConcrete(MsgRevokePermission{}, "token/MsgRevokePermission", nil) + cdc.RegisterConcrete(MsgTransfer{}, "token/MsgTransfer", nil) + cdc.RegisterInterface((*Token)(nil), nil) + cdc.RegisterConcrete(&BaseToken{}, "token/Token", nil) + cdc.RegisterInterface((*Supply)(nil), nil) + cdc.RegisterConcrete(&BaseSupply{}, "token/Supply", nil) + cdc.RegisterInterface((*Account)(nil), nil) + cdc.RegisterConcrete(&BaseAccount{}, "token/Account", nil) + cdc.RegisterInterface((*AccountPermissionI)(nil), nil) + cdc.RegisterConcrete(&AccountPermission{}, "token/AccountPermission", nil) + cdc.RegisterConcrete(MsgApprove{}, "token/MsgApprove", nil) + cdc.RegisterConcrete(MsgTransferFrom{}, "token/MsgTransferFrom", nil) + cdc.RegisterConcrete(MsgBurnFrom{}, "token/MsgBurnFrom", nil) +} diff --git a/x/token/internal/types/common_test.go b/x/token/internal/types/common_test.go new file mode 100644 index 0000000000..16d9aff437 --- /dev/null +++ b/x/token/internal/types/common_test.go @@ -0,0 +1,21 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +const ( + defaultName = "name" + defaultContractID = "linktkn" + defaultSymbol = "BTC" + defaultMeta = "{}" + defaultImageURI = "image-uri" + defaultDecimals = 6 + defaultAmount = 1000 +) + +var ( + addr1 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) +) diff --git a/x/token/internal/types/encoder.go b/x/token/internal/types/encoder.go new file mode 100644 index 0000000000..f152e4e232 --- /dev/null +++ b/x/token/internal/types/encoder.go @@ -0,0 +1,93 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type EncodeHandler func(jsonMsg json.RawMessage) ([]sdk.Msg, error) +type EncodeQuerier func(ctx sdk.Context, jsonQuerier json.RawMessage) ([]byte, error) + +const ( + EncodeRouterKey = "tokenencode" +) + +type MsgRoute string + +const ( + RIssue = MsgRoute("issue") + RTransfer = MsgRoute("transfer") + RTransferFrom = MsgRoute("transfer_from") + RMint = MsgRoute("mint") + RBurn = MsgRoute("burn") + RBurnFrom = MsgRoute("burn_from") + RGrantPerm = MsgRoute("grant_perm") + RRevokePerm = MsgRoute("revoke_perm") + RModify = MsgRoute("modify") + RApprove = MsgRoute("approve") +) + +// WasmCustomMsg - wasm custom msg parser +type WasmCustomMsg struct { + Route string `json:"route"` + Data json.RawMessage `json:"data"` +} + +type WasmCustomQuerier struct { + Route string `json:"route"` + Data json.RawMessage `json:"data"` +} + +type QueryTokenWrapper struct { + TokenParam TokenParam `json:"token_param"` +} + +type TokenParam struct { + ContractID string `json:"contract_id"` +} + +type QueryBalanceWrapper struct { + BalanceParam BalanceParam `json:"balance_param"` +} + +type BalanceParam struct { + ContractID string `json:"contract_id"` + Address sdk.AccAddress `json:"address"` +} + +type QueryTotalWrapper struct { + TotalParam TotalParam `json:"total_param"` +} + +type TotalParam struct { + ContractID string `json:"contract_id"` +} + +type QueryPermWrapper struct { + PermParam PermParam `json:"perm_param"` +} + +type PermParam struct { + ContractID string `json:"contract_id"` + Address sdk.AccAddress `json:"address"` +} + +type QueryIsApprovedWrapper struct { + IsApprovedParam IsApprovedParam `json:"is_approved_param"` +} + +type IsApprovedParam struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + Approver sdk.AccAddress `json:"approver"` +} + +type QueryApproversWrapper struct { + ApproversParam ApproversParam `json:"approvers_param"` +} + +type ApproversParam struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` +} diff --git a/x/token/internal/types/errors.go b/x/token/internal/types/errors.go new file mode 100644 index 0000000000..dab4ad7b4a --- /dev/null +++ b/x/token/internal/types/errors.go @@ -0,0 +1,45 @@ +package types + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var ( + ErrTokenExist = sdkerrors.Register(ModuleName, 1, "token already exists") + ErrTokenNotExist = sdkerrors.Register(ModuleName, 2, "token does not exist") + ErrTokenNotMintable = sdkerrors.Register(ModuleName, 3, "token is not mintable") + ErrInvalidTokenName = sdkerrors.Register(ModuleName, 4, "token name should not be empty") + ErrInvalidTokenDecimals = sdkerrors.Register(ModuleName, 5, "token decimals should be within the range in 0 ~ 18") + ErrInvalidAmount = sdkerrors.Register(ModuleName, 6, "invalid token amount") + ErrInvalidImageURILength = sdkerrors.Register(ModuleName, 7, "invalid token uri length") + ErrInvalidNameLength = sdkerrors.Register(ModuleName, 8, "invalid name length") + ErrInvalidTokenSymbol = sdkerrors.Register(ModuleName, 9, "invalid token symbol") + ErrTokenNoPermission = sdkerrors.Register(ModuleName, 10, "account does not have the permission") + ErrAccountExist = sdkerrors.Register(ModuleName, 11, "account already exists") + ErrAccountNotExist = sdkerrors.Register(ModuleName, 12, "account does not exists") + ErrInsufficientBalance = sdkerrors.Register(ModuleName, 13, "insufficient balance") + ErrSupplyExist = sdkerrors.Register(ModuleName, 14, "supply for token already exists") + ErrInsufficientSupply = sdkerrors.Register(ModuleName, 15, "insufficient supply") + ErrInvalidChangesFieldCount = sdkerrors.Register(ModuleName, 16, "invalid count of field changes") + ErrEmptyChanges = sdkerrors.Register(ModuleName, 17, "changes is empty") + ErrInvalidChangesField = sdkerrors.Register(ModuleName, 18, "invalid field of changes") + ErrDuplicateChangesField = sdkerrors.Register(ModuleName, 19, "invalid field of changes") + ErrInvalidMetaLength = sdkerrors.Register(ModuleName, 20, "invalid meta length") + ErrSupplyOverflow = sdkerrors.Register(ModuleName, 21, "supply for token reached maximum") + ErrApproverProxySame = sdkerrors.Register(ModuleName, 22, "approver is same with proxy") + ErrTokenNotApproved = sdkerrors.Register(ModuleName, 23, "proxy is not approved on the token") + ErrTokenAlreadyApproved = sdkerrors.Register(ModuleName, 24, "proxy is already approved on the token") + ErrInvalidPermissionAction = sdkerrors.Register(ModuleName, 25, "invalid permission action") +) + +func WrapIfOverflowPanic(r interface{}) error { + if isOverflowPanic(r) { + return ErrSupplyOverflow + } + // unknown panic, bubble up :( + panic(r) +} + +func isOverflowPanic(r interface{}) bool { + return r == "Int overflow" || r == "negative coin amount" +} diff --git a/x/token/internal/types/events.go b/x/token/internal/types/events.go new file mode 100644 index 0000000000..44d5e4460b --- /dev/null +++ b/x/token/internal/types/events.go @@ -0,0 +1,30 @@ +package types + +var ( + EventTypeIssueToken = "issue" + EventTypeMintToken = "mint" + EventTypeBurnToken = "burn" + EventTypeBurnTokenFrom = "burn_from" + EventTypeModifyToken = "modify_token" + EventTypeGrantPermToken = "grant_perm" + EventTypeRevokePermToken = "revoke_perm" + EventTypeTransfer = "transfer" + EventTypeTransferFrom = "transfer_from" + EventTypeApproveToken = "approve_token" + + AttributeKeyName = "name" + AttributeKeySymbol = "symbol" + AttributeKeyContractID = "contract_id" + AttributeKeyOwner = "owner" + AttributeKeyAmount = "amount" + AttributeKeyDecimals = "decimals" + AttributeKeyMeta = "meta" + AttributeKeyImageURI = "img_uri" + AttributeKeyMintable = "mintable" + AttributeKeyFrom = "from" + AttributeKeyTo = "to" + AttributeKeyPerm = "perm" + AttributeKeyApprover = "approver" + AttributeKeyProxy = "proxy" + AttributeValueCategory = ModuleName +) diff --git a/x/token/internal/types/expected_keeper.go b/x/token/internal/types/expected_keeper.go new file mode 100644 index 0000000000..865b6266c3 --- /dev/null +++ b/x/token/internal/types/expected_keeper.go @@ -0,0 +1,12 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + auth "github.com/cosmos/cosmos-sdk/x/auth/exported" +) + +type AccountKeeper interface { + NewAccountWithAddress(ctx sdk.Context, addr sdk.AccAddress) auth.Account + GetAccount(ctx sdk.Context, addr sdk.AccAddress) auth.Account + SetAccount(ctx sdk.Context, acc auth.Account) +} diff --git a/x/token/internal/types/key.go b/x/token/internal/types/key.go new file mode 100644 index 0000000000..752e689b35 --- /dev/null +++ b/x/token/internal/types/key.go @@ -0,0 +1,49 @@ +package types + +import sdk "github.com/cosmos/cosmos-sdk/types" + +const ( + ModuleName = "token" + + StoreKey = ModuleName + RouterKey = ModuleName +) + +var ( + TokenKeyPrefix = []byte{0x00} + BlacklistKeyPrefix = []byte{0x01} + AccountKeyPrefix = []byte{0x02} + SupplyKeyPrefix = []byte{0x03} + PermKeyPrefix = []byte{0x04} + TokenApprovedKeyPrefix = []byte{0x05} +) + +func BlacklistKey(addr sdk.AccAddress, action string) []byte { + key := append(BlacklistKeyPrefix, addr...) + key = append(key, []byte(":"+action)...) + return key +} + +func TokenKey(contractID string) []byte { + return append(TokenKeyPrefix, []byte(contractID)...) +} + +func SupplyKey(contractID string) []byte { + return append(SupplyKeyPrefix, []byte(contractID)...) +} + +func AccountKey(contractID string, addr sdk.AccAddress) []byte { + return append(append(AccountKeyPrefix, []byte(contractID)...), addr...) +} + +func PermKey(contractID string, addr sdk.AccAddress) []byte { + return append(append(PermKeyPrefix, []byte(contractID)...), addr...) +} + +func TokenApprovedKey(contractID string, proxy sdk.AccAddress, approver sdk.AccAddress) []byte { + return append(append(append(TokenApprovedKeyPrefix, []byte(contractID)...), proxy.Bytes()...), approver.Bytes()...) +} + +func TokenApproversKey(contractID string, proxy sdk.AccAddress) []byte { + return append(append(TokenApprovedKeyPrefix, []byte(contractID)...), proxy.Bytes()...) +} diff --git a/x/token/internal/types/key_test.go b/x/token/internal/types/key_test.go new file mode 100644 index 0000000000..a76ad22e00 --- /dev/null +++ b/x/token/internal/types/key_test.go @@ -0,0 +1,31 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +func TestTokenApproveKey(t *testing.T) { + addr1 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + contractID1 := "abcdef012" + contractID2 := "abcdef013" + + require.NotEqual(t, TokenApprovedKey(contractID1, addr1, addr2), TokenApprovedKey(contractID1, addr2, addr1)) + require.NotEqual(t, TokenApprovedKey(contractID1, addr1, addr2), TokenApprovedKey(contractID2, addr1, addr2)) +} + +func TestTokenApproversKey(t *testing.T) { + addr1 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + addr2 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + contractID := "abcdef012" + + tokenApproversKey := TokenApproversKey(contractID, addr1) + + require.NotEqual(t, tokenApproversKey, TokenApproversKey(contractID, addr2)) + require.Contains(t, string(tokenApproversKey), contractID) + require.Contains(t, string(tokenApproversKey), string(addr1.Bytes())) +} diff --git a/x/token/internal/types/modify.go b/x/token/internal/types/modify.go new file mode 100644 index 0000000000..a0e22b2c6f --- /dev/null +++ b/x/token/internal/types/modify.go @@ -0,0 +1,29 @@ +package types + +type Change struct { + Field string `json:"field"` + Value string `json:"value"` +} + +func NewChange(field string, value string) Change { + return Change{ + Field: field, + Value: value, + } +} + +type Changes []Change + +func NewChanges(changes ...Change) Changes { + return changes +} + +func NewChangesWithMap(changesMap map[string]string) Changes { + changes := make([]Change, len(changesMap)) + idx := 0 + for k, v := range changesMap { + changes[idx] = Change{Field: k, Value: v} + idx++ + } + return NewChanges(changes...) +} diff --git a/x/token/internal/types/msgs_modify.go b/x/token/internal/types/msgs_modify.go new file mode 100644 index 0000000000..b1c3725ab4 --- /dev/null +++ b/x/token/internal/types/msgs_modify.go @@ -0,0 +1,48 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ contract.Msg = (*MsgModify)(nil) + +type MsgModify struct { + Owner sdk.AccAddress `json:"owner"` + ContractID string `json:"contract_id"` + Changes Changes `json:"changes"` +} + +func NewMsgModify(owner sdk.AccAddress, contractID string, changes Changes) MsgModify { + return MsgModify{ + Owner: owner, + ContractID: contractID, + Changes: changes, + } +} + +func (msg MsgModify) Route() string { return RouterKey } +func (msg MsgModify) Type() string { return "modify_token" } +func (msg MsgModify) GetContractID() string { return msg.ContractID } +func (msg MsgModify) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} +func (msg MsgModify) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Owner} } + +func (msg MsgModify) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + validator := NewChangesValidator() + if err := validator.Validate(msg.Changes); err != nil { + return err + } + + if msg.Owner.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty") + } + + return nil +} diff --git a/x/token/internal/types/msgs_modify_test.go b/x/token/internal/types/msgs_modify_test.go new file mode 100644 index 0000000000..3d3917ac1a --- /dev/null +++ b/x/token/internal/types/msgs_modify_test.go @@ -0,0 +1,132 @@ +package types + +import ( + "testing" + "unicode/utf8" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +const ( + ModifyMsgType = "modify_token" + DefaultContractID = "abcd1234" +) + +func TestNewMsgModify(t *testing.T) { + msg := AMsgModify().Build() + + require.Equal(t, ModifyMsgType, msg.Type()) + require.Equal(t, ModuleName, msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, msg.Owner, msg.GetSigners()[0]) +} + +func TestMarshalMsgModify(t *testing.T) { + // Given + msg := AMsgModify().Build() + + // When marshal and unmarshal it + msg2 := MsgModify{} + err := ModuleCdc.UnmarshalJSON(msg.GetSignBytes(), &msg2) + require.NoError(t, err) + + // Then they are equal + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.Changes, msg2.Changes) + require.Equal(t, msg.Owner, msg2.Owner) +} + +func TestMsgModify_ValidateBasic(t *testing.T) { + t.Log("normal case") + { + msg := AMsgModify().Build() + require.NoError(t, msg.ValidateBasic()) + } + t.Log("empty contractID found") + { + msg := AMsgModify().ContractID("").Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(contract.ErrInvalidContractID, "ContractID: ").Error()) + } + t.Log("empty owner") + { + msg := AMsgModify().Owner(nil).Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty").Error()) + } + t.Log("invalid contractID found") + { + invalidContractID := "invalid2198721987" + msg := AMsgModify().ContractID(invalidContractID).Build() + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(contract.ErrInvalidContractID, "ContractID: %s", invalidContractID).Error()) + } + t.Log("image uri too long") + { + msg := AMsgModify().Changes(NewChangesWithMap(map[string]string{"img_uri": length1001String})).Build() + + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrInvalidImageURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", length1001String, MaxImageURILength, utf8.RuneCountInString(length1001String)).Error()) + } + t.Log("name too long") + { + msg := AMsgModify().Changes(NewChangesWithMap(map[string]string{"name": length1001String})).Build() + + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", length1001String, MaxTokenNameLength, utf8.RuneCountInString(length1001String)).Error()) + } + t.Log("invalid changes field") + { + msg := AMsgModify().Changes(NewChangesWithMap(map[string]string{"invalid_field": "val"})).Build() + + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidChangesField, "Field: invalid_field").Error()) + } + t.Log("no token uri field") + { + msg := AMsgModify().Changes(NewChangesWithMap(map[string]string{"name": "new_name"})).Build() + require.NoError(t, msg.ValidateBasic()) + } + t.Log("Test with changes more than max") + { + // Given changes more than max + changeList := make([]Change, MaxChangeFieldsCount+1) + msg := AMsgModify().Changes(changeList).Build() + + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrInvalidChangesFieldCount, "You can not change fields more than [%d] at once, current count: [%d]", MaxChangeFieldsCount, len(changeList)).Error()) + } +} + +func AMsgModify() *MsgModifyBuilder { + return &MsgModifyBuilder{ + msgModify: NewMsgModify( + sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()), + DefaultContractID, + NewChangesWithMap(map[string]string{ + "name": "new_name", + "img_uri": "new_img_uri", + }), + ), + } +} + +type MsgModifyBuilder struct { + msgModify MsgModify +} + +func (b *MsgModifyBuilder) Build() MsgModify { + return b.msgModify +} + +func (b *MsgModifyBuilder) Owner(owner sdk.AccAddress) *MsgModifyBuilder { + b.msgModify.Owner = owner + return b +} + +func (b *MsgModifyBuilder) ContractID(contractID string) *MsgModifyBuilder { + b.msgModify.ContractID = contractID + return b +} + +func (b *MsgModifyBuilder) Changes(changes Changes) *MsgModifyBuilder { + b.msgModify.Changes = changes + return b +} diff --git a/x/token/internal/types/msgs_perm.go b/x/token/internal/types/msgs_perm.go new file mode 100644 index 0000000000..9c027fe893 --- /dev/null +++ b/x/token/internal/types/msgs_perm.go @@ -0,0 +1,95 @@ +package types + +import ( + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ sdk.Msg = (*MsgGrantPermission)(nil) + +func NewMsgGrantPermission(from sdk.AccAddress, contractID string, to sdk.AccAddress, perm Permission) MsgGrantPermission { + return MsgGrantPermission{ + From: from, + ContractID: contractID, + To: to, + Permission: perm, + } +} + +type MsgGrantPermission struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Permission Permission `json:"permission"` +} + +func (MsgGrantPermission) Route() string { return RouterKey } +func (MsgGrantPermission) Type() string { return "grant_perm" } +func (msg MsgGrantPermission) GetContractID() string { return msg.ContractID } +func (msg MsgGrantPermission) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgGrantPermission) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgGrantPermission) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.From.Empty() || msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "addresses cannot be empty") + } + + if msg.From.Equals(msg.To) { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from, to address can not be the same") + } + + return validateAction(msg.Permission.String(), MintAction, BurnAction, ModifyAction) +} + +var _ sdk.Msg = (*MsgRevokePermission)(nil) + +func NewMsgRevokePermission(from sdk.AccAddress, contractID string, perm Permission) MsgRevokePermission { + return MsgRevokePermission{ + From: from, + ContractID: contractID, + Permission: perm, + } +} + +type MsgRevokePermission struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + Permission Permission `json:"permission"` +} + +func (MsgRevokePermission) Route() string { return RouterKey } +func (MsgRevokePermission) Type() string { return "revoke_perm" } +func (msg MsgRevokePermission) GetContractID() string { return msg.ContractID } +func (msg MsgRevokePermission) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgRevokePermission) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgRevokePermission) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "addresses cannot be empty") + } + + return validateAction(msg.Permission.String(), MintAction, BurnAction, ModifyAction) +} +func validateAction(action string, actions ...string) error { + for _, a := range actions { + if action == a { + return nil + } + } + return sdkerrors.Wrap(ErrInvalidPermissionAction, + fmt.Sprintf("permission action should be one of [%s]", strings.Join(actions, ","))) +} diff --git a/x/token/internal/types/msgs_proxy.go b/x/token/internal/types/msgs_proxy.go new file mode 100644 index 0000000000..85b410ef0a --- /dev/null +++ b/x/token/internal/types/msgs_proxy.go @@ -0,0 +1,49 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ contract.Msg = (*MsgApprove)(nil) + +type MsgApprove struct { + Approver sdk.AccAddress `json:"approver"` + ContractID string `json:"contract_id"` + Proxy sdk.AccAddress `json:"proxy"` +} + +func NewMsgApprove(approver sdk.AccAddress, contractID string, proxy sdk.AccAddress) MsgApprove { + return MsgApprove{ + Approver: approver, + ContractID: contractID, + Proxy: proxy, + } +} + +func (msg MsgApprove) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return nil + } + if msg.Approver.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Approver cannot be empty") + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.Approver.Equals(msg.Proxy) { + return sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", msg.Approver.String()) + } + return nil +} + +func (MsgApprove) Route() string { return RouterKey } +func (MsgApprove) Type() string { return "approve_token" } +func (msg MsgApprove) GetContractID() string { return msg.ContractID } +func (msg MsgApprove) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Approver} +} +func (msg MsgApprove) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} diff --git a/x/token/internal/types/msgs_test.go b/x/token/internal/types/msgs_test.go new file mode 100644 index 0000000000..1f8b47bb75 --- /dev/null +++ b/x/token/internal/types/msgs_test.go @@ -0,0 +1,252 @@ +package types + +import ( + "strings" + "testing" + "unicode/utf8" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +// nolint:dupl +func TestMsgBasics(t *testing.T) { + cdc := ModuleCdc + addr := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + length1001String := strings.Repeat("Eng글자日本語はスゲ", 91) // 11 * 91 = 1001 + + { + msg := NewMsgIssue(addr, addr, "name", "BTC", "{}", "imageuri", sdk.NewInt(1), sdk.NewInt(8), true) + require.Equal(t, "issue_token", msg.Type()) + require.Equal(t, "token", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgIssue{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Name, msg2.Name) + require.Equal(t, msg.Symbol, msg2.Symbol) + require.Equal(t, msg.ImageURI, msg2.ImageURI) + require.Equal(t, msg.Owner, msg2.Owner) + require.Equal(t, msg.Amount, msg.Amount) + require.Equal(t, msg.Decimals, msg2.Decimals) + require.Equal(t, msg.Mintable, msg2.Mintable) + } + { + msg := NewMsgIssue(addr, addr, "name", "BTC", "", length1001String, sdk.NewInt(1), sdk.NewInt(8), true) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrInvalidImageURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", length1001String, MaxImageURILength, utf8.RuneCountInString(length1001String)).Error()) + } + { + msg := NewMsgIssue(addr, addr, "name", "", "", length1001String, sdk.NewInt(1), sdk.NewInt(8), true) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenSymbol, "Symbol: ").Error()) + } + { + msg := NewMsgIssue(addr, addr, "name", "123456789012345678901", "", length1001String, sdk.NewInt(1), sdk.NewInt(8), true) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenSymbol, "Symbol: 123456789012345678901").Error()) + } + { + msg := NewMsgIssue(addr, addr, "name", "BCD_A", "", length1001String, sdk.NewInt(1), sdk.NewInt(8), true) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenSymbol, "Symbol: BCD_A").Error()) + } + { + msg := NewMsgIssue(addr, addr, "name", "12", "", length1001String, sdk.NewInt(1), sdk.NewInt(8), true) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidTokenSymbol, "Symbol: 12").Error()) + } + { + msg := NewMsgIssue(addr, addr, length1001String, "BTC", "", "tokenuri", sdk.NewInt(1), sdk.NewInt(8), true) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", length1001String, MaxTokenNameLength, utf8.RuneCountInString(length1001String)).Error()) + } + { + msg := NewMsgIssue(addr, addr, "", "BTC", "", "tokenuri", sdk.NewInt(1), sdk.NewInt(8), true) + require.EqualError(t, msg.ValidateBasic(), ErrInvalidTokenName.Error()) + } + { + msg := NewMsgIssue(addr, addr, "name", "BTC", "", "tokenuri", sdk.NewInt(1), sdk.NewInt(19), true) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrInvalidTokenDecimals, "Decimals: %s", sdk.NewInt(19).String()).Error()) + } + { + msg := NewMsgMint(addr, contract.SampleContractID, addr, sdk.NewInt(1)) + require.Equal(t, "mint", msg.Type()) + require.Equal(t, "token", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgMint{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.Amount, msg2.Amount) + } + { + msg := NewMsgBurn(addr, contract.SampleContractID, sdk.NewInt(1)) + require.Equal(t, "burn", msg.Type()) + require.Equal(t, "token", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgBurn{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.Amount, msg2.Amount) + } + { + msg := NewMsgBurnFrom(addr1, contract.SampleContractID, addr2, sdk.NewInt(1)) + require.Equal(t, "burn_from", msg.Type()) + require.Equal(t, "token", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgBurnFrom{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.Amount, msg2.Amount) + } + + { + msg := NewMsgBurnFrom(addr1, contract.SampleContractID, addr1, sdk.NewInt(1)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", addr1.String()).Error()) + + msg = NewMsgBurnFrom(nil, contract.SampleContractID, addr1, sdk.NewInt(1)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty").Error()) + + msg = NewMsgBurnFrom(addr1, contract.SampleContractID, nil, sdk.NewInt(1)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgBurnFrom(addr1, contract.SampleContractID, addr2, sdk.NewInt(-1)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(ErrInvalidAmount, "-1").Error()) + } + { + addr2 := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + msg := NewMsgGrantPermission(addr, contract.SampleContractID, addr2, NewMintPermission()) + require.Equal(t, "grant_perm", msg.Type()) + require.Equal(t, "token", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgGrantPermission{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.Permission, msg2.Permission) + } + + { + msg := NewMsgRevokePermission(addr, contract.SampleContractID, NewMintPermission()) + require.Equal(t, "revoke_perm", msg.Type()) + require.Equal(t, "token", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgRevokePermission{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.Permission, msg2.Permission) + } + + { + msg := NewMsgTransfer(addr, addr, contract.SampleContractID, sdk.NewInt(4)) + require.Equal(t, "transfer_ft", msg.Type()) + require.Equal(t, "token", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgTransfer{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.Amount, msg2.Amount) + } + + { + msg := NewMsgTransfer(nil, addr, contract.SampleContractID, sdk.NewInt(4)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from cannot be empty").Error()) + + msg = NewMsgTransfer(addr, nil, contract.SampleContractID, sdk.NewInt(4)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "to cannot be empty").Error()) + + msg = NewMsgTransfer(addr, addr, "m", sdk.NewInt(4)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(contract.ErrInvalidContractID, "ContractID: m").Error()) + + msg = NewMsgTransfer(addr, addr, contract.SampleContractID, sdk.NewInt(-1)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "send amount must be positive").Error()) + } + + { + msg := NewMsgTransferFrom(addr1, contract.SampleContractID, addr2, addr2, sdk.NewInt(defaultAmount)) + require.Equal(t, "transfer_from", msg.Type()) + require.Equal(t, "token", msg.Route()) + require.Equal(t, sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)), msg.GetSignBytes()) + require.Equal(t, addr1, msg.GetSigners()[0]) + require.NoError(t, msg.ValidateBasic()) + + b := msg.GetSignBytes() + + msg2 := MsgTransferFrom{} + + err := cdc.UnmarshalJSON(b, &msg2) + require.NoError(t, err) + + require.Equal(t, msg.Proxy, msg2.Proxy) + require.Equal(t, msg.From, msg2.From) + require.Equal(t, msg.To, msg2.To) + require.Equal(t, msg.ContractID, msg2.ContractID) + require.Equal(t, msg.Amount, msg2.Amount) + } + + { + msg := NewMsgTransferFrom(nil, contract.SampleContractID, addr2, addr2, sdk.NewInt(defaultAmount)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty").Error()) + + msg = NewMsgTransferFrom(addr1, contract.SampleContractID, nil, addr2, sdk.NewInt(defaultAmount)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty").Error()) + + msg = NewMsgTransferFrom(addr1, contract.SampleContractID, addr2, nil, sdk.NewInt(defaultAmount)) + require.EqualError(t, msg.ValidateBasic(), sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty").Error()) + } +} diff --git a/x/token/internal/types/msgs_token.go b/x/token/internal/types/msgs_token.go new file mode 100644 index 0000000000..86fb4e3281 --- /dev/null +++ b/x/token/internal/types/msgs_token.go @@ -0,0 +1,213 @@ +package types + +import ( + "unicode/utf8" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ sdk.Msg = (*MsgIssue)(nil) + +type MsgIssue struct { + Owner sdk.AccAddress `json:"owner"` + To sdk.AccAddress `json:"to"` + Name string `json:"name"` + Symbol string `json:"symbol"` + ImageURI string `json:"img_uri"` + Meta string `json:"meta"` + Amount sdk.Int `json:"amount"` + Mintable bool `json:"mintable"` + Decimals sdk.Int `json:"decimals"` +} + +func NewMsgIssue(owner, to sdk.AccAddress, name, symbol, meta string, imageURI string, amount sdk.Int, decimal sdk.Int, mintable bool) MsgIssue { + return MsgIssue{ + Owner: owner, + To: to, + Name: name, + Symbol: symbol, + Meta: meta, + ImageURI: imageURI, + Amount: amount, + Mintable: mintable, + Decimals: decimal, + } +} + +func (msg MsgIssue) Route() string { return RouterKey } +func (msg MsgIssue) Type() string { return "issue_token" } +func (msg MsgIssue) GetSignBytes() []byte { return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) } +func (msg MsgIssue) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Owner} } + +func (msg MsgIssue) ValidateBasic() error { + if len(msg.Name) == 0 { + return ErrInvalidTokenName + } + if msg.Owner.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner cannot be empty") + } + + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "to cannot be empty") + } + + if !ValidateName(msg.Name) { + return sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.Name, MaxTokenNameLength, utf8.RuneCountInString(msg.Name)) + } + if !ValidateMeta(msg.Meta) { + return sdkerrors.Wrapf(ErrInvalidMetaLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.Meta, MaxTokenMetaLength, utf8.RuneCountInString(msg.Meta)) + } + + if err := ValidateTokenSymbol(msg.Symbol); err != nil { + return sdkerrors.Wrapf(ErrInvalidTokenSymbol, "Symbol: %s", msg.Symbol) + } + + if !ValidateImageURI(msg.ImageURI) { + return sdkerrors.Wrapf(ErrInvalidImageURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", msg.ImageURI, MaxImageURILength, utf8.RuneCountInString(msg.ImageURI)) + } + + if msg.Decimals.GT(sdk.NewInt(18)) || msg.Decimals.IsNegative() { + return sdkerrors.Wrapf(ErrInvalidTokenDecimals, "Decimals: %s", msg.Decimals) + } + + if !msg.Amount.IsPositive() { + return sdkerrors.Wrap(ErrInvalidAmount, msg.Amount.String()) + } + + return nil +} + +var _ contract.Msg = (*MsgMint)(nil) + +type MsgMint struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Amount sdk.Int `json:"amount"` +} + +func NewMsgMint(from sdk.AccAddress, contractID string, to sdk.AccAddress, amount sdk.Int) MsgMint { + return MsgMint{ + From: from, + ContractID: contractID, + To: to, + Amount: amount, + } +} +func (MsgMint) Route() string { return RouterKey } +func (MsgMint) Type() string { return "mint" } +func (msg MsgMint) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgMint) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgMint) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.Amount.IsNegative() { + return sdkerrors.Wrap(ErrInvalidAmount, msg.Amount.String()) + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from cannot be empty") + } + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "to cannot be empty") + } + return nil +} +func (msg MsgMint) GetContractID() string { + return msg.ContractID +} + +var _ contract.Msg = (*MsgBurn)(nil) + +type MsgBurn struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + Amount sdk.Int `json:"amount"` +} + +func NewMsgBurn(from sdk.AccAddress, contractID string, amount sdk.Int) MsgBurn { + return MsgBurn{ + From: from, + ContractID: contractID, + Amount: amount, + } +} +func (MsgBurn) Route() string { return RouterKey } +func (MsgBurn) Type() string { return "burn" } +func (msg MsgBurn) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.From} } +func (msg MsgBurn) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgBurn) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if !msg.Amount.IsPositive() { + return sdkerrors.Wrap(ErrInvalidAmount, msg.Amount.String()) + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner cannot be empty") + } + if msg.Amount.IsNegative() { + return sdkerrors.Wrap(ErrInvalidAmount, msg.Amount.String()) + } + return nil +} + +func (msg MsgBurn) GetContractID() string { + return msg.ContractID +} + +var _ contract.Msg = (*MsgBurnFrom)(nil) + +type MsgBurnFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + Amount sdk.Int `json:"amount"` +} + +func NewMsgBurnFrom(proxy sdk.AccAddress, contractID string, from sdk.AccAddress, amount sdk.Int) MsgBurnFrom { + return MsgBurnFrom{ + Proxy: proxy, + ContractID: contractID, + From: from, + Amount: amount, + } +} +func (MsgBurnFrom) Route() string { return RouterKey } +func (MsgBurnFrom) Type() string { return "burn_from" } +func (msg MsgBurnFrom) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{msg.Proxy} } +func (msg MsgBurnFrom) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgBurnFrom) ValidateBasic() error { + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if !msg.Amount.IsPositive() { + return sdkerrors.Wrap(ErrInvalidAmount, msg.Amount.String()) + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + if msg.Proxy.Equals(msg.From) { + return sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", msg.Proxy.String()) + } + + return nil +} + +func (msg MsgBurnFrom) GetContractID() string { + return msg.ContractID +} diff --git a/x/token/internal/types/msgs_transfer.go b/x/token/internal/types/msgs_transfer.go new file mode 100644 index 0000000000..bb1d6843f8 --- /dev/null +++ b/x/token/internal/types/msgs_transfer.go @@ -0,0 +1,122 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/contract" +) + +var _ contract.Msg = (*MsgTransfer)(nil) + +type MsgTransfer struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Amount sdk.Int `json:"amount"` +} + +func NewMsgTransfer(from sdk.AccAddress, to sdk.AccAddress, contractID string, amount sdk.Int) MsgTransfer { + return MsgTransfer{From: from, To: to, ContractID: contractID, Amount: amount} +} + +func (msg MsgTransfer) Route() string { return RouterKey } + +func (msg MsgTransfer) Type() string { return "transfer_ft" } + +func (msg MsgTransfer) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "from cannot be empty") + } + + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "to cannot be empty") + } + + if !msg.Amount.IsPositive() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "send amount must be positive") + } + return nil +} + +func (msg MsgTransfer) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgTransfer) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.From} +} + +func (msg MsgTransfer) GetContractID() string { + return msg.ContractID +} + +var _ contract.Msg = (*MsgTransferFrom)(nil) + +type MsgTransferFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + To sdk.AccAddress `json:"to"` + Amount sdk.Int `json:"amount"` +} + +func NewMsgTransferFrom(proxy sdk.AccAddress, contractID string, from sdk.AccAddress, to sdk.AccAddress, amount sdk.Int) MsgTransferFrom { + return MsgTransferFrom{ + Proxy: proxy, + ContractID: contractID, + From: from, + To: to, + Amount: amount, + } +} +func (msg MsgTransferFrom) MarshalJSON() ([]byte, error) { + type msgAlias MsgTransferFrom + return json.Marshal(msgAlias(msg)) +} + +func (msg *MsgTransferFrom) UnmarshalJSON(data []byte) error { + type msgAlias *MsgTransferFrom + return json.Unmarshal(data, msgAlias(msg)) +} + +func (MsgTransferFrom) Route() string { return RouterKey } + +func (MsgTransferFrom) Type() string { return "transfer_from" } + +func (msg MsgTransferFrom) GetContractID() string { return msg.ContractID } + +func (msg MsgTransferFrom) ValidateBasic() error { + if err := contract.ValidateContractIDBasic(msg); err != nil { + return err + } + if msg.Proxy.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "Proxy cannot be empty") + } + if msg.From.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "From cannot be empty") + } + if msg.To.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "To cannot be empty") + } + if msg.From.Equals(msg.Proxy) { + return sdkerrors.Wrapf(ErrApproverProxySame, "Approver: %s", msg.From.String()) + } + if !msg.Amount.IsPositive() { + return sdkerrors.Wrap(ErrInvalidAmount, "invalid amount") + } + return nil +} + +func (msg MsgTransferFrom) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgTransferFrom) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Proxy} +} diff --git a/x/token/internal/types/perm.go b/x/token/internal/types/perm.go new file mode 100644 index 0000000000..77391c9045 --- /dev/null +++ b/x/token/internal/types/perm.go @@ -0,0 +1,137 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + MintAction = "mint" + BurnAction = "burn" + ModifyAction = "modify" +) + +type Permission string + +func NewMintPermission() Permission { + return MintAction +} + +func NewBurnPermission() Permission { + return BurnAction +} + +func NewModifyPermission() Permission { + return ModifyAction +} + +func (p Permission) Equal(p2 Permission) bool { + return p == p2 +} +func (p Permission) String() string { + return string(p) +} + +func (p Permission) Validate() bool { + if p == MintAction { + return true + } + if p == BurnAction { + return true + } + if p == ModifyAction { + return true + } + return false +} + +type Permissions []Permission + +func NewPermissions(perms ...Permission) Permissions { + pms := Permissions{} + for _, perm := range perms { + pms.AddPermission(perm) + } + return pms +} + +func (pms *Permissions) GetPermissions() []Permission { + return []Permission(*pms) +} + +func (pms *Permissions) RemoveElement(idx int) { + *pms = append((*pms)[:idx], (*pms)[idx+1:]...) +} + +func (pms *Permissions) AddPermission(p Permission) { + for _, pin := range *pms { + if pin.Equal(p) { + return + } + } + *pms = append(*pms, p) +} + +func (pms *Permissions) RemovePermission(p Permission) { + for idx, pin := range *pms { + if pin.Equal(p) { + pms.RemoveElement(idx) + return + } + } +} + +func (pms Permissions) HasPermission(p Permission) bool { + for _, pin := range pms { + if pin.Equal(p) { + return true + } + } + return false +} +func (pms Permissions) String() string { + return fmt.Sprintf("%#v", pms) +} + +type AccountPermissionI interface { + GetAddress() sdk.AccAddress + HasPermission(Permission) bool + AddPermission(Permission) + RemovePermission(Permission) + String() string + GetPermissions() Permissions +} + +type AccountPermission struct { + Address sdk.AccAddress + Permissions Permissions +} + +func NewAccountPermission(addr sdk.AccAddress) AccountPermissionI { + return &AccountPermission{ + Address: addr, + } +} + +func (ap *AccountPermission) String() string { + return fmt.Sprintf("%#v", ap) +} + +func (ap *AccountPermission) GetPermissions() Permissions { + return ap.Permissions.GetPermissions() +} + +func (ap *AccountPermission) GetAddress() sdk.AccAddress { + return ap.Address +} + +func (ap *AccountPermission) HasPermission(p Permission) bool { + return ap.Permissions.HasPermission(p) +} +func (ap *AccountPermission) AddPermission(p Permission) { + ap.Permissions.AddPermission(p) +} +func (ap *AccountPermission) RemovePermission(p Permission) { + ap.Permissions.RemovePermission(p) +} diff --git a/x/token/internal/types/perm_test.go b/x/token/internal/types/perm_test.go new file mode 100644 index 0000000000..99208113d7 --- /dev/null +++ b/x/token/internal/types/perm_test.go @@ -0,0 +1,50 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPermission(t *testing.T) { + // Given permissions + mintPerm := NewMintPermission() + burnPerm := NewBurnPermission() + modifyPerm := NewModifyPermission() + + require.True(t, mintPerm.Validate()) + require.True(t, burnPerm.Validate()) + require.True(t, modifyPerm.Validate()) + + require.True(t, mintPerm.Equal(mintPerm)) + require.False(t, mintPerm.Equal(burnPerm)) + require.False(t, mintPerm.Equal(modifyPerm)) + + // When make resource or action empty + mintPerm = "" + burnPerm = "" + + // Then they are invalid + require.False(t, mintPerm.Validate()) + require.False(t, burnPerm.Validate()) +} + +func TestPermissionString(t *testing.T) { + mintPerm := NewMintPermission() + burnPerm := NewBurnPermission() + modifyPerm := NewModifyPermission() + + require.Equal(t, mintPerm.String(), "mint") + require.Equal(t, burnPerm.String(), "burn") + require.Equal(t, modifyPerm.String(), "modify") +} + +func TestPermissionsString(t *testing.T) { + perms := Permissions{ + NewMintPermission(), + NewBurnPermission(), + NewModifyPermission(), + } + + require.Equal(t, `types.Permissions{"mint", "burn", "modify"}`, perms.String()) +} diff --git a/x/token/internal/types/querier.go b/x/token/internal/types/querier.go new file mode 100644 index 0000000000..889b15a0b1 --- /dev/null +++ b/x/token/internal/types/querier.go @@ -0,0 +1,60 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + QuerierRoute = "token" + QueryTokens = "tokens" + QueryPerms = "perms" + QueryBalance = "balance" + QuerySupply = "supply" + QueryMint = "mint" + QueryBurn = "burn" + QueryIsApproved = "approved" + QueryApprovers = "approvers" +) + +type NodeQuerier interface { + QueryWithData(path string, data []byte) ([]byte, int64, error) + WithHeight(height int64) context.CLIContext +} + +type QueryContractIDAccAddressParams struct { + Addr sdk.AccAddress `json:"addr"` +} + +func NewQueryContractIDAccAddressParams(addr sdk.AccAddress) QueryContractIDAccAddressParams { + return QueryContractIDAccAddressParams{Addr: addr} +} + +type QueryIsApprovedParams struct { + Proxy sdk.AccAddress `json:"proxy"` + Approver sdk.AccAddress `json:"approver"` +} + +func NewQueryIsApprovedParams(proxy sdk.AccAddress, approver sdk.AccAddress) QueryIsApprovedParams { + return QueryIsApprovedParams{ + Proxy: proxy, + Approver: approver, + } +} + +type QueryProxyParams struct { + Proxy sdk.AccAddress `json:"proxy"` +} + +func NewQueryApproverParams(proxy sdk.AccAddress) QueryProxyParams { + return QueryProxyParams{Proxy: proxy} +} + +func IsAddressContains(addresses []sdk.AccAddress, address sdk.AccAddress) bool { + for _, it := range addresses { + if address.Equals(it) { + return true + } + } + return false +} diff --git a/x/token/internal/types/supply.go b/x/token/internal/types/supply.go new file mode 100644 index 0000000000..df5df1b1c2 --- /dev/null +++ b/x/token/internal/types/supply.go @@ -0,0 +1,94 @@ +package types + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type Supply interface { + GetTotalSupply() sdk.Int + SetTotalSupply(total sdk.Int) Supply + GetTotalBurn() sdk.Int + GetTotalMint() sdk.Int + GetContractID() string + + Inflate(amount sdk.Int) Supply + Deflate(amount sdk.Int) Supply + + String() string +} + +type BaseSupply struct { + ContractID string `json:"contract_id"` + TotalSupply sdk.Int `json:"total_supply"` + TotalMint sdk.Int `json:"total_mint"` + TotalBurn sdk.Int `json:"total_burn"` +} + +func NewSupply(contractID string, total sdk.Int) Supply { + return BaseSupply{contractID, total, total, sdk.ZeroInt()} +} + +func DefaultSupply(contractID string) Supply { + return NewSupply(contractID, sdk.ZeroInt()) +} + +func (supply BaseSupply) SetTotalSupply(total sdk.Int) Supply { + supply.TotalSupply = total + supply.TotalMint = total + supply.TotalBurn = sdk.ZeroInt() + return supply +} + +func (supply BaseSupply) GetContractID() string { + return supply.ContractID +} + +func (supply BaseSupply) GetTotalSupply() sdk.Int { + return supply.TotalSupply +} + +func (supply BaseSupply) GetTotalMint() sdk.Int { + return supply.TotalMint +} + +func (supply BaseSupply) GetTotalBurn() sdk.Int { + return supply.TotalBurn +} + +func (supply BaseSupply) Inflate(amount sdk.Int) Supply { + supply.TotalSupply = supply.TotalSupply.Add(amount) + supply.TotalMint = supply.TotalMint.Add(amount) + supply.checkInvariant() + return supply +} + +func (supply BaseSupply) Deflate(amount sdk.Int) Supply { + supply.TotalSupply = supply.TotalSupply.Sub(amount) + supply.TotalBurn = supply.TotalBurn.Add(amount) + supply.checkInvariant() + return supply +} + +func (supply BaseSupply) String() string { + b, err := json.Marshal(supply) + if err != nil { + panic(err) + } + return string(b) +} + +// panic if totalSupply != totalMint - totalBurn +func (supply BaseSupply) checkInvariant() { + if !supply.TotalSupply.Equal(supply.TotalMint.Sub(supply.TotalBurn)) { + panic(fmt.Sprintf( + "Token [%v]'s total supply [%v] does not match with total mint [%v] - total burn [%v]", + supply.GetContractID(), + supply.TotalSupply, + supply.TotalMint, + supply.TotalBurn, + )) + } +} diff --git a/x/token/internal/types/supply_test.go b/x/token/internal/types/supply_test.go new file mode 100644 index 0000000000..ee6eb6ee80 --- /dev/null +++ b/x/token/internal/types/supply_test.go @@ -0,0 +1,51 @@ +package types + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestSupply(t *testing.T) { + var supply Supply + supply = DefaultSupply(defaultContractID) + + // create default + require.Equal(t, defaultContractID, supply.GetContractID()) + require.Equal(t, sdk.ZeroInt(), supply.GetTotalSupply()) + require.Equal(t, sdk.ZeroInt(), supply.GetTotalMint()) + require.Equal(t, sdk.ZeroInt(), supply.GetTotalBurn()) + + // set total supply + initialSupply := sdk.NewInt(3) + supply = supply.SetTotalSupply(initialSupply) + require.Equal(t, initialSupply, supply.GetTotalSupply()) + require.Equal(t, initialSupply, supply.GetTotalMint()) + require.Equal(t, sdk.ZeroInt(), supply.GetTotalBurn()) + + // inflate + toInflate := sdk.NewInt(2) + supply = supply.Inflate(toInflate) + require.Equal(t, initialSupply.Add(toInflate), supply.GetTotalSupply()) + require.Equal(t, initialSupply.Add(toInflate), supply.GetTotalMint()) + require.Equal(t, sdk.ZeroInt(), supply.GetTotalBurn()) + + // deflate + toDeflate := sdk.NewInt(4) + supply = supply.Deflate(toDeflate) + require.Equal(t, initialSupply.Add(toInflate).Sub(toDeflate), supply.GetTotalSupply()) + require.Equal(t, initialSupply.Add(toInflate), supply.GetTotalMint()) + require.Equal(t, toDeflate, supply.GetTotalBurn()) + + // total + expected := fmt.Sprintf( + `{"contract_id":"%s","total_supply":"%s","total_mint":"%s","total_burn":"%s"}`, + defaultContractID, + initialSupply.Add(toInflate).Sub(toDeflate), + initialSupply.Add(toInflate), + toDeflate, + ) + require.Equal(t, expected, supply.String()) +} diff --git a/x/token/internal/types/symbol.go b/x/token/internal/types/symbol.go new file mode 100644 index 0000000000..c5a6c5eba6 --- /dev/null +++ b/x/token/internal/types/symbol.go @@ -0,0 +1,24 @@ +package types + +import ( + "fmt" + "regexp" +) + +const ( + /* #nosec */ + reUserTokenSymbolString = `[A-Z][A-Z0-9]{1,4}` +) + +var ( + reUserTokenSymbol = regexp.MustCompile(fmt.Sprintf(`^%s$`, reUserTokenSymbolString)) +) + +func ValidateReg(symbol string, reg *regexp.Regexp) error { + if !reg.MatchString(symbol) { + return fmt.Errorf("symbol [%s] mismatched to [%s]", symbol, reg.String()) + } + return nil +} + +func ValidateTokenSymbol(symbol string) error { return ValidateReg(symbol, reUserTokenSymbol) } diff --git a/x/token/internal/types/token.go b/x/token/internal/types/token.go new file mode 100644 index 0000000000..ac05248e70 --- /dev/null +++ b/x/token/internal/types/token.go @@ -0,0 +1,80 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type Tokens []Token + +func (ts Tokens) String() string { + b, err := json.Marshal(ts) + if err != nil { + panic(err) + } + return string(b) +} + +type Token interface { + GetContractID() string + GetName() string + SetName(name string) + GetSymbol() string + GetMeta() string + SetMeta(meta string) + GetImageURI() string + SetImageURI(tokenURI string) + GetMintable() bool + GetDecimals() sdk.Int + String() string +} + +var _ Token = (*BaseToken)(nil) + +type BaseToken struct { + ContractID string `json:"contract_id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Meta string `json:"meta"` + ImageURI string `json:"img_uri"` + Decimals sdk.Int `json:"decimals"` + Mintable bool `json:"mintable"` +} + +func NewToken(contractID, name, symbol, meta string, imageURI string, decimals sdk.Int, mintable bool) Token { + return &BaseToken{ + ContractID: contractID, + Name: name, + Symbol: symbol, + Meta: meta, + ImageURI: imageURI, + Decimals: decimals, + Mintable: mintable, + } +} + +func (t BaseToken) GetContractID() string { return t.ContractID } +func (t BaseToken) GetName() string { return t.Name } +func (t BaseToken) GetSymbol() string { return t.Symbol } +func (t BaseToken) GetImageURI() string { return t.ImageURI } +func (t BaseToken) GetMintable() bool { return t.Mintable } +func (t BaseToken) GetDecimals() sdk.Int { return t.Decimals } +func (t *BaseToken) SetName(name string) { + t.Name = name +} +func (t BaseToken) GetMeta() string { return t.Meta } +func (t *BaseToken) SetMeta(meta string) { + t.Meta = meta +} +func (t *BaseToken) SetImageURI(tokenURI string) { + t.ImageURI = tokenURI +} + +func (t BaseToken) String() string { + b, err := json.Marshal(t) + if err != nil { + panic(err) + } + return string(b) +} diff --git a/x/token/internal/types/token_test.go b/x/token/internal/types/token_test.go new file mode 100644 index 0000000000..39fce64581 --- /dev/null +++ b/x/token/internal/types/token_test.go @@ -0,0 +1,56 @@ +package types + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalToken(t *testing.T) { + // Given a token + token := NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + var token2 Token + + // When marshal and unmarshal the token + bz, err := ModuleCdc.MarshalJSON(token) + require.NoError(t, err) + err = ModuleCdc.UnmarshalJSON(bz, &token2) + require.NoError(t, err) + + // Then the properties are same + r := require.New(t) + r.EqualValues(defaultName, token.GetName(), token2.GetName()) + r.Equal(defaultContractID, token.GetContractID(), token2.GetContractID()) + r.Equal(defaultImageURI, token.GetImageURI(), token2.GetImageURI()) + r.Equal(int64(defaultDecimals), token.GetDecimals().Int64(), token2.GetDecimals().Int64()) + r.Equal(true, token.GetMintable(), token2.GetMintable()) +} + +func TestSetToken(t *testing.T) { + // Given a token + token := NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + + // When change name and test uri, Then they are changed + token.SetName("new_name") + token.SetImageURI("new_token_uri") + token.SetMeta("new_meta") + require.Equal(t, "new_name", token.GetName()) + require.Equal(t, "new_token_uri", token.GetImageURI()) + require.Equal(t, "new_meta", token.GetMeta()) +} + +func TestBaseToken_String(t *testing.T) { + token := NewToken(defaultContractID, defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true) + + require.Equal(t, `{"contract_id":"linktkn","name":"name","symbol":"BTC","meta":"{}","img_uri":"image-uri","decimals":"6","mintable":true}`, token.String()) +} + +func TestTokensString(t *testing.T) { + tokens := Tokens{ + NewToken(defaultContractID+"1", defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true), + NewToken(defaultContractID+"2", defaultName, defaultSymbol, defaultMeta, defaultImageURI, sdk.NewInt(defaultDecimals), true), + } + + require.Equal(t, `[{"contract_id":"linktkn1","name":"name","symbol":"BTC","meta":"{}","img_uri":"image-uri","decimals":"6","mintable":true},{"contract_id":"linktkn2","name":"name","symbol":"BTC","meta":"{}","img_uri":"image-uri","decimals":"6","mintable":true}]`, tokens.String()) +} diff --git a/x/token/internal/types/validators.go b/x/token/internal/types/validators.go new file mode 100644 index 0000000000..e3d9194453 --- /dev/null +++ b/x/token/internal/types/validators.go @@ -0,0 +1,98 @@ +package types + +import ( + "unicode/utf8" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + MaxImageURILength = 1000 + MaxTokenNameLength = 20 + MaxTokenMetaLength = 1000 + MaxChangeFieldsCount = 100 +) + +var ( + TokenModifiableFields = ModifiableFields{ + AttributeKeyName: true, + AttributeKeyMeta: true, + AttributeKeyImageURI: true, + } +) + +type ModifiableFields map[string]bool + +func ValidateName(name string) bool { + return utf8.RuneCountInString(name) <= MaxTokenNameLength +} + +func ValidateMeta(meta string) bool { + return utf8.RuneCountInString(meta) <= MaxTokenMetaLength +} + +func ValidateImageURI(imageURI string) bool { + return utf8.RuneCountInString(imageURI) <= MaxImageURILength +} + +type ChangesValidator struct { + modifiableFields ModifiableFields + handlers map[string]func(value string) error +} + +func NewChangesValidator() *ChangesValidator { + hs := make(map[string]func(value string) error) + hs[AttributeKeyName] = func(value string) error { + if !ValidateName(value) { + return sdkerrors.Wrapf(ErrInvalidNameLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", value, MaxTokenNameLength, utf8.RuneCountInString(value)) + } + return nil + } + hs[AttributeKeyImageURI] = func(value string) error { + if !ValidateImageURI(value) { + return sdkerrors.Wrapf(ErrInvalidImageURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", value, MaxImageURILength, utf8.RuneCountInString(value)) + } + return nil + } + hs[AttributeKeyMeta] = func(value string) error { + if !ValidateMeta(value) { + return sdkerrors.Wrapf(ErrInvalidMetaLength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", value, MaxTokenMetaLength, utf8.RuneCountInString(value)) + } + return nil + } + return &ChangesValidator{ + modifiableFields: TokenModifiableFields, + handlers: hs, + } +} + +func (c *ChangesValidator) Validate(changes Changes) error { + if len(changes) == 0 { + return ErrEmptyChanges + } + + if len(changes) > MaxChangeFieldsCount { + return sdkerrors.Wrapf(ErrInvalidChangesFieldCount, "You can not change fields more than [%d] at once, current count: [%d]", MaxChangeFieldsCount, len(changes)) + } + + checkedFields := map[string]bool{} + for _, change := range changes { + if !c.modifiableFields[change.Field] { + return sdkerrors.Wrapf(ErrInvalidChangesField, "Field: %s", change.Field) + } + if checkedFields[change.Field] { + return sdkerrors.Wrapf(ErrDuplicateChangesField, "Field: %s", change.Field) + } + + validateHandler, ok := c.handlers[change.Field] + if !ok { + return sdkerrors.Wrapf(ErrInvalidChangesField, "Field: %s", change.Field) + } + + if err := validateHandler(change.Value); err != nil { + return err + } + checkedFields[change.Field] = true + } + return nil +} diff --git a/x/token/internal/types/validators_test.go b/x/token/internal/types/validators_test.go new file mode 100644 index 0000000000..c7c672ce38 --- /dev/null +++ b/x/token/internal/types/validators_test.go @@ -0,0 +1,98 @@ +package types + +import ( + "strings" + "testing" + "unicode/utf8" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" +) + +var length1001String = strings.Repeat("Eng글자日本語はスゲ", 91) // 11 * 91 = 1001 + +func TestValidateName(t *testing.T) { + t.Log("Given valid name") + { + var length20String = strings.Repeat("Eng글자日本語はス", 2) // 10 * 2 = 20 + require.True(t, ValidateName(length20String)) + } + t.Log("Given invalid name") + { + var length21String = strings.Repeat("Eng글자日本", 3) // 7 * 3 = 21 + require.False(t, ValidateName(length21String)) + } +} + +func TestValidateTokenURI(t *testing.T) { + t.Log("Given valid base_img_uri") + { + var length990String = strings.Repeat("Eng글자日本語はスゲ", 90) // 11 * 90 = 990 + require.True(t, ValidateImageURI(length990String)) + } + t.Log("Given invalid token_uri") + { + require.False(t, ValidateImageURI(length1001String)) + } +} + +func TestValidateChanges(t *testing.T) { + validator := NewChangesValidator() + t.Log("Test with valid changes") + { + changes := NewChangesWithMap(map[string]string{ + "name": "new_name", + "img_uri": "new_img_uri", + }) + + require.Nil(t, validator.Validate(changes)) + } + t.Log("Test with empty changes") + { + changes := Changes{} + require.EqualError(t, validator.Validate(changes), ErrEmptyChanges.Error()) + } + t.Log("Test with img_uri too long") + { + length1001String := strings.Repeat("Eng글자日本語はスゲ", 91) // 11 * 91 = 1001 + changes := NewChangesWithMap(map[string]string{ + "name": "new_name", + "img_uri": length1001String, + }) + + require.EqualError( + t, + validator.Validate(changes), + sdkerrors.Wrapf(ErrInvalidImageURILength, "[%s] should be shorter than [%d] UTF-8 characters, current length: [%d]", length1001String, MaxImageURILength, utf8.RuneCountInString(length1001String)).Error(), + ) + } + t.Log("Test with invalid changes field") + { + // Given changes with invalid fields + changes := NewChanges( + NewChange("invalid_field", "value"), + ) + // Then error is occurred + require.EqualError(t, validator.Validate(changes), sdkerrors.Wrap(ErrInvalidChangesField, "Field: invalid_field").Error()) + } + t.Log("Test with changes more than max") + { + // Given changes more than max + changeList := make([]Change, MaxChangeFieldsCount+1) + changes := Changes(changeList) + + // Then error is occurred + require.EqualError(t, validator.Validate(changes), sdkerrors.Wrapf(ErrInvalidChangesFieldCount, "You can not change fields more than [%d] at once, current count: [%d]", MaxChangeFieldsCount, len(changeList)).Error()) + } + t.Log("Test with duplicate fields") + { + // Given changes with duplicate fields + changes := NewChanges( + NewChange("name", "value"), + NewChange("name", "value2"), + ) + + // Then error is occurred + require.EqualError(t, validator.Validate(changes), sdkerrors.Wrapf(ErrDuplicateChangesField, "Field: name").Error()) + } +} diff --git a/x/token/module.go b/x/token/module.go new file mode 100644 index 0000000000..696263f3ac --- /dev/null +++ b/x/token/module.go @@ -0,0 +1,129 @@ +package token + +import ( + "encoding/json" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/x/upgrade" + "github.com/line/lbm-sdk/v2/x/token/client/cli" + "github.com/line/lbm-sdk/v2/x/token/client/rest" + "github.com/line/lbm-sdk/v2/x/token/internal/handler" + "github.com/line/lbm-sdk/v2/x/token/internal/keeper" + "github.com/line/lbm-sdk/v2/x/token/internal/legacy" + "github.com/line/lbm-sdk/v2/x/token/internal/querier" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// app module basics object +type AppModuleBasic struct{} + +// module name +func (AppModuleBasic) Name() string { return ModuleName } + +// register module codec +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { RegisterCodec(cdc) } + +// default genesis state +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return ModuleCdc.MustMarshalJSON(DefaultGenesisState()) +} + +// module validate genesis +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var data GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &data) + if err != nil { + return err + } + return ValidateGenesis(data) +} + +// register rest routes +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { + rest.RegisterRoutes(ctx, rtr) +} + +// get the root tx command of this module +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetTxCmd(cdc) +} + +// get the root query command of this module +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetQueryCmd(cdc) +} + +func (AppModuleBasic) GetUpgradeHandler(version string) upgrade.UpgradeHandler { + return legacy.UpgradeHandler(version) +} + +// ___________________________ +// app module +type AppModule struct { + AppModuleBasic + keeper keeper.Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper keeper.Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// module name +func (AppModule) Name() string { return ModuleName } + +// register invariants +// TODO: should this module need invariants? +func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} + +// module message route name +func (AppModule) Route() string { return RouterKey } + +// module handler +func (am AppModule) NewHandler() sdk.Handler { return handler.NewHandler(am.keeper) } + +// module querier route name +func (AppModule) QuerierRoute() string { return RouterKey } + +// module querier +func (am AppModule) NewQuerierHandler() sdk.Querier { + return querier.NewQuerier(am.keeper) +} + +// module init-genesis +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + ModuleCdc.MustUnmarshalJSON(data, &genesisState) + InitGenesis(ctx, am.keeper, genesisState) + return []abci.ValidatorUpdate{} +} + +// module export genesis +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return ModuleCdc.MustMarshalJSON(gs) +} + +// module begin-block +func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// module end-block +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/token/spec/01_concept.md b/x/token/spec/01_concept.md new file mode 100644 index 0000000000..a525a86adb --- /dev/null +++ b/x/token/spec/01_concept.md @@ -0,0 +1,2 @@ +# Concept +TBD \ No newline at end of file diff --git a/x/token/spec/02_keepers.md b/x/token/spec/02_keepers.md new file mode 100644 index 0000000000..bddc31f1af --- /dev/null +++ b/x/token/spec/02_keepers.md @@ -0,0 +1,2 @@ +# Keepers +TBD diff --git a/x/token/spec/03_messages.md b/x/token/spec/03_messages.md new file mode 100644 index 0000000000..e6e1450ba6 --- /dev/null +++ b/x/token/spec/03_messages.md @@ -0,0 +1,180 @@ +# Messages +## MsgIssue + +**Issue token messages are to create a new token on Link Chain** +- See [symbol rule](01_concept.md#rule-for-defining-symbols) for the details +- The first issuer for the token symbol occupies the symbol and the issue permission is granted to the issuer +- An issuer who granted issue permission can issue collective tokens +- Mint permission is granted to the token issuer when the token is mintable +- The identifier for the collective token is defined by the concatenation of the symbol and the token id + +### MsgIssue +```golang +type MsgIssue struct { + Owner sdk.AccAddress `json:"owner"` + To sdk.AccAddress `json:"to"` + Name string `json:"name"` + Meta string `json:"meta"` + Symbol string `json:"symbol"` + ImageURI string `json:"img_uri"` + Amount sdk.Int `json:"amount"` + Mintable bool `json:"mintable"` + Decimals sdk.Int `json:"decimals"` +} +``` +## Mint + +**Mint message is to increase the total supply of the token** +- Signer(From) of this message must have permission +- Minted token is added to the `To` account + +### MsgMint + +```golang +type MsgMint struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Amount sdk.Int `json:"amount"` +} +``` + +## Burn +**Burn message is to decrease the total supply of the token** +- Signer(From) of this message must have the amount of the tokens +- Token is subtracted from the `From` account + +### MsgBurn + +```golang +type MsgBurn struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + Amount sdk.Int `json:"amount"` +} +``` + +### MsgBurnFrom + +```golang +type MsgBurnFrom struct { + Proxy sdk.AccAddress `json:"proxy"` + ContractID string `json:"contract_id"` + From sdk.AccAddress `json:"from"` + Amount sdk.Int `json:"amount"` +} +``` + + +## MsgGrantPermission + +```golang +type MsgGrantPermission struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Permission Permission `json:"permission"` +} +``` + +**Grant Permission is to give a permission to the `To` account** +- `From` account must has the permission + +## MsgRevokePermission + +```golang +type MsgRevokePermission struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + Permission Permission `json:"permission"` +} +``` + +**Revoke Permission is to dump a permission from the `From` account** +- `From` account must has the permission + + +## MsgTransfer + +```golang +type MsgTransfer struct { + From sdk.AccAddress `json:"from"` + ContractID string `json:"contract_id"` + To sdk.AccAddress `json:"to"` + Amount sdk.Int `json:"amount"` +} +``` + +**Transfer message is to transfer a non-reserved fungible token** +- Signer of this message must have the amount of the tokens +- Token is subtracted from the `From` account +- Token is added to the `To` account + +## MsgModify + +```golang +type MsgModify struct { + Owner sdk.AccAddress `json:"owner"` + ContractID string `json:"contract_id"` + Changes linktype.Changes `json:"changes"` +} +``` + +**Modify message is to modify fields of token** +- `Owner` is the signer + +## MsgApprove + +```golang +type MsgApprove struct { + Approver sdk.AccAddress `json:"approver"` + ContractID string `json:"contract_id"` + Proxy sdk.AccAddress `json:"proxy"` +} +``` + +**Approve message is to approve a proxy to transfer and burn token** +- `Approver` is the signer + + + +# Syntax +| Message/Attributes | Tag | Type | +| ---- | ---- | ---- | +| Message | token/MsgIssue | github.com/line/link/x/token/internal/types.MsgIssue | + | Attributes | owner | []uint8 | + | Attributes | to | []uint8 | + | Attributes | name | string | + | Attributes | symbol | string | + | Attributes | img_uri | string | + | Attributes | meta | string | + | Attributes | amount | github.com/cosmos/cosmos-sdk/types.Int | + | Attributes | mintable | bool | + | Attributes | decimals | github.com/cosmos/cosmos-sdk/types.Int | +| Message | token/MsgModify | github.com/line/link/x/token/internal/types.MsgModify | + | Attributes | owner | []uint8 | + | Attributes | contract_id | string | + | Attributes | changes | []github.com/line/link/types.Change | +| Message | token/MsgMint | github.com/line/link/x/token/internal/types.MsgMint | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | to | []uint8 | + | Attributes | amount | github.com/cosmos/cosmos-sdk/types.Int | +| Message | token/MsgBurn | github.com/line/link/x/token/internal/types.MsgBurn | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | amount | github.com/cosmos/cosmos-sdk/types.Int | +| Message | token/MsgGrantPermission | github.com/line/link/x/token/internal/types.MsgGrantPermission | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | to | []uint8 | + | Attributes | permission | github.com/line/link/x/token/internal/types.Permission | +| Message | token/MsgRevokePermission | github.com/line/link/x/token/internal/types.MsgRevokePermission | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | permission | github.com/line/link/x/token/internal/types.Permission | +| Message | token/MsgTransfer | github.com/line/link/x/token/internal/types.MsgTransfer | + | Attributes | from | []uint8 | + | Attributes | contract_id | string | + | Attributes | to | []uint8 | + | Attributes | amount | github.com/cosmos/cosmos-sdk/types.Int | diff --git a/x/token/spec/04_events.md b/x/token/spec/04_events.md new file mode 100644 index 0000000000..216badf210 --- /dev/null +++ b/x/token/spec/04_events.md @@ -0,0 +1,121 @@ +# Events +**Not fully documented yet** +The token module emits the following events: + + +### MsgIssue +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | token | +| message | sender | {ownerAddress} | +| message | action | issue_token | +| grant_perm | to | {ownerAddress} | +| grant_perm | contract_id | {contractID} | +| grant_perm | perm | mint | +| grant_perm | to | {ownerAddress} | +| grant_perm | contract_id | {contractID} | +| grant_perm | perm | modify | +| issue | contract_id | {contractID} | +| issue | name | {name} | +| issue | symbol | {symbol} | +| issue | owner | {ownerAddress} | +| issue | to | {toAddress} | +| issue | amount | {amount} | +| issue | mintable | {mintable} | +| issue | decimals | {decimals} | + +### MsgMint +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | token | +| message | sender | {ownerAddress} | +| message | action | mint | +| mint | contract_id | {contractID} | +| mint | amount | {amount} | +| mint | from | {ownerAddress} | +| mint | to | {toAddress} | + +### MsgBurn +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | token | +| message | sender | {ownerAddress} | +| message | action | burn | +| burn | contract_id | {contractID} | +| burn | amount | {amount} | +| burn | from | {ownerAddress} | + +### MsgBurnFrom +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | token | +| message | sender | {proxyAddress} | +| message | action | burn | +| burn_from | contract_id | {contractID} | +| burn_from | proxy | {proxyAddress} | +| burn_from | from | {fromAddress} | +| burn_from | amount | {amount} | + +### MsgBurnFrom +| Type | Attribute Key | Attribute Value | +|------------------|----------------|------------------------------| +| message | module | token | +| message | sender | {proxyAddress} | +| message | action | burn_ft | +| burn_ft_from | contract_id | {contractID} | +| burn_ft_from | proxy | {proxyAddress} | +| burn_ft_from | from | {fromAddress} | +| burn_ft_from | amount | {amount}{contractID}{tokenID}| + + +### MsgTransfer +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | token | +| message | sender | {fromAddress} | +| message | action | transfer_ft | +| transfer_ft | contract_id | {contractID} | +| transfer_ft | from | {fromAddress} | +| transfer_ft | to | {toAddress} | +| transfer_ft | amount | {amount} | + +### MsgModify +| Type | Attribute Key | Attribute Value | +|-----------------------|----------------|-----------------------| +| message | module | token | +| message | sender | {ownerAddress} | +| message | action | modify_token | +| modify_token | contract_id | {contract_id} | +| modify_token | {modifiedField}| {modifiedValue} | + +### MsgGrantPermission +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | token | +| message | sender | {fromAddress} | +| message | action | grant_permission | +| grant_perm | from | {fromAddress} | +| grant_perm | to | {toAddress} | +| grant_perm | contract_id | {resource} | +| grant_perm | perm | issue/mint/burn/modify | + +### MsgRevokePermission +| Type | Attribute Key | Attribute Value | +|------------------|----------------|--------------------------| +| message | module | token | +| message | sender | {fromAddress} | +| message | action | revoke_permission | +| revoke_perm | from | {fromAddress} | +| revoke_perm | contract_id | {resource} | +| revoke_perm | perm | issue/mint/burn/modify | + +### MsgApprove +| Type | Attribute Key | Attribute Value | +|---------------|----------------|-------------------| +| message | module | token | +| message | sender | {approverAddress} | +| message | action | approve_token | +| approve_token | contract_id | {contractID} | +| approve_token | approver | {approverAddress} | +| approve_token | proxy | {proxyAddress} | + diff --git a/x/token/spec/README.md b/x/token/spec/README.md new file mode 100644 index 0000000000..e0c31c380d --- /dev/null +++ b/x/token/spec/README.md @@ -0,0 +1,14 @@ +# Token module specification + + + +## Abstract + +This document specifies the token module of the Link Network. + +## Contents + +1. **[Concept](01_concept.md)** +2. **[Keepers](02_keepers.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** diff --git a/x/wasm/CHANGELOG.md b/x/wasm/CHANGELOG.md new file mode 100644 index 0000000000..16425b347e --- /dev/null +++ b/x/wasm/CHANGELOG.md @@ -0,0 +1,27 @@ +# CHANGELOG + +## Unreleased Version +### Added +- Fork CosmWasm/wasmd/x/wasm (v0.10.0) to add wasm module (#1) +- Implement the token module encoder (#5) +- Implement a querier to get approvers of a token (#7) +- Add linkwasmd and cli tests (#20) +- Implement the collection module encoder (#22) +- Add cli tests for token (#42) +- Add performance reporting feature in CI (#43) +- Add cli tests for collection (#50) +- Add tests for managing max contract size (#51) + +### Changed +- Change used marshal/unmarshal from json.(Un)Marshal -> codec.(Un)Marshal (#37) +- Change the max size of contract managed with the parameter module (#44) +- Update linkwasmd to follow CosmWasm's wasmd v0.11.1 (#65) +- Rewrite Governonce.md (#73, #75) +- Rename QueryXxxParam to XxxParam (#81) +- Change Query total interface (#81) + +### Fixed +- Fix linkwasmd's wasmKeeper for #5 (#32) +- Solve a TODO in wasm's cli test (#53) +- Fix CI error on develop branch (#71) +- Fix init params first in InitGenesis (cherry-pick CosmWasm/wasmd@ae169ce) (#76) diff --git a/x/wasm/Governance.md b/x/wasm/Governance.md new file mode 100644 index 0000000000..55b768f07e --- /dev/null +++ b/x/wasm/Governance.md @@ -0,0 +1,103 @@ +# 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 5 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 +* `UpdateAdminProposal` - set a new admin for a contract +* `ClearAdminProposal` - clear admin for a contract to prevent further migrations + +For details, see the proposal type [implementation](internal/types/proposal.go) + +A wasm message but no proposal type: +* `ExecuteContract` - execute a command on a wasm contract + +And you can use `Parameter Change Proposal` to change wasm parameters. +These parameters are as following. + +* `UploadAccess` - who can upload wasm codes +* `DefaultInstantiatePermission` - who can instantiate contracts from a code in default +* `MaxWasmCodeSize` - max size of wasm code to be uploaded + +### Unit tests +[Proposal type validations](internal/types/proposal_test.go) + +## Proposal Handler +The [wasm proposal_handler](internal/keeper/proposal_handler.go) implements the `gov.Handler` function +and executes the wasm proposal types after a successful tally. + +The proposal handler uses a [`GovAuthorizationPolicy`](internal/keeper/authz_policy.go#L29) to bypass the existing contract's authorization policy. + +### Tests +* [Integration: Submit and execute proposal](internal/keeper/proposal_integration_test.go) + +## Gov Integration +The wasm proposal handler can be added to the gov router in the [abci app](linkwasmd/app/app.go#L240) +to receive proposal execution calls. +```go +govRouter.AddRoute(wasm.RouterKey, wasm.NewWasmProposalHandler(app.wasmKeeper, wasm.EnableAllProposals)) +``` + +## Wasm 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](internal/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. + +### Enable gov proposals +Gov proposals authorization policy needs to be specified with `enabledProposalTypes` which is an argument of NewWasmProposalHandler in [proposal_handler.go](internal/keeper/proposal_handler.go) + +### Tests +* [params validation unit tests](internal/types/params_test.go) +* [genesis validation tests](internal/types/genesis_test.go) +* [policy integration tests](internal/keeper/keeper_test.go) + +## CLI + +```shell script + wasmcli 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`](client/proposal_handler.go) + +* Integration +```shell script +gov.NewAppModuleBasic(append(wasmclient.ProposalHandlers, paramsclient.ProposalHandler,)...), +``` +In [abci app](linkwasmd/app/app.go) + +### Tests +* [Rest Unit tests](client/proposal_handler_test.go) +* [CLI tests](linkwasmd/cli_test/cli_test.go) diff --git a/x/wasm/README.md b/x/wasm/README.md new file mode 100644 index 0000000000..eedea71be2 --- /dev/null +++ b/x/wasm/README.md @@ -0,0 +1,213 @@ +# Wasm Module + +This should be a brief overview of the functionality + +## Configuration + +You can add the following section to `config/app.toml`. Below is shown with defaults: + +```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 is the number of wasm vm instances we keep cached in memory for speed-up +# Warning: this is currently unstable and may lead to crashes, best to keep for 0 unless testing locally +lru_size = 0 +``` + +## 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": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5" + } + ] +} +``` + +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": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5" + }, + { + "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": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5" + }, + { + "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": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5" + }, + { + "key": "sender", + "value": "cosmos1zm074khx32hqy20hlshlsd423n07pwlu9cpt37" + }, + { + "key": "amount", + "value": "5000denom" + } + ] + }, + { + "Type": "wasm", + "Attr": [ + { + "key": "contract_address", + "value": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5" + }, + { + "key": "action", + "value": "release" + }, + { + "key": "destination", + "value": "cosmos14k7v7ms4jxkk2etmg9gljxjm4ru3qjdugfsflq" + } + ] + }, + { + "Type": "transfer", + "Attr": [ + { + "key": "recipient", + "value": "cosmos14k7v7ms4jxkk2etmg9gljxjm4ru3qjdugfsflq" + }, + { + "key": "sender", + "value": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5" + }, + { + "key": "amount", + "value": "105000denom" + } + ] + }, + { + "Type": "message", + "Attr": [ + { + "key": "module", + "value": "wasm" + }, + { + "key": "action", + "value": "execute" + }, + { + "key": "signer", + "value": "cosmos1zm074khx32hqy20hlshlsd423n07pwlu9cpt37" + }, + { + "key": "contract_address", + "value": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5" + } + ] + } +] +``` + +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 diff --git a/x/wasm/alias.go b/x/wasm/alias.go new file mode 100644 index 0000000000..3556077f31 --- /dev/null +++ b/x/wasm/alias.go @@ -0,0 +1,134 @@ +// nolint +// autogenerated code using github.com/rigelrozanski/multitool +// aliases generated for the following subdirectories: +// ALIASGEN: github.com/cosmwasm/wasmd/x/wasm/internal/types +// ALIASGEN: github.com/cosmwasm/wasmd/x/wasm/internal/keeper +package wasm + +import ( + "github.com/line/lbm-sdk/v2/x/wasm/internal/keeper" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +const ( + firstCodeID = 1 + DefaultParamspace = types.DefaultParamspace + ModuleName = types.ModuleName + StoreKey = types.StoreKey + TStoreKey = types.TStoreKey + QuerierRoute = types.QuerierRoute + RouterKey = types.RouterKey + MaxWasmSize = types.MaxWasmSize + MaxLabelSize = types.MaxLabelSize + BuildTagRegexp = types.BuildTagRegexp + MaxBuildTagSize = types.MaxBuildTagSize + CustomEventType = types.CustomEventType + AttributeKeyContractAddr = types.AttributeKeyContractAddr + ProposalTypeStoreCode = types.ProposalTypeStoreCode + ProposalTypeInstantiateContract = types.ProposalTypeInstantiateContract + ProposalTypeMigrateContract = types.ProposalTypeMigrateContract + ProposalTypeUpdateAdmin = types.ProposalTypeUpdateAdmin + ProposalTypeClearAdmin = types.ProposalTypeClearAdmin + GasMultiplier = keeper.GasMultiplier + MaxGas = keeper.MaxGas + 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.RegisterCodec + ValidateGenesis = types.ValidateGenesis + ConvertToProposals = types.ConvertToProposals + GetCodeKey = types.GetCodeKey + GetContractAddressKey = types.GetContractAddressKey + GetContractStorePrefixKey = types.GetContractStorePrefixKey + NewCodeInfo = types.NewCodeInfo + NewAbsoluteTxPosition = types.NewAbsoluteTxPosition + NewContractInfo = types.NewContractInfo + NewEnv = types.NewEnv + NewWasmCoins = types.NewWasmCoins + ParseEvents = types.ParseEvents + DefaultWasmConfig = types.DefaultWasmConfig + DefaultParams = types.DefaultParams + InitGenesis = keeper.InitGenesis + ExportGenesis = keeper.ExportGenesis + NewMessageHandler = keeper.NewMessageHandler + DefaultEncoders = keeper.DefaultEncoders + EncodeBankMsg = keeper.EncodeBankMsg + CustomMsg = keeper.CustomMsg + EncodeStakingMsg = keeper.EncodeStakingMsg + EncodeWasmMsg = keeper.EncodeWasmMsg + NewKeeper = keeper.NewKeeper + NewQuerier = keeper.NewQuerier + DefaultQueryPlugins = keeper.DefaultQueryPlugins + BankQuerier = keeper.BankQuerier + StakingQuerier = keeper.StakingQuerier + WasmQuerier = keeper.WasmQuerier + MakeTestCodec = keeper.MakeTestCodec + CreateTestInput = keeper.CreateTestInput + TestHandler = keeper.TestHandler + CustomQuerier = keeper.CustomQuerier + NewWasmProposalHandler = keeper.NewWasmProposalHandler + NewRouter = types.NewRouter + NewQuerierRouter = types.NewQuerierRouter + + // 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 + MsgInstantiateContract = types.MsgInstantiateContract + MsgExecuteContract = types.MsgExecuteContract + MsgMigrateContract = types.MsgMigrateContract + MsgUpdateAdmin = types.MsgUpdateAdmin + MsgClearAdmin = types.MsgClearAdmin + Model = types.Model + CodeInfo = types.CodeInfo + ContractInfo = types.ContractInfo + CreatedAt = types.AbsoluteTxPosition + Config = types.WasmConfig + MessageHandler = keeper.MessageHandler + BankEncoder = keeper.BankEncoder + CustomEncoder = keeper.CustomEncoder + StakingEncoder = keeper.StakingEncoder + WasmEncoder = keeper.WasmEncoder + MessageEncoders = keeper.MessageEncoders + Keeper = keeper.Keeper + CodeInfoResponse = types.CodeInfoResponse + ContractInfoResponse = types.ContractInfoResponse + ContractHistoryResponse = types.ContractHistoryResponse + QueryHandler = keeper.QueryHandler + QueryPlugins = keeper.QueryPlugins + + EncodeHandler = types.EncodeHandler + EncodeQuerier = types.EncodeQuerier +) diff --git a/x/wasm/client/cli/gov_tx.go b/x/wasm/client/cli/gov_tx.go new file mode 100644 index 0000000000..4c2ffb4181 --- /dev/null +++ b/x/wasm/client/cli/gov_tx.go @@ -0,0 +1,297 @@ +package cli + +import ( + "bufio" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +func ProposalStoreCodeCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "wasm-store [wasm file] --source [source] --builder [builder] --title [text] --description [text] --run-as [address]", + Short: "Submit a wasm binary proposal", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + src, err := parseStoreCodeArgs(args, cliCtx) + if err != nil { + return err + } + if len(viper.GetString(flagRunAs)) == 0 { + return errors.New("run-as address is required") + } + runAsAddr, err := sdk.AccAddressFromBech32(viper.GetString(flagRunAs)) + if err != nil { + return errors.Wrap(err, "run-as") + } + content := types.StoreCodeProposal{ + WasmProposal: types.WasmProposal{ + Title: viper.GetString(cli.FlagTitle), + Description: viper.GetString(cli.FlagDescription), + }, + RunAs: runAsAddr, + WASMByteCode: src.WASMByteCode, + Source: src.Source, + Builder: src.Builder, + InstantiatePermission: src.InstantiatePermission, + } + + deposit, err := sdk.ParseCoins(viper.GetString(cli.FlagDeposit)) + if err != nil { + return err + } + + msg := govtypes.NewMsgSubmitProposal(content, deposit, cliCtx.GetFromAddress()) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd.Flags().String(flagSource, "", "A valid URI reference to the contract's source code, optional") + cmd.Flags().String(flagBuilder, "", "A valid docker tag for the build system, optional") + cmd.Flags().String(flagRunAs, "", "The address that is stored as code creator") + cmd.Flags().String(flagInstantiateByEverybody, "", "Everybody can instantiate a contract from the code, optional") + cmd.Flags().String(flagInstantiateByAddress, "", "Only this address can instantiate a contract instance from the code, optional") + + // 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") + cmd.Flags().String(cli.FlagProposal, "", "Proposal file path (if this path is given, other proposal flags are ignored)") + // type values must match the "ProposalHandler" "routes" in cli + cmd.Flags().String(flagProposalType, "", "Type of proposal, types: store-code/instantiate/migrate/update-admin/clear-admin/text/parameter_change/software_upgrade") + return cmd +} + +func ProposalInstantiateContractCmd(cdc *codec.Codec) *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 { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + src, err := parseInstantiateArgs(args, cliCtx) + if err != nil { + return err + } + if len(viper.GetString(flagRunAs)) == 0 { + return errors.New("creator address is required") + } + creator, err := sdk.AccAddressFromBech32(viper.GetString(flagRunAs)) + if err != nil { + return errors.Wrap(err, "creator") + } + content := types.InstantiateContractProposal{ + WasmProposal: types.WasmProposal{ + Title: viper.GetString(cli.FlagTitle), + Description: viper.GetString(cli.FlagDescription), + }, + RunAs: creator, + Admin: src.Admin, + CodeID: src.CodeID, + Label: src.Label, + InitMsg: src.InitMsg, + InitFunds: src.InitFunds, + } + + deposit, err := sdk.ParseCoins(viper.GetString(cli.FlagDeposit)) + if err != nil { + return err + } + + msg := govtypes.NewMsgSubmitProposal(content, deposit, cliCtx.GetFromAddress()) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + 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") + + // 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") + cmd.Flags().String(cli.FlagProposal, "", "Proposal file path (if this path is given, other proposal flags are ignored)") + // type values must match the "ProposalHandler" "routes" in cli + cmd.Flags().String(flagProposalType, "", "Type of proposal, types: store-code/instantiate/migrate/update-admin/clear-admin/text/parameter_change/software_upgrade") + return cmd +} + +func ProposalMigrateContractCmd(cdc *codec.Codec) *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 { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + src, err := parseMigrateContractArgs(args, cliCtx) + if err != nil { + return err + } + + if len(viper.GetString(flagRunAs)) == 0 { + return errors.New("run-as address is required") + } + runAs, err := sdk.AccAddressFromBech32(viper.GetString(flagRunAs)) + if err != nil { + return errors.Wrap(err, "run-as") + } + + content := types.MigrateContractProposal{ + WasmProposal: types.WasmProposal{ + Title: viper.GetString(cli.FlagTitle), + Description: viper.GetString(cli.FlagDescription), + }, + Contract: src.Contract, + CodeID: src.CodeID, + MigrateMsg: src.MigrateMsg, + RunAs: runAs, + } + + deposit, err := sdk.ParseCoins(viper.GetString(cli.FlagDeposit)) + if err != nil { + return err + } + + msg := govtypes.NewMsgSubmitProposal(content, deposit, cliCtx.GetFromAddress()) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().String(flagRunAs, "", "The address that is passed as sender to the contract on proposal execution") + + // 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") + cmd.Flags().String(cli.FlagProposal, "", "Proposal file path (if this path is given, other proposal flags are ignored)") + // type values must match the "ProposalHandler" "routes" in cli + cmd.Flags().String(flagProposalType, "", "Type of proposal, types: store-code/instantiate/migrate/update-admin/clear-admin/text/parameter_change/software_upgrade") + return cmd +} + +func ProposalUpdateContractAdminCmd(cdc *codec.Codec) *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 { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + src, err := parseUpdateContractAdminArgs(args, cliCtx) + if err != nil { + return err + } + + content := types.UpdateAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: viper.GetString(cli.FlagTitle), + Description: viper.GetString(cli.FlagDescription), + }, + Contract: src.Contract, + NewAdmin: src.NewAdmin, + } + + deposit, err := sdk.ParseCoins(viper.GetString(cli.FlagDeposit)) + if err != nil { + return err + } + + msg := govtypes.NewMsgSubmitProposal(content, deposit, cliCtx.GetFromAddress()) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + // 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") + cmd.Flags().String(cli.FlagProposal, "", "Proposal file path (if this path is given, other proposal flags are ignored)") + // type values must match the "ProposalHandler" "routes" in cli + cmd.Flags().String(flagProposalType, "", "Type of proposal, types: store-code/instantiate/migrate/update-admin/clear-admin/text/parameter_change/software_upgrade") + return cmd +} + +func ProposalClearContractAdminCmd(cdc *codec.Codec) *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 { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + contractAddr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return sdkerrors.Wrap(err, "contract") + } + + content := types.ClearAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: viper.GetString(cli.FlagTitle), + Description: viper.GetString(cli.FlagDescription), + }, + Contract: contractAddr, + } + + deposit, err := sdk.ParseCoins(viper.GetString(cli.FlagDeposit)) + if err != nil { + return err + } + + msg := govtypes.NewMsgSubmitProposal(content, deposit, cliCtx.GetFromAddress()) + if err = msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + // 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") + cmd.Flags().String(cli.FlagProposal, "", "Proposal file path (if this path is given, other proposal flags are ignored)") + // type values must match the "ProposalHandler" "routes" in cli + cmd.Flags().String(flagProposalType, "", "Type of proposal, types: store-code/instantiate/migrate/update-admin/clear-admin/text/parameter_change/software_upgrade") + return cmd +} diff --git a/x/wasm/client/cli/new_tx.go b/x/wasm/client/cli/new_tx.go new file mode 100644 index 0000000000..77280c71dc --- /dev/null +++ b/x/wasm/client/cli/new_tx.go @@ -0,0 +1,135 @@ +package cli + +import ( + "bufio" + "strconv" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +// MigrateContractCmd will migrate a contract to a new code version +func MigrateContractCmd(cdc *codec.Codec) *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", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + msg, err := parseMigrateContractArgs(args, cliCtx) + if err != nil { + return err + } + if err := msg.ValidateBasic(); err != nil { + return nil + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} + +func parseMigrateContractArgs(args []string, cliCtx context.CLIContext) (types.MsgMigrateContract, error) { + contractAddr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return types.MsgMigrateContract{}, sdkerrors.Wrap(err, "contract") + } + + // 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(), + Contract: contractAddr, + CodeID: codeID, + MigrateMsg: []byte(migrateMsg), + } + return msg, nil +} + +// UpdateContractAdminCmd sets an new admin for a contract +func UpdateContractAdminCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "set-contract-admin [contract_addr_bech32] [new_admin_addr_bech32]", + Short: "Set new admin for a contract", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + msg, err := parseUpdateContractAdminArgs(args, cliCtx) + if err != nil { + return err + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} + +func parseUpdateContractAdminArgs(args []string, cliCtx context.CLIContext) (types.MsgUpdateAdmin, error) { + contractAddr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return types.MsgUpdateAdmin{}, sdkerrors.Wrap(err, "contract") + } + newAdmin, err := sdk.AccAddressFromBech32(args[1]) + if err != nil { + return types.MsgUpdateAdmin{}, sdkerrors.Wrap(err, "new admin") + } + + msg := types.MsgUpdateAdmin{ + Sender: cliCtx.GetFromAddress(), + Contract: contractAddr, + NewAdmin: newAdmin, + } + return msg, nil +} + +// ClearContractAdminCmd clears an admin for a contract +func ClearContractAdminCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "clear-contract-admin [contract_addr_bech32]", + Short: "Clears admin for a contract to prevent further migrations", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + contractAddr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return sdkerrors.Wrap(err, "contract") + } + + msg := types.MsgClearAdmin{ + Sender: cliCtx.GetFromAddress(), + Contract: contractAddr, + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} diff --git a/x/wasm/client/cli/query.go b/x/wasm/client/cli/query.go new file mode 100644 index 0000000000..ef6282644d --- /dev/null +++ b/x/wasm/client/cli/query.go @@ -0,0 +1,337 @@ +package cli + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "strconv" + + flag "github.com/spf13/pflag" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/keeper" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +func GetQueryCmd(cdc *codec.Codec) *cobra.Command { + queryCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Querying commands for the wasm module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + queryCmd.AddCommand(flags.GetCommands( + GetCmdListCode(cdc), + GetCmdListContractByCode(cdc), + GetCmdQueryCode(cdc), + GetCmdGetContractInfo(cdc), + GetCmdGetContractHistory(cdc), + GetCmdGetContractState(cdc), + )...) + return queryCmd +} + +// GetCmdListCode lists all wasm code uploaded +func GetCmdListCode(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "list-code", + Short: "List all wasm bytecode on the chain", + Long: "List all wasm bytecode on the chain", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, keeper.QueryListCode) + res, _, err := cliCtx.Query(route) + if err != nil { + return err + } + fmt.Println(string(res)) + return nil + }, + } +} + +// GetCmdListContractByCode lists all wasm code uploaded for given code id +func GetCmdListContractByCode(cdc *codec.Codec) *cobra.Command { + return &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", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + codeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + route := fmt.Sprintf("custom/%s/%s/%d", types.QuerierRoute, keeper.QueryListContractByCode, codeID) + res, _, err := cliCtx.Query(route) + if err != nil { + return err + } + fmt.Println(string(res)) + return nil + }, + } +} + +// GetCmdQueryCode returns the bytecode for a given contract +func GetCmdQueryCode(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "code [code_id] [output filename]", + Short: "Downloads wasm bytecode for given code id", + Long: "Downloads wasm bytecode for given code id", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + codeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return err + } + + route := fmt.Sprintf("custom/%s/%s/%d", types.QuerierRoute, keeper.QueryGetCode, codeID) + res, _, err := cliCtx.Query(route) + if err != nil { + return err + } + if len(res) == 0 { + return fmt.Errorf("contract not found") + } + var code types.CodeInfoResponse + err = cliCtx.Codec.UnmarshalJSON(res, &code) + if err != nil { + return err + } + + if len(code.GetData()) == 0 { + return fmt.Errorf("contract not found") + } + + fmt.Printf("Downloading wasm code to %s\n", args[1]) + return ioutil.WriteFile(args[1], code.GetData(), 0600) + }, + } +} + +// GetCmdGetContractInfo gets details about a given contract +func GetCmdGetContractInfo(cdc *codec.Codec) *cobra.Command { + return &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", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + addr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + route := fmt.Sprintf("custom/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContract, addr.String()) + res, _, err := cliCtx.Query(route) + if err != nil { + return err + } + fmt.Println(string(res)) + return nil + }, + } +} + +// GetCmdGetContractState dumps full internal state of a given contract +func GetCmdGetContractState(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "contract-state", + Short: "Querying commands for the wasm module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + cmd.AddCommand(flags.GetCommands( + GetCmdGetContractStateAll(cdc), + GetCmdGetContractStateRaw(cdc), + GetCmdGetContractStateSmart(cdc), + )...) + return cmd +} + +func GetCmdGetContractStateAll(cdc *codec.Codec) *cobra.Command { + return &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 { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + addr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateAll) + res, _, err := cliCtx.Query(route) + if err != nil { + return err + } + fmt.Println(string(res)) + return nil + }, + } +} + +func GetCmdGetContractStateRaw(cdc *codec.Codec) *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(_ *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + addr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + queryData, err := decoder.DecodeString(args[1]) + if err != nil { + return err + } + route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateRaw) + res, _, err := cliCtx.QueryWithData(route, queryData) + if err != nil { + return err + } + fmt.Println(string(res)) + return nil + }, + } + decoder.RegisterFlags(cmd.PersistentFlags(), "key argument") + return cmd +} + +func GetCmdGetContractStateSmart(cdc *codec.Codec) *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(_ *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + addr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + key := args[1] + if key == "" { + return errors.New("key must not be empty") + } + route := fmt.Sprintf("custom/%s/%s/%s/%s", types.QuerierRoute, keeper.QueryGetContractState, addr.String(), keeper.QueryMethodContractStateSmart) + + 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") + } + res, _, err := cliCtx.QueryWithData(route, queryData) + if err != nil { + return err + } + fmt.Println(string(res)) + return nil + }, + } + decoder.RegisterFlags(cmd.PersistentFlags(), "query argument") + return cmd +} + +// GetCmdGetContractHistory prints the code history for a given contract +func GetCmdGetContractHistory(cdc *codec.Codec) *cobra.Command { + return &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", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + addr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + route := fmt.Sprintf("custom/%s/%s/%s", types.QuerierRoute, keeper.QueryContractHistory, addr.String()) + res, _, err := cliCtx.Query(route) + if err != nil { + return err + } + fmt.Println(string(res)) + return nil + }, + } +} + +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 +} diff --git a/x/wasm/client/cli/tx.go b/x/wasm/client/cli/tx.go new file mode 100644 index 0000000000..53b178ab39 --- /dev/null +++ b/x/wasm/client/cli/tx.go @@ -0,0 +1,235 @@ +package cli + +import ( + "bufio" + "fmt" + "io/ioutil" + "strconv" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + + wasmUtils "github.com/line/lbm-sdk/v2/x/wasm/client/utils" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +const ( + flagAmount = "amount" + flagSource = "source" + flagBuilder = "builder" + flagLabel = "label" + flagAdmin = "admin" + flagRunAs = "run-as" + flagInstantiateByEverybody = "instantiate-everybody" + flagInstantiateByAddress = "instantiate-only-address" + flagProposalType = "type" +) + +// GetTxCmd returns the transaction commands for this module +func GetTxCmd(cdc *codec.Codec) *cobra.Command { + txCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Wasm transaction subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + txCmd.AddCommand(flags.PostCommands( + StoreCodeCmd(cdc), + InstantiateContractCmd(cdc), + ExecuteContractCmd(cdc), + MigrateContractCmd(cdc), + UpdateContractAdminCmd(cdc), + ClearContractAdminCmd(cdc), + )...) + return txCmd +} + +// StoreCodeCmd will upload code to be reused. +func StoreCodeCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "store [wasm file] --source [source] --builder [builder]", + Short: "Upload a wasm binary", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + msg, err := parseStoreCodeArgs(args, cliCtx) + if err != nil { + return err + } + if err = msg.ValidateBasic(); err != nil { + return err + } + + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd.Flags().String(flagSource, "", "A valid URI reference to the contract's source code, optional") + cmd.Flags().String(flagBuilder, "", "A valid docker tag for the build system, optional") + cmd.Flags().String(flagInstantiateByEverybody, "", "Everybody can instantiate a contract from the code, optional") + cmd.Flags().String(flagInstantiateByAddress, "", "Only this address can instantiate a contract instance from the code, optional") + + return cmd +} + +func parseStoreCodeArgs(args []string, cliCtx context.CLIContext) (types.MsgStoreCode, error) { + wasm, err := ioutil.ReadFile(args[0]) + if err != nil { + return types.MsgStoreCode{}, err + } + + // gzip the wasm file + if wasmUtils.IsWasm(wasm) { + wasm, err = wasmUtils.GzipIt(wasm) + + if err != nil { + return types.MsgStoreCode{}, err + } + } else if !wasmUtils.IsGzip(wasm) { + return types.MsgStoreCode{}, fmt.Errorf("invalid input file. Use wasm binary or gzip") + } + + var perm *types.AccessConfig + if onlyAddrStr := viper.GetString(flagInstantiateByAddress); onlyAddrStr != "" { + allowedAddr, err := sdk.AccAddressFromBech32(onlyAddrStr) + if err != nil { + return types.MsgStoreCode{}, sdkerrors.Wrap(err, flagInstantiateByAddress) + } + x := types.OnlyAddress.With(allowedAddr) + perm = &x + } else if everybody := viper.GetBool(flagInstantiateByEverybody); everybody { + perm = &types.AllowEverybody + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.MsgStoreCode{ + Sender: cliCtx.GetFromAddress(), + WASMByteCode: wasm, + Source: viper.GetString(flagSource), + Builder: viper.GetString(flagBuilder), + InstantiatePermission: perm, + } + return msg, nil +} + +// InstantiateContractCmd will instantiate a contract from previously uploaded code. +func InstantiateContractCmd(cdc *codec.Codec) *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", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + msg, err := parseInstantiateArgs(args, cliCtx) + if err != nil { + return err + } + if err := msg.ValidateBasic(); err != nil { + return err + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + 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") + return cmd +} + +func parseInstantiateArgs(args []string, cliCtx context.CLIContext) (types.MsgInstantiateContract, error) { + // get the id of the code to instantiate + codeID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return types.MsgInstantiateContract{}, err + } + + amounstStr := viper.GetString(flagAmount) + amount, err := sdk.ParseCoins(amounstStr) + if err != nil { + return types.MsgInstantiateContract{}, err + } + + label := viper.GetString(flagLabel) + if label == "" { + return types.MsgInstantiateContract{}, fmt.Errorf("label is required on all contracts") + } + + initMsg := args[1] + + adminStr := viper.GetString(flagAdmin) + var adminAddr sdk.AccAddress + if len(adminStr) != 0 { + adminAddr, err = sdk.AccAddressFromBech32(adminStr) + if err != nil { + return types.MsgInstantiateContract{}, sdkerrors.Wrap(err, "admin") + } + } + + // build and sign the transaction, then broadcast to Tendermint + msg := types.MsgInstantiateContract{ + Sender: cliCtx.GetFromAddress(), + CodeID: codeID, + Label: label, + InitFunds: amount, + InitMsg: []byte(initMsg), + Admin: adminAddr, + } + return msg, nil +} + +// ExecuteContractCmd will instantiate a contract from previously uploaded code. +func ExecuteContractCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "execute [contract_addr_bech32] [json_encoded_send_args]", + Short: "Execute a command on a wasm contract", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + // get the id of the code to instantiate + contractAddr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return err + } + + amounstStr := viper.GetString(flagAmount) + amount, err := sdk.ParseCoins(amounstStr) + if err != nil { + return err + } + + execMsg := args[1] + + // build and sign the transaction, then broadcast to Tendermint + msg := types.MsgExecuteContract{ + Sender: cliCtx.GetFromAddress(), + Contract: contractAddr, + SentFunds: amount, + Msg: []byte(execMsg), + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + + cmd.Flags().String(flagAmount, "", "Coins to send to the contract along with command") + return cmd +} diff --git a/x/wasm/client/proposal_handler.go b/x/wasm/client/proposal_handler.go new file mode 100644 index 0000000000..2dc04b2a26 --- /dev/null +++ b/x/wasm/client/proposal_handler.go @@ -0,0 +1,17 @@ +package client + +import ( + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + + "github.com/line/lbm-sdk/v2/x/wasm/client/cli" + "github.com/line/lbm-sdk/v2/x/wasm/client/rest" +) + +// ProposalHandlers define the wasm cli proposal types and rest handler. +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.ProposalUpdateContractAdminCmd, rest.UpdateContractAdminProposalHandler), + govclient.NewProposalHandler(cli.ProposalClearContractAdminCmd, rest.ClearContractAdminProposalHandler), +} diff --git a/x/wasm/client/proposal_handler_test.go b/x/wasm/client/proposal_handler_test.go new file mode 100644 index 0000000000..4de9f1f160 --- /dev/null +++ b/x/wasm/client/proposal_handler_test.go @@ -0,0 +1,236 @@ +// nolint: scopelint +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + authvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting" + "github.com/cosmos/cosmos-sdk/x/gov" + + wasmtypes "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +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"}}, + } + ) + cdc := MakeCodec() + clientCtx := context.CLIContext{}.WithChainID("testing").WithCodec(cdc) + + // 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": "my/builder:tag", + "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": "my/builder:tag", + "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": "my/builder:tag", + "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/", + "builder": "my/builder:tag", + "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": "", + "source": "https://example.com/", + "builder": "my/builder:tag", + "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/", + "init_msg": "my/builder:tag", + "init_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": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", + "code_id": "1", + "msg": dict{"foo": "bar"}, + "run_as": "cosmos100dejzacpanrldpjjwksjm62shqhyss44jf5xz", + "deposit": []dict{{"denom": "ustake", "amount": "10"}}, + "proposer": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "base_req": aBaseReq, + }, + expCode: http.StatusOK, + }, + "update contract admin": { + srcPath: "/gov/proposals/wasm_update_admin", + srcBody: dict{ + "title": "Test Proposal", + "description": "My proposal", + "type": "migrate", + "contract": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", + "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": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", + "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()) + }) + } +} + +func MakeCodec() *codec.Codec { + var cdc = codec.New() + wasmtypes.RegisterCodec(cdc) + gov.RegisterCodec(cdc) + sdk.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + codec.RegisterEvidences(cdc) + authvesting.RegisterCodec(cdc) + + return cdc.Seal() +} diff --git a/x/wasm/client/rest/gov.go b/x/wasm/client/rest/gov.go new file mode 100644 index 0000000000..b345e5c21f --- /dev/null +++ b/x/wasm/client/rest/gov.go @@ -0,0 +1,271 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/cosmos/cosmos-sdk/x/gov" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/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 sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + RunAs sdk.AccAddress `json:"run_as" yaml:"run_as"` + // WASMByteCode can be raw or gzip compressed + WASMByteCode []byte `json:"wasm_byte_code" yaml:"wasm_byte_code"` + // Source is a valid absolute HTTPS URI to the contract's source code, optional + Source string `json:"source" yaml:"source"` + // Builder is a valid docker image name with tag, optional + Builder string `json:"builder" yaml:"builder"` + // InstantiatePermission to apply on contract creation, optional + InstantiatePermission *types.AccessConfig `json:"instantiate_permission" yaml:"instantiate_permission"` +} + +func (s StoreCodeProposalJSONReq) Content() gov.Content { + return types.StoreCodeProposal{ + WasmProposal: types.WasmProposal{ + Title: s.Title, + Description: s.Description, + }, + RunAs: s.RunAs, + WASMByteCode: s.WASMByteCode, + Source: s.Source, + Builder: s.Builder, + InstantiatePermission: s.InstantiatePermission, + } +} +func (s StoreCodeProposalJSONReq) GetProposer() sdk.AccAddress { + return s.Proposer +} +func (s StoreCodeProposalJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} +func (s StoreCodeProposalJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func StoreCodeProposalHandler(cliCtx context.CLIContext) 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.Codec, &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 sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + RunAs sdk.AccAddress `json:"run_as" yaml:"run_as"` + // Admin is an optional address that can execute migrations + Admin sdk.AccAddress `json:"admin,omitempty" yaml:"admin"` + Code uint64 `json:"code_id" yaml:"code_id"` + Label string `json:"label" yaml:"label"` + InitMsg json.RawMessage `json:"init_msg" yaml:"init_msg"` + InitFunds sdk.Coins `json:"init_funds" yaml:"init_funds"` +} + +func (s InstantiateProposalJSONReq) Content() gov.Content { + return types.InstantiateContractProposal{ + WasmProposal: types.WasmProposal{Title: s.Title, Description: s.Description}, + RunAs: s.RunAs, + Admin: s.Admin, + CodeID: s.Code, + Label: s.Label, + InitMsg: s.InitMsg, + InitFunds: s.InitFunds, + } +} +func (s InstantiateProposalJSONReq) GetProposer() sdk.AccAddress { + return s.Proposer +} +func (s InstantiateProposalJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} +func (s InstantiateProposalJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} + +func InstantiateProposalHandler(cliCtx context.CLIContext) 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.Codec, &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 sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + Contract sdk.AccAddress `json:"contract" yaml:"contract"` + Code uint64 `json:"code_id" yaml:"code_id"` + MigrateMsg json.RawMessage `json:"msg" yaml:"msg"` + // RunAs is the role that is passed to the contract's environment + RunAs sdk.AccAddress `json:"run_as" yaml:"run_as"` +} + +func (s MigrateProposalJSONReq) Content() gov.Content { + return types.MigrateContractProposal{ + WasmProposal: types.WasmProposal{Title: s.Title, Description: s.Description}, + Contract: s.Contract, + CodeID: s.Code, + MigrateMsg: s.MigrateMsg, + RunAs: s.RunAs, + } +} +func (s MigrateProposalJSONReq) GetProposer() sdk.AccAddress { + return s.Proposer +} +func (s MigrateProposalJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} +func (s MigrateProposalJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} +func MigrateProposalHandler(cliCtx context.CLIContext) 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.Codec, &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 sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + NewAdmin sdk.AccAddress `json:"new_admin" yaml:"new_admin"` + Contract sdk.AccAddress `json:"contract" yaml:"contract"` +} + +func (s UpdateAdminJSONReq) Content() gov.Content { + return types.UpdateAdminProposal{ + WasmProposal: types.WasmProposal{Title: s.Title, Description: s.Description}, + Contract: s.Contract, + NewAdmin: s.NewAdmin, + } +} +func (s UpdateAdminJSONReq) GetProposer() sdk.AccAddress { + return s.Proposer +} +func (s UpdateAdminJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} +func (s UpdateAdminJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} +func UpdateContractAdminProposalHandler(cliCtx context.CLIContext) 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.Codec, &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 sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + + Contract sdk.AccAddress `json:"contract" yaml:"contract"` +} + +func (s ClearAdminJSONReq) Content() gov.Content { + return types.ClearAdminProposal{ + WasmProposal: types.WasmProposal{Title: s.Title, Description: s.Description}, + Contract: s.Contract, + } +} +func (s ClearAdminJSONReq) GetProposer() sdk.AccAddress { + return s.Proposer +} +func (s ClearAdminJSONReq) GetDeposit() sdk.Coins { + return s.Deposit +} +func (s ClearAdminJSONReq) GetBaseReq() rest.BaseReq { + return s.BaseReq +} +func ClearContractAdminProposalHandler(cliCtx context.CLIContext) 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.Codec, &req) { + return + } + toStdTxResponse(cliCtx, w, req) + }, + } +} + +type wasmProposalData interface { + Content() gov.Content + GetProposer() sdk.AccAddress + GetDeposit() sdk.Coins + GetBaseReq() rest.BaseReq +} + +func toStdTxResponse(cliCtx context.CLIContext, w http.ResponseWriter, data wasmProposalData) { + msg := gov.NewMsgSubmitProposal(data.Content(), data.GetDeposit(), data.GetProposer()) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + baseReq := data.GetBaseReq().Sanitize() + if !baseReq.ValidateBasic(w) { + return + } + utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg}) +} diff --git a/x/wasm/client/rest/new_tx.go b/x/wasm/client/rest/new_tx.go new file mode 100644 index 0000000000..8d272c57c9 --- /dev/null +++ b/x/wasm/client/rest/new_tx.go @@ -0,0 +1,98 @@ +package rest + +import ( + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/gorilla/mux" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +func registerNewTxRoutes(cliCtx context.CLIContext, 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 sdk.AccAddress `json:"admin,omitempty" yaml:"admin"` + CodeID uint64 `json:"code_id" yaml:"code_id"` + MigrateMsg []byte `json:"migrate_msg,omitempty" yaml:"migrate_msg"` +} +type updateContractAdministrateReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Admin sdk.AccAddress `json:"admin,omitempty" yaml:"admin"` +} + +func setContractAdminHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req updateContractAdministrateReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + vars := mux.Vars(r) + contractAddr := vars["contractAddr"] + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + contractAddress, err := sdk.AccAddressFromBech32(contractAddr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + msg := types.MsgUpdateAdmin{ + Sender: cliCtx.GetFromAddress(), + NewAdmin: req.Admin, + Contract: contractAddress, + } + if err = msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +func migrateContractHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req migrateContractReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + vars := mux.Vars(r) + contractAddr := vars["contractAddr"] + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + contractAddress, err := sdk.AccAddressFromBech32(contractAddr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + msg := types.MsgMigrateContract{ + Sender: cliCtx.GetFromAddress(), + Contract: contractAddress, + CodeID: req.CodeID, + MigrateMsg: req.MigrateMsg, + } + if err = msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/wasm/client/rest/query.go b/x/wasm/client/rest/query.go new file mode 100644 index 0000000000..41180db652 --- /dev/null +++ b/x/wasm/client/rest/query.go @@ -0,0 +1,270 @@ +package rest + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/keeper" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +func registerQueryRoutes(cliCtx context.CLIContext, 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 context.CLIContext) 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 context.CLIContext) 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 context.CLIContext) 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 context.CLIContext) 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 context.CLIContext) 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 context.CLIContext) 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 context.CLIContext) 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 context.CLIContext) 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) + } +} diff --git a/x/wasm/client/rest/rest.go b/x/wasm/client/rest/rest.go new file mode 100644 index 0000000000..fc47da384d --- /dev/null +++ b/x/wasm/client/rest/rest.go @@ -0,0 +1,14 @@ +package rest + +import ( + "github.com/gorilla/mux" + + "github.com/cosmos/cosmos-sdk/client/context" +) + +// RegisterRoutes registers staking-related REST handlers to a router +func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { + registerQueryRoutes(cliCtx, r) + registerTxRoutes(cliCtx, r) + registerNewTxRoutes(cliCtx, r) +} diff --git a/x/wasm/client/rest/tx.go b/x/wasm/client/rest/tx.go new file mode 100644 index 0000000000..0e50bd6c9a --- /dev/null +++ b/x/wasm/client/rest/tx.go @@ -0,0 +1,168 @@ +package rest + +import ( + "net/http" + "strconv" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/gorilla/mux" + + wasmUtils "github.com/line/lbm-sdk/v2/x/wasm/client/utils" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +func registerTxRoutes(cliCtx context.CLIContext, 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") +} + +// limit max bytes read to prevent gzip bombs +const maxSize = 400 * 1024 + +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"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + Admin sdk.AccAddress `json:"admin,omitempty" yaml:"admin"` + InitMsg []byte `json:"init_msg" yaml:"init_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 context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req storeCodeReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + var err error + wasm := req.WasmBytes + if len(wasm) > maxSize { + rest.WriteErrorResponse(w, http.StatusBadRequest, "Binary size exceeds maximum limit") + return + } + + // gzip the wasm file + if wasmUtils.IsWasm(wasm) { + wasm, err = wasmUtils.GzipIt(wasm) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + } else if !wasmUtils.IsGzip(wasm) { + rest.WriteErrorResponse(w, http.StatusBadRequest, "Invalid input file, use wasm binary or zip") + return + } + + fromAddr, err := sdk.AccAddressFromBech32(req.BaseReq.From) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + // build and sign the transaction, then broadcast to Tendermint + msg := types.MsgStoreCode{ + Sender: fromAddr, + WASMByteCode: wasm, + } + + err = msg.ValidateBasic() + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +func instantiateContractHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req instantiateContractReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + vars := mux.Vars(r) + cid := vars["codeId"] + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + // get the id of the code to instantiate + codeID, err := strconv.ParseUint(cid, 10, 64) + if err != nil { + return + } + + msg := types.MsgInstantiateContract{ + Sender: cliCtx.GetFromAddress(), + CodeID: codeID, + InitFunds: req.Deposit, + InitMsg: req.InitMsg, + Admin: req.Admin, + } + + err = msg.ValidateBasic() + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +func executeContractHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req executeContractReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + vars := mux.Vars(r) + contractAddr := vars["contractAddr"] + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + contractAddress, err := sdk.AccAddressFromBech32(contractAddr) + if err != nil { + return + } + + msg := types.MsgExecuteContract{ + Sender: cliCtx.GetFromAddress(), + Contract: contractAddress, + Msg: req.ExecMsg, + SentFunds: req.Amount, + } + + err = msg.ValidateBasic() + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/wasm/client/utils/utils.go b/x/wasm/client/utils/utils.go new file mode 100644 index 0000000000..bbe9adc7f9 --- /dev/null +++ b/x/wasm/client/utils/utils.go @@ -0,0 +1,38 @@ +package utils + +import ( + "bytes" + "compress/gzip" +) + +var ( + 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 bytes.Equal(input[:3], gzipIdent) +} + +// 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 +} diff --git a/x/wasm/client/utils/utils_test.go b/x/wasm/client/utils/utils_test.go new file mode 100644 index 0000000000..bfba136007 --- /dev/null +++ b/x/wasm/client/utils/utils_test.go @@ -0,0 +1,65 @@ +package utils + +import ( + "io/ioutil" + "testing" + + "github.com/stretchr/testify/require" +) + +func GetTestData() ([]byte, []byte, []byte, error) { + wasmCode, err := ioutil.ReadFile("../../internal/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.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) +} diff --git a/x/wasm/genesis_test.go b/x/wasm/genesis_test.go new file mode 100644 index 0000000000..580375eab4 --- /dev/null +++ b/x/wasm/genesis_test.go @@ -0,0 +1,145 @@ +// nolint: staticcheck, errcheck, deadcode, unused +package wasm + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type contractState struct { +} + +func TestInitGenesis(t *testing.T) { + data, cleanup := setupTest(t) + defer cleanup() + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(data.ctx, data.acctKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(data.ctx, data.acctKeeper, topUp) + + h := data.module.NewHandler() + q := data.module.NewQuerierHandler() + + t.Log("fail with invalid source url") + msg := MsgStoreCode{ + Sender: creator, + WASMByteCode: testContract, + Source: "someinvalidurl", + Builder: "", + } + + err := msg.ValidateBasic() + require.Error(t, err) + + _, err = h(data.ctx, msg) + require.Error(t, err) + + t.Log("fail with relative source url") + msg = MsgStoreCode{ + Sender: creator, + WASMByteCode: testContract, + Source: "./testdata/escrow.wasm", + Builder: "", + } + + err = msg.ValidateBasic() + require.Error(t, err) + + _, err = h(data.ctx, msg) + require.Error(t, err) + + t.Log("fail with invalid build tag") + msg = MsgStoreCode{ + Sender: creator, + WASMByteCode: testContract, + Source: "", + Builder: "somerandombuildtag-0.6.2", + } + + err = msg.ValidateBasic() + require.Error(t, err) + + _, err = h(data.ctx, msg) + require.Error(t, err) + + t.Log("no error with valid source and build tag") + msg = MsgStoreCode{ + Sender: creator, + WASMByteCode: testContract, + Source: "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", + Builder: "confio/cosmwasm-opt:0.7.0", + } + err = msg.ValidateBasic() + require.NoError(t, err) + + res, err := h(data.ctx, msg) + require.NoError(t, err) + require.Equal(t, res.Data, []byte("1")) + + _, _, bob := keyPubAddr() + initMsg := initMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + initCmd := MsgInstantiateContract{ + Sender: creator, + CodeID: firstCodeID, + InitMsg: initMsgBz, + InitFunds: deposit, + } + res, err = h(data.ctx, initCmd) + require.NoError(t, err) + contractAddr := sdk.AccAddress(res.Data) + + execCmd := MsgExecuteContract{ + Sender: fred, + Contract: contractAddr, + Msg: []byte(`{"release":{}}`), + SentFunds: topUp, + } + res, err = h(data.ctx, execCmd) + require.NoError(t, err) + + // 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{contractAddr.String()}) + assertContractInfo(t, q, data.ctx, contractAddr, 1, creator) + assertContractState(t, q, data.ctx, contractAddr, state{ + Verifier: []byte(fred), + Beneficiary: []byte(bob), + Funder: []byte(creator), + }) + + // export into genstate + genState := ExportGenesis(data.ctx, data.keeper) + + // create new app to import genstate into + newData, newCleanup := setupTest(t) + defer newCleanup() + q2 := newData.module.NewQuerierHandler() + + // 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{contractAddr.String()}) + assertContractInfo(t, q2, newData.ctx, contractAddr, 1, creator) + assertContractState(t, q2, newData.ctx, contractAddr, state{ + Verifier: []byte(fred), + Beneficiary: []byte(bob), + Funder: []byte(creator), + }) +} diff --git a/x/wasm/handler.go b/x/wasm/handler.go new file mode 100644 index 0000000000..9d6d06d2b2 --- /dev/null +++ b/x/wasm/handler.go @@ -0,0 +1,163 @@ +package wasm + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +// NewHandler returns a handler for "bank" type messages. +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + + switch msg := msg.(type) { + case MsgStoreCode: + return handleStoreCode(ctx, k, &msg) + case MsgInstantiateContract: + return handleInstantiate(ctx, k, &msg) + case MsgExecuteContract: + return handleExecute(ctx, k, &msg) + case MsgMigrateContract: + return handleMigration(ctx, k, &msg) + case MsgUpdateAdmin: + return handleUpdateContractAdmin(ctx, k, &msg) + case MsgClearAdmin: + return handleClearContractAdmin(ctx, k, &msg) + default: + errMsg := fmt.Sprintf("unrecognized wasm message type: %T", msg) + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg) + } + } +} + +// filterMessageEvents returns the same events with all of type == EventTypeMessage removed. +// this is so only our top-level message event comes through +func filterMessageEvents(manager *sdk.EventManager) sdk.Events { + events := manager.Events() + res := make([]sdk.Event, 0, len(events)+1) + for _, e := range events { + if e.Type != sdk.EventTypeMessage { + res = append(res, e) + } + } + return res +} + +func handleStoreCode(ctx sdk.Context, k Keeper, msg *MsgStoreCode) (*sdk.Result, error) { + err := msg.ValidateBasic() + if err != nil { + return nil, err + } + + codeID, err := k.Create(ctx, msg.Sender, msg.WASMByteCode, msg.Source, msg.Builder, msg.InstantiatePermission) + if err != nil { + return nil, err + } + + events := filterMessageEvents(ctx.EventManager()) + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(types.AttributeKeySigner, msg.Sender.String()), + sdk.NewAttribute(types.AttributeKeyCodeID, fmt.Sprintf("%d", codeID)), + ) + + return &sdk.Result{ + Data: []byte(fmt.Sprintf("%d", codeID)), + Events: append(events, ourEvent), + }, nil +} + +func handleInstantiate(ctx sdk.Context, k Keeper, msg *MsgInstantiateContract) (*sdk.Result, error) { + contractAddr, err := k.Instantiate(ctx, msg.CodeID, msg.Sender, msg.Admin, msg.InitMsg, msg.Label, msg.InitFunds) + if err != nil { + return nil, err + } + + events := filterMessageEvents(ctx.EventManager()) + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(types.AttributeKeySigner, msg.Sender.String()), + sdk.NewAttribute(types.AttributeKeyCodeID, fmt.Sprintf("%d", msg.CodeID)), + sdk.NewAttribute(types.AttributeKeyContract, contractAddr.String()), + ) + + return &sdk.Result{ + Data: contractAddr, + Events: append(events, ourEvent), + }, nil +} + +func handleExecute(ctx sdk.Context, k Keeper, msg *MsgExecuteContract) (*sdk.Result, error) { + res, err := k.Execute(ctx, msg.Contract, msg.Sender, msg.Msg, msg.SentFunds) + if err != nil { + return nil, err + } + + events := filterMessageEvents(ctx.EventManager()) + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(types.AttributeKeySigner, msg.Sender.String()), + sdk.NewAttribute(types.AttributeKeyContract, msg.Contract.String()), + ) + + events = append(events, ourEvent) + res.Events = events + return res, nil +} + +func handleMigration(ctx sdk.Context, k Keeper, msg *MsgMigrateContract) (*sdk.Result, error) { + res, err := k.Migrate(ctx, msg.Contract, msg.Sender, msg.CodeID, msg.MigrateMsg) + if err != nil { + return nil, err + } + + events := filterMessageEvents(ctx.EventManager()) + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(types.AttributeKeySigner, msg.Sender.String()), + sdk.NewAttribute(types.AttributeKeyContract, msg.Contract.String()), + ) + events = append(events, ourEvent) + res.Events = events + return res, nil +} + +func handleUpdateContractAdmin(ctx sdk.Context, k Keeper, msg *MsgUpdateAdmin) (*sdk.Result, error) { + if err := k.UpdateContractAdmin(ctx, msg.Contract, msg.Sender, msg.NewAdmin); err != nil { + return nil, err + } + events := ctx.EventManager().Events() + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(types.AttributeKeySigner, msg.Sender.String()), + sdk.NewAttribute(types.AttributeKeyContract, msg.Contract.String()), + ) + return &sdk.Result{ + Events: append(events, ourEvent), + }, nil +} + +func handleClearContractAdmin(ctx sdk.Context, k Keeper, msg *MsgClearAdmin) (*sdk.Result, error) { + if err := k.ClearContractAdmin(ctx, msg.Contract, msg.Sender); err != nil { + return nil, err + } + events := ctx.EventManager().Events() + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, ModuleName), + sdk.NewAttribute(types.AttributeKeySigner, msg.Sender.String()), + sdk.NewAttribute(types.AttributeKeyContract, msg.Contract.String()), + ) + return &sdk.Result{ + Events: append(events, ourEvent), + }, nil +} diff --git a/x/wasm/internal/keeper/api.go b/x/wasm/internal/keeper/api.go new file mode 100644 index 0000000000..36c7b3b58e --- /dev/null +++ b/x/wasm/internal/keeper/api.go @@ -0,0 +1,30 @@ +package keeper + +import ( + "fmt" + + wasmvm "github.com/CosmWasm/wasmvm" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var ( + CostHumanize = 5 * GasMultiplier + CostCanonical = 4 * GasMultiplier +) + +func humanAddress(canon []byte) (string, uint64, error) { + if len(canon) != sdk.AddrLen { + return "", CostHumanize, fmt.Errorf("expected %d byte address", sdk.AddrLen) + } + 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, +} diff --git a/x/wasm/internal/keeper/authz_policy.go b/x/wasm/internal/keeper/authz_policy.go new file mode 100644 index 0000000000..1c2278773e --- /dev/null +++ b/x/wasm/internal/keeper/authz_policy.go @@ -0,0 +1,42 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +type AuthorizationPolicy interface { + CanCreateCode(c types.AccessConfig, creator sdk.AccAddress) bool + CanInstantiateContract(c types.AccessConfig, actor sdk.AccAddress) bool + CanModifyContract(admin, actor sdk.AccAddress) bool +} + +type DefaultAuthorizationPolicy struct { +} + +func (p DefaultAuthorizationPolicy) CanCreateCode(config types.AccessConfig, actor sdk.AccAddress) bool { + return config.Allowed(actor) +} + +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) +} + +type GovAuthorizationPolicy struct { +} + +func (p GovAuthorizationPolicy) CanCreateCode(types.AccessConfig, sdk.AccAddress) 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 +} diff --git a/x/wasm/internal/keeper/genesis.go b/x/wasm/internal/keeper/genesis.go new file mode 100644 index 0000000000..1f643249f8 --- /dev/null +++ b/x/wasm/internal/keeper/genesis.go @@ -0,0 +1,104 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" + // authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + // "github.com/CosmWasm/wasmd/x/wasm/internal/types" +) + +// 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) error { + keeper.setParams(ctx, data.Params) + + var maxCodeID uint64 + for i, code := range data.Codes { + err := keeper.importCode(ctx, code.CodeID, code.CodeInfo, code.CodesBytes) + if err != nil { + return sdkerrors.Wrapf(err, "code %d with id: %d", i, code.CodeID) + } + if code.CodeID > maxCodeID { + maxCodeID = code.CodeID + } + } + + var maxContractID int + for i, contract := range data.Contracts { + err := keeper.importContract(ctx, contract.ContractAddress, &contract.ContractInfo, contract.ContractState) + if err != nil { + return 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 sdkerrors.Wrapf(err, "sequence number %d", i) + } + } + + // sanity check seq values + if keeper.peekAutoIncrementID(ctx, types.KeyLastCodeID) <= maxCodeID { + return sdkerrors.Wrapf(types.ErrInvalid, "seq %s must be greater %d ", string(types.KeyLastCodeID), maxCodeID) + } + if keeper.peekAutoIncrementID(ctx, types.KeyLastInstanceID) <= uint64(maxContractID) { + return sdkerrors.Wrapf(types.ErrInvalid, "seq %s must be greater %d ", string(types.KeyLastInstanceID), maxContractID) + } + + return 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, + CodesBytes: bytecode, + }) + return false + }) + + keeper.IterateContractInfo(ctx, func(addr sdk.AccAddress, contract types.ContractInfo) bool { + contractStateIterator := keeper.GetContractState(ctx, addr) + var state []types.Model + for ; contractStateIterator.Valid(); contractStateIterator.Next() { + m := types.Model{ + Key: contractStateIterator.Key(), + Value: contractStateIterator.Value(), + } + state = append(state, m) + } + // redact contract info + contract.Created = nil + + genState.Contracts = append(genState.Contracts, types.Contract{ + ContractAddress: addr, + ContractInfo: contract, + ContractState: state, + }) + + 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 +} diff --git a/x/wasm/internal/keeper/genesis_test.go b/x/wasm/internal/keeper/genesis_test.go new file mode 100644 index 0000000000..c7404ea423 --- /dev/null +++ b/x/wasm/internal/keeper/genesis_test.go @@ -0,0 +1,505 @@ +// nolint: errcheck, scopelint +package keeper + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "os" + "testing" + "time" + + fuzz "github.com/google/gofuzz" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/staking" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +const firstCodeID = 1 + +func TestGenesisExportImport(t *testing.T) { + srcKeeper, srcCtx, srcStoreKeys := setupKeeper(t) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + // store some test data + f := fuzz.New().Funcs(ModelFuzzers...) + + srcKeeper.setParams(srcCtx, types.DefaultParams()) + + for i := 0; i < 25; i++ { + var ( + codeInfo types.CodeInfo + contract types.ContractInfo + stateModels []types.Model + history []types.ContractCodeHistoryEntry + ) + f.Fuzz(&codeInfo) + f.Fuzz(&contract) + f.Fuzz(&stateModels) + f.NilChance(0).Fuzz(&history) + codeID, err := srcKeeper.Create(srcCtx, codeInfo.Creator, wasmCode, codeInfo.Source, codeInfo.Builder, &codeInfo.InstantiateConfig) + require.NoError(t, err) + contract.CodeID = codeID + contractAddr := srcKeeper.generateContractAddress(srcCtx, codeID) + srcKeeper.setContractInfo(srcCtx, contractAddr, &contract) + srcKeeper.appendToContractHistory(srcCtx, contractAddr, history...) + srcKeeper.importContractState(srcCtx, contractAddr, stateModels) + } + var wasmParams types.Params + f.NilChance(0).Fuzz(&wasmParams) + srcKeeper.setParams(srcCtx, wasmParams) + + // export + exportedState := ExportGenesis(srcCtx, srcKeeper) + // 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 := json.Marshal(exportedState) + require.NoError(t, err) + + // reset contract history in source DB for comparison with dest DB + srcKeeper.IterateContractInfo(srcCtx, func(address sdk.AccAddress, info types.ContractInfo) bool { + info.ResetFromGenesis(srcCtx) + srcKeeper.setContractInfo(srcCtx, address, &info) + return false + }) + + // re-import + dstKeeper, dstCtx, dstStoreKeys := setupKeeper(t) + + var importState types.GenesisState + err = json.Unmarshal(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: %s", srcStoreKeys[j].Name(), srcIT.Key()) + require.Equal(t, srcIT.Key(), dstIT.Key(), i) + + isContractHistory := srcStoreKeys[j].Name() == types.StoreKey && bytes.HasPrefix(srcIT.Key(), types.ContractHistoryStorePrefix) + if !isContractHistory { // only skip history entries because we know they are different + require.Equal(t, srcIT.Value(), dstIT.Value(), "[%s] element (%d): %X", srcStoreKeys[j].Name(), i, srcIT.Key()) + } + srcIT.Next() + dstIT.Next() + } + if !assert.False(t, dstIT.Valid()) { + t.Fatalf("dest Iterator still has key :%X", dstIT.Key()) + } + } +} + +func TestFailFastImport(t *testing.T) { + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + myCodeInfo := types.CodeInfoFixture(types.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, + CodesBytes: 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, + CodesBytes: wasmCode, + }, { + CodeID: 3, + CodeInfo: myCodeInfo, + CodesBytes: 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, + CodesBytes: wasmCode, + }, { + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodesBytes: 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: types.CodeInfoFixture(func(i *types.CodeInfo) { i.CodeHash = make([]byte, sha256.Size) }), + CodesBytes: wasmCode, + }}, + Params: types.DefaultParams(), + }}, + "prevent duplicate codeIDs": {src: types.GenesisState{ + Codes: []types.Code{ + { + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodesBytes: wasmCode, + }, + { + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodesBytes: wasmCode, + }, + }, + Params: types.DefaultParams(), + }}, + "happy path: code id in info and contract do match": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodesBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: contractAddress(1, 1), + ContractInfo: types.ContractInfoFixture(func(c *types.ContractInfo) { c.CodeID = 1 }, types.OnlyGenesisFields), + }, + }, + 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, + CodesBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: contractAddress(1, 1), + ContractInfo: types.ContractInfoFixture(func(c *types.ContractInfo) { c.CodeID = 1 }, types.OnlyGenesisFields), + }, { + ContractAddress: contractAddress(1, 2), + ContractInfo: types.ContractInfoFixture(func(c *types.ContractInfo) { c.CodeID = 1 }, types.OnlyGenesisFields), + }, + }, + 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: contractAddress(1, 1), + ContractInfo: types.ContractInfoFixture(func(c *types.ContractInfo) { c.CodeID = 1 }, types.OnlyGenesisFields), + }, + }, + Params: types.DefaultParams(), + }, + }, + "prevent duplicate contract address": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodesBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: contractAddress(1, 1), + ContractInfo: types.ContractInfoFixture(func(c *types.ContractInfo) { c.CodeID = 1 }, types.OnlyGenesisFields), + }, { + ContractAddress: contractAddress(1, 1), + ContractInfo: types.ContractInfoFixture(func(c *types.ContractInfo) { c.CodeID = 1 }, types.OnlyGenesisFields), + }, + }, + Params: types.DefaultParams(), + }, + }, + "prevent duplicate contract model keys": { + src: types.GenesisState{ + Codes: []types.Code{{ + CodeID: firstCodeID, + CodeInfo: myCodeInfo, + CodesBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: contractAddress(1, 1), + ContractInfo: types.ContractInfoFixture(func(c *types.ContractInfo) { c.CodeID = 1 }, types.OnlyGenesisFields), + ContractState: []types.Model{ + { + Key: []byte{0x1}, + Value: []byte("foo"), + }, + { + Key: []byte{0x1}, + Value: []byte("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, + CodesBytes: 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, + CodesBytes: wasmCode, + }}, + Contracts: []types.Contract{ + { + ContractAddress: contractAddress(1, 1), + ContractInfo: types.ContractInfoFixture(func(c *types.ContractInfo) { c.CodeID = 1 }, types.OnlyGenesisFields), + }, + }, + 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)) + got := InitGenesis(ctx, keeper, spec.src) + if spec.expSuccess { + require.NoError(t, got) + return + } + require.Error(t, got) + }) + } +} + +func TestImportContractWithCodeHistoryReset(t *testing.T) { + genesis := ` +{ + "params":{ + "code_upload_access": { + "permission": "Everybody" + }, + "instantiate_default_permission": "Everybody", + "max_wasm_code_size": "500000" + }, + "codes": [ + { + "code_id": "1", + "code_info": { + "code_hash": %q, + "creator": "cosmos1qtu5n0cnhfkjj6l2rq97hmky9fd89gwca9yarx", + "source": "https://example.com", + "builder": "foo/bar:tag", + "instantiate_config": { + "permission": "OnlyAddress", + "address": "cosmos1qtu5n0cnhfkjj6l2rq97hmky9fd89gwca9yarx" + } + }, + "code_bytes": %q + } + ], + "contracts": [ + { + "contract_address": "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", + "contract_info": { + "code_id": "1", + "creator": "cosmos13x849jzd03vne42ynpj25hn8npjecxqrjghd8x", + "admin": "cosmos1h5t8zxmjr30e9dqghtlpl40f2zz5cgey6esxtn", + "label": "ȀĴnZV芢毤" + } + } + ], + "sequences": [ + {"id_key": %q, "value": "2"}, + {"id_key": %q, "value": "2"} + ] +}` + keeper, ctx, _ := setupKeeper(t) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + wasmCodeHash := sha256.Sum256(wasmCode) + enc64 := base64.StdEncoding.EncodeToString + var importState types.GenesisState + err = keeper.cdc.UnmarshalJSON([]byte( + fmt.Sprintf(genesis, enc64(wasmCodeHash[:]), enc64(wasmCode), + enc64(append([]byte{0x04}, []byte("lastCodeId")...)), + enc64(append([]byte{0x04}, []byte("lastContractId")...))), + ), &importState) + require.NoError(t, err) + require.NoError(t, importState.ValidateBasic()) + + 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, _ := sdk.AccAddressFromBech32("cosmos1qtu5n0cnhfkjj6l2rq97hmky9fd89gwca9yarx") + expCodeInfo := types.CodeInfo{ + CodeHash: wasmCodeHash[:], + Creator: codeCreatorAddr, + Source: "https://example.com", + Builder: "foo/bar:tag", + InstantiateConfig: types.AccessConfig{ + Type: types.OnlyAddress, + Address: codeCreatorAddr, + }, + } + assert.Equal(t, expCodeInfo, *gotCodeInfo) + + // verify contract + contractAddr, _ := sdk.AccAddressFromBech32("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5") + gotContractInfo := keeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, gotContractInfo) + contractCreatorAddr, _ := sdk.AccAddressFromBech32("cosmos13x849jzd03vne42ynpj25hn8npjecxqrjghd8x") + adminAddr, _ := sdk.AccAddressFromBech32("cosmos1h5t8zxmjr30e9dqghtlpl40f2zz5cgey6esxtn") + + expContractInfo := types.ContractInfo{ + CodeID: firstCodeID, + Creator: contractCreatorAddr, + Admin: adminAddr, + Label: "ȀĴnZV芢毤", + Created: &types.AbsoluteTxPosition{BlockHeight: 0, TxIndex: 0}, + } + assert.Equal(t, expContractInfo, *gotContractInfo) + + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.GenesisContractCodeHistoryType, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + }, + } + assert.Equal(t, expHistory, keeper.GetContractHistory(ctx, contractAddr)) +} + +func setupKeeper(t *testing.T) (Keeper, sdk.Context, []sdk.StoreKey) { + t.Helper() + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tempDir) }) + var ( + keyParams = sdk.NewKVStoreKey(params.StoreKey) + tkeyParams = sdk.NewTransientStoreKey(params.TStoreKey) + keyWasm = sdk.NewKVStoreKey(types.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, abci.Header{ + Height: 1234567, + Time: time.Date(2020, time.April, 22, 12, 0, 0, 0, time.UTC), + }, false, log.NewNopLogger()) + cdc := MakeTestCodec() + pk := params.NewKeeper(cdc, keyParams, tkeyParams) + wasmConfig := types.DefaultWasmConfig() + srcKeeper := NewKeeper(cdc, keyWasm, pk.Subspace(types.DefaultParamspace), auth.AccountKeeper{}, nil, staking.Keeper{}, distribution.Keeper{}, nil, nil, nil, tempDir, wasmConfig, "", nil, nil) + return srcKeeper, ctx, []sdk.StoreKey{keyWasm, keyParams} +} diff --git a/x/wasm/internal/keeper/handler_plugin.go b/x/wasm/internal/keeper/handler_plugin.go new file mode 100644 index 0000000000..7b2c0935e6 --- /dev/null +++ b/x/wasm/internal/keeper/handler_plugin.go @@ -0,0 +1,301 @@ +package keeper + +import ( + "encoding/json" + "fmt" + + wasmTypes "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/x/distribution" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/line/lbm-sdk/v2/x/coin" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +type MessageHandler struct { + router sdk.Router + encodeRouter types.Router + encoders MessageEncoders +} + +func NewMessageHandler(router sdk.Router, encodeRouter types.Router, customEncoders *MessageEncoders) MessageHandler { + encoders := DefaultEncoders().Merge(customEncoders) + return MessageHandler{ + router: router, + encodeRouter: encodeRouter, + encoders: encoders, + } +} + +type BankEncoder func(sender sdk.AccAddress, msg *wasmTypes.BankMsg) ([]sdk.Msg, error) +type CustomEncoder func(sender sdk.AccAddress, msg json.RawMessage, router types.Router) ([]sdk.Msg, error) +type StakingEncoder func(sender sdk.AccAddress, msg *wasmTypes.StakingMsg) ([]sdk.Msg, error) +type WasmEncoder func(sender sdk.AccAddress, msg *wasmTypes.WasmMsg) ([]sdk.Msg, error) + +type MessageEncoders struct { + Bank BankEncoder + Custom CustomEncoder + Staking StakingEncoder + Wasm WasmEncoder +} + +func DefaultEncoders() MessageEncoders { + return MessageEncoders{ + Bank: EncodeBankMsg, + Custom: CustomMsg, + Staking: EncodeStakingMsg, + Wasm: EncodeWasmMsg, + } +} + +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.Staking != nil { + e.Staking = o.Staking + } + if o.Wasm != nil { + e.Wasm = o.Wasm + } + return e +} + +func (e MessageEncoders) Encode(contractAddr sdk.AccAddress, msg wasmTypes.CosmosMsg, router types.Router) ([]sdk.Msg, error) { + switch { + case msg.Bank != nil: + return e.Bank(contractAddr, msg.Bank) + case msg.Custom != nil: + return e.Custom(contractAddr, msg.Custom, router) + case msg.Staking != nil: + return e.Staking(contractAddr, msg.Staking) + case msg.Wasm != nil: + return e.Wasm(contractAddr, msg.Wasm) + } + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown variant of Wasm") +} + +func EncodeBankMsg(sender sdk.AccAddress, msg *wasmTypes.BankMsg) ([]sdk.Msg, error) { + if msg.Send == nil { + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown variant of Bank") + } + if len(msg.Send.Amount) == 0 { + return nil, nil + } + fromAddr, stderr := sdk.AccAddressFromBech32(msg.Send.FromAddress) + if stderr != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Send.FromAddress) + } + toAddr, stderr := sdk.AccAddressFromBech32(msg.Send.ToAddress) + if stderr != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Send.ToAddress) + } + toSend, err := convertWasmCoinsToSdkCoins(msg.Send.Amount) + if err != nil { + return nil, err + } + sdkMsg := coin.MsgSend{ + From: fromAddr, + To: toAddr, + Amount: toSend, + } + return []sdk.Msg{sdkMsg}, nil +} + +func CustomMsg(sender sdk.AccAddress, jsonMsg json.RawMessage, router types.Router) ([]sdk.Msg, error) { + var linkMsgWrapper types.LinkMsgWrapper + err := json.Unmarshal(jsonMsg, &linkMsgWrapper) + if err != nil { + return nil, err + } + handler := router.GetRoute(linkMsgWrapper.Module) + if handler == nil { + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized handler: %T", linkMsgWrapper.Module) + } + return handler(linkMsgWrapper.MsgData) +} + +func EncodeStakingMsg(sender sdk.AccAddress, msg *wasmTypes.StakingMsg) ([]sdk.Msg, error) { + if msg.Delegate != nil { + validator, err := sdk.ValAddressFromBech32(msg.Delegate.Validator) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Delegate.Validator) + } + coin, err := convertWasmCoinToSdkCoin(msg.Delegate.Amount) + if err != nil { + return nil, err + } + sdkMsg := staking.MsgDelegate{ + DelegatorAddress: sender, + ValidatorAddress: validator, + Amount: coin, + } + return []sdk.Msg{sdkMsg}, nil + } + if msg.Redelegate != nil { + src, err := sdk.ValAddressFromBech32(msg.Redelegate.SrcValidator) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Redelegate.SrcValidator) + } + dst, err := sdk.ValAddressFromBech32(msg.Redelegate.DstValidator) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Redelegate.DstValidator) + } + coin, err := convertWasmCoinToSdkCoin(msg.Redelegate.Amount) + if err != nil { + return nil, err + } + sdkMsg := staking.MsgBeginRedelegate{ + DelegatorAddress: sender, + ValidatorSrcAddress: src, + ValidatorDstAddress: dst, + Amount: coin, + } + return []sdk.Msg{sdkMsg}, nil + } + if msg.Undelegate != nil { + validator, err := sdk.ValAddressFromBech32(msg.Undelegate.Validator) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Undelegate.Validator) + } + coin, err := convertWasmCoinToSdkCoin(msg.Undelegate.Amount) + if err != nil { + return nil, err + } + sdkMsg := staking.MsgUndelegate{ + DelegatorAddress: sender, + ValidatorAddress: validator, + Amount: coin, + } + return []sdk.Msg{sdkMsg}, nil + } + if msg.Withdraw != nil { + var err error + rcpt := sender + if len(msg.Withdraw.Recipient) != 0 { + rcpt, err = sdk.AccAddressFromBech32(msg.Withdraw.Recipient) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Withdraw.Recipient) + } + } + validator, err := sdk.ValAddressFromBech32(msg.Withdraw.Validator) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Withdraw.Validator) + } + setMsg := distribution.MsgSetWithdrawAddress{ + DelegatorAddress: sender, + WithdrawAddress: rcpt, + } + withdrawMsg := distribution.MsgWithdrawDelegatorReward{ + DelegatorAddress: sender, + ValidatorAddress: validator, + } + return []sdk.Msg{setMsg, withdrawMsg}, nil + } + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown variant of Staking") +} + +func EncodeWasmMsg(sender sdk.AccAddress, msg *wasmTypes.WasmMsg) ([]sdk.Msg, error) { + if msg.Execute != nil { + contractAddr, err := sdk.AccAddressFromBech32(msg.Execute.ContractAddr) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Execute.ContractAddr) + } + coins, err := convertWasmCoinsToSdkCoins(msg.Execute.Send) + if err != nil { + return nil, err + } + + sdkMsg := types.MsgExecuteContract{ + Sender: sender, + Contract: contractAddr, + Msg: msg.Execute.Msg, + SentFunds: coins, + } + return []sdk.Msg{sdkMsg}, nil + } + if msg.Instantiate != nil { + coins, err := convertWasmCoinsToSdkCoins(msg.Instantiate.Send) + if err != nil { + return nil, err + } + + sdkMsg := types.MsgInstantiateContract{ + Sender: sender, + CodeID: msg.Instantiate.CodeID, + // TODO: add this to CosmWasm + Label: fmt.Sprintf("Auto-created by %s", sender), + InitMsg: msg.Instantiate.Msg, + InitFunds: coins, + } + return []sdk.Msg{sdkMsg}, nil + } + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown variant of Wasm") +} + +func (h MessageHandler) Dispatch(ctx sdk.Context, contractAddr sdk.AccAddress, msg wasmTypes.CosmosMsg, router types.Router) error { + sdkMsgs, err := h.encoders.Encode(contractAddr, msg, router) + if err != nil { + return err + } + for _, sdkMsg := range sdkMsgs { + if err := h.handleSdkMessage(ctx, contractAddr, sdkMsg); err != nil { + return err + } + } + return nil +} + +func (h MessageHandler) handleSdkMessage(ctx sdk.Context, contractAddr sdk.Address, msg sdk.Msg) error { + // make sure this account can send it + for _, acct := range msg.GetSigners() { + if !acct.Equals(contractAddr) { + return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "contract doesn't have permission") + } + } + + // find the handler and execute it + handler := h.router.Route(ctx, msg.Route()) + if handler == nil { + return sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, msg.Route()) + } + res, err := handler(ctx, msg) + if err != nil { + return err + } + // redispatch all events, (type sdk.EventTypeMessage will be filtered out in the handler) + ctx.EventManager().EmitEvents(res.Events) + + return nil +} + +func convertWasmCoinsToSdkCoins(coins []wasmTypes.Coin) (sdk.Coins, error) { + var toSend sdk.Coins + for _, coin := range coins { + c, err := convertWasmCoinToSdkCoin(coin) + if err != nil { + return nil, err + } + toSend = append(toSend, c) + } + return toSend, nil +} + +func convertWasmCoinToSdkCoin(coin wasmTypes.Coin) (sdk.Coin, error) { + amount, ok := sdk.NewIntFromString(coin.Amount) + if !ok { + return sdk.Coin{}, sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, coin.Amount+coin.Denom) + } + return sdk.Coin{ + Denom: coin.Denom, + Amount: amount, + }, nil +} diff --git a/x/wasm/internal/keeper/handler_plugin_test.go b/x/wasm/internal/keeper/handler_plugin_test.go new file mode 100644 index 0000000000..dee0323c43 --- /dev/null +++ b/x/wasm/internal/keeper/handler_plugin_test.go @@ -0,0 +1,277 @@ +package keeper + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/staking" + + wasmTypes "github.com/CosmWasm/wasmvm/types" + + "github.com/line/lbm-sdk/v2/x/coin" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +func TestEncoding(t *testing.T) { + _, _, addr1 := keyPubAddr() + _, _, addr2 := keyPubAddr() + invalidAddr := "xrnd1d02kd90n38qvr3qb9qof83fn2d2" + valAddr := make(sdk.ValAddress, sdk.AddrLen) + valAddr[0] = 12 + valAddr2 := make(sdk.ValAddress, sdk.AddrLen) + valAddr2[1] = 123 + + jsonMsg := json.RawMessage(`{"foo": 123}`) + + cases := map[string]struct { + sender sdk.AccAddress + input wasmTypes.CosmosMsg + // set if valid + output []sdk.Msg + // set if invalid + isError bool + }{ + "simple send": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Bank: &wasmTypes.BankMsg{ + Send: &wasmTypes.SendMsg{ + FromAddress: addr1.String(), + ToAddress: addr2.String(), + Amount: []wasmTypes.Coin{ + { + Denom: "uatom", + Amount: "12345", + }, + { + Denom: "usdt", + Amount: "54321", + }, + }, + }, + }, + }, + output: []sdk.Msg{ + coin.MsgSend{ + From: addr1, + To: addr2, + Amount: sdk.Coins{ + sdk.NewInt64Coin("uatom", 12345), + sdk.NewInt64Coin("usdt", 54321), + }, + }, + }, + }, + "invalid send amount": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Bank: &wasmTypes.BankMsg{ + Send: &wasmTypes.SendMsg{ + FromAddress: addr1.String(), + ToAddress: addr2.String(), + Amount: []wasmTypes.Coin{ + { + Denom: "uatom", + Amount: "123.456", + }, + }, + }, + }, + }, + isError: true, + }, + "invalid address": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Bank: &wasmTypes.BankMsg{ + Send: &wasmTypes.SendMsg{ + FromAddress: addr1.String(), + ToAddress: invalidAddr, + Amount: []wasmTypes.Coin{ + { + Denom: "uatom", + Amount: "7890", + }, + }, + }, + }, + }, + isError: true, + }, + "wasm execute": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Wasm: &wasmTypes.WasmMsg{ + Execute: &wasmTypes.ExecuteMsg{ + ContractAddr: addr2.String(), + Msg: jsonMsg, + Send: []wasmTypes.Coin{ + wasmTypes.NewCoin(12, "eth"), + }, + }, + }, + }, + output: []sdk.Msg{ + types.MsgExecuteContract{ + Sender: addr1, + Contract: addr2, + Msg: jsonMsg, + SentFunds: sdk.NewCoins(sdk.NewInt64Coin("eth", 12)), + }, + }, + }, + "wasm instantiate": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Wasm: &wasmTypes.WasmMsg{ + Instantiate: &wasmTypes.InstantiateMsg{ + CodeID: 7, + Msg: jsonMsg, + Send: []wasmTypes.Coin{ + wasmTypes.NewCoin(123, "eth"), + }, + }, + }, + }, + output: []sdk.Msg{ + types.MsgInstantiateContract{ + Sender: addr1, + CodeID: 7, + // TODO: fix this + Label: fmt.Sprintf("Auto-created by %s", addr1), + InitMsg: jsonMsg, + InitFunds: sdk.NewCoins(sdk.NewInt64Coin("eth", 123)), + }, + }, + }, + "staking delegate": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Staking: &wasmTypes.StakingMsg{ + Delegate: &wasmTypes.DelegateMsg{ + Validator: valAddr.String(), + Amount: wasmTypes.NewCoin(777, "stake"), + }, + }, + }, + output: []sdk.Msg{ + staking.MsgDelegate{ + DelegatorAddress: addr1, + ValidatorAddress: valAddr, + Amount: sdk.NewInt64Coin("stake", 777), + }, + }, + }, + "staking delegate to non-validator": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Staking: &wasmTypes.StakingMsg{ + Delegate: &wasmTypes.DelegateMsg{ + Validator: addr2.String(), + Amount: wasmTypes.NewCoin(777, "stake"), + }, + }, + }, + isError: true, + }, + "staking undelegate": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Staking: &wasmTypes.StakingMsg{ + Undelegate: &wasmTypes.UndelegateMsg{ + Validator: valAddr.String(), + Amount: wasmTypes.NewCoin(555, "stake"), + }, + }, + }, + output: []sdk.Msg{ + staking.MsgUndelegate{ + DelegatorAddress: addr1, + ValidatorAddress: valAddr, + Amount: sdk.NewInt64Coin("stake", 555), + }, + }, + }, + "staking redelegate": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Staking: &wasmTypes.StakingMsg{ + Redelegate: &wasmTypes.RedelegateMsg{ + SrcValidator: valAddr.String(), + DstValidator: valAddr2.String(), + Amount: wasmTypes.NewCoin(222, "stake"), + }, + }, + }, + output: []sdk.Msg{ + staking.MsgBeginRedelegate{ + DelegatorAddress: addr1, + ValidatorSrcAddress: valAddr, + ValidatorDstAddress: valAddr2, + Amount: sdk.NewInt64Coin("stake", 222), + }, + }, + }, + "staking withdraw (implicit recipient)": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Staking: &wasmTypes.StakingMsg{ + Withdraw: &wasmTypes.WithdrawMsg{ + Validator: valAddr2.String(), + }, + }, + }, + output: []sdk.Msg{ + distribution.MsgSetWithdrawAddress{ + DelegatorAddress: addr1, + WithdrawAddress: addr1, + }, + distribution.MsgWithdrawDelegatorReward{ + DelegatorAddress: addr1, + ValidatorAddress: valAddr2, + }, + }, + }, + "staking withdraw (explicit recipient)": { + sender: addr1, + input: wasmTypes.CosmosMsg{ + Staking: &wasmTypes.StakingMsg{ + Withdraw: &wasmTypes.WithdrawMsg{ + Validator: valAddr2.String(), + Recipient: addr2.String(), + }, + }, + }, + output: []sdk.Msg{ + distribution.MsgSetWithdrawAddress{ + DelegatorAddress: addr1, + WithdrawAddress: addr2, + }, + distribution.MsgWithdrawDelegatorReward{ + DelegatorAddress: addr1, + ValidatorAddress: valAddr2, + }, + }, + }, + } + + encoder := DefaultEncoders() + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + res, err := encoder.Encode(tc.sender, tc.input, nil) + if tc.isError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.output, res) + } + }) + } +} diff --git a/x/wasm/internal/keeper/ioutil.go b/x/wasm/internal/keeper/ioutil.go new file mode 100644 index 0000000000..caeedc1764 --- /dev/null +++ b/x/wasm/internal/keeper/ioutil.go @@ -0,0 +1,53 @@ +package keeper + +import ( + "bytes" + "compress/gzip" + "io" + "io/ioutil" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +// 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 +var gzipIdent = []byte("\x1F\x8B\x08") + +// uncompress returns gzip uncompressed content or given src when not gzip. +func uncompress(src []byte, limit uint64) ([]byte, error) { + switch n := uint64(len(src)); { + case n < 3: + return src, nil + case n > limit: + return nil, types.ErrLimit + } + if !bytes.Equal(gzipIdent, src[0:3]) { + return src, nil + } + zr, err := gzip.NewReader(bytes.NewReader(src)) + if err != nil { + return nil, err + } + zr.Multistream(false) + defer zr.Close() + return ioutil.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) +} diff --git a/x/wasm/internal/keeper/ioutil_test.go b/x/wasm/internal/keeper/ioutil_test.go new file mode 100644 index 0000000000..50828f3cf8 --- /dev/null +++ b/x/wasm/internal/keeper/ioutil_test.go @@ -0,0 +1,103 @@ +// nolint: scopelint +package keeper + +import ( + "bytes" + "compress/gzip" + "errors" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUncompress(t *testing.T) { + wasmRaw, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + wasmGzipped, err := ioutil.ReadFile("./testdata/hackatom.wasm.gzip") + require.NoError(t, err) + + const maxSize = 400_000 + + specs := map[string]struct { + src []byte + expError error + expResult []byte + }{ + "handle wasm uncompressed": { + src: wasmRaw, + expResult: wasmRaw, + }, + "handle wasm compressed": { + src: wasmGzipped, + expResult: wasmRaw, + }, + "handle nil slice": { + src: nil, + expResult: nil, + }, + "handle short unidentified": { + src: []byte{0x1, 0x2}, + expResult: []byte{0x1, 0x2}, + }, + "handle input slice exceeding limit": { + src: []byte(strings.Repeat("a", maxSize+1)), + expError: types.ErrLimit, + }, + "handle input slice at limit": { + src: []byte(strings.Repeat("a", maxSize)), + expResult: []byte(strings.Repeat("a", maxSize)), + }, + "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() +} diff --git a/x/wasm/internal/keeper/keeper.go b/x/wasm/internal/keeper/keeper.go new file mode 100644 index 0000000000..9476059100 --- /dev/null +++ b/x/wasm/internal/keeper/keeper.go @@ -0,0 +1,678 @@ +package keeper + +import ( + "bytes" + "encoding/binary" + "path/filepath" + + wasm "github.com/CosmWasm/wasmvm" + wasmTypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/codec" + "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/x/auth" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/params/subspace" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/crypto" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +// 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 +// 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) +// +// Please not that all gas prices returned to the wasmer engine should have this multiplied +const GasMultiplier uint64 = 100 + +// MaxGas for a contract is 10 billion wasmer gas (enforced in rust to prevent overflow) +// The limit for v0.9.3 is defined here: https://github.com/CosmWasm/cosmwasm/blob/v0.9.3/packages/vm/src/backends/singlepass.rs#L15-L23 +// (this will be increased in future releases) +const MaxGas = 10_000_000_000 + +// InstanceCost 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. +const InstanceCost uint64 = 40_000 + +// CompileCost is how much SDK gas we charge *per byte* for compiling WASM code. +const CompileCost uint64 = 2 + +// Keeper will have a reference to Wasmer with it's own data directory. +type Keeper struct { + storeKey sdk.StoreKey + cdc *codec.Codec + accountKeeper auth.AccountKeeper + bankKeeper types.BankKeeper + + wasmer wasm.VM + queryPlugins QueryPlugins + messenger MessageHandler + // queryGasLimit is the max wasm gas that can be spent on executing a query with a contract + queryGasLimit uint64 + authZPolicy AuthorizationPolicy + paramSpace subspace.Subspace +} + +// 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 params.Subspace, accountKeeper auth.AccountKeeper, bankKeeper types.BankKeeper, + stakingKeeper staking.Keeper, distKeeper distribution.Keeper, + router sdk.Router, encodeRouter types.Router, queryRouter types.QueryRouter, homeDir string, wasmConfig types.WasmConfig, supportedFeatures string, customEncoders *MessageEncoders, customPlugins *QueryPlugins) Keeper { + wasmer, err := wasm.NewVM(filepath.Join(homeDir, "wasm"), supportedFeatures, 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, + wasmer: *wasmer, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, + messenger: NewMessageHandler(router, encodeRouter, customEncoders), + queryGasLimit: wasmConfig.SmartQueryGasLimit, + authZPolicy: DefaultAuthorizationPolicy{}, + paramSpace: paramSpace, + } + keeper.queryPlugins = DefaultQueryPlugins(bankKeeper, stakingKeeper, distKeeper, queryRouter, &keeper).Merge(customPlugins) + return keeper +} + +func (k Keeper) getUploadAccessConfig(ctx sdk.Context) types.AccessConfig { + var a types.AccessConfig + k.paramSpace.Get(ctx, types.ParamStoreKeyUploadAccess, &a) + return a +} + +func (k Keeper) getInstantiateAccessConfig(ctx sdk.Context) types.AccessType { + var a types.AccessType + k.paramSpace.Get(ctx, types.ParamStoreKeyInstantiateAccess, &a) + return a +} + +func (k Keeper) GetMaxWasmCodeSize(ctx sdk.Context) uint64 { + var a uint64 + k.paramSpace.Get(ctx, types.ParamStoreKeyMaxWasmCodeSize, &a) + return a +} + +// GetParams returns the total set of wasm parameters. +func (k Keeper) GetParams(ctx sdk.Context) types.Params { + var params types.Params + k.paramSpace.GetParamSet(ctx, ¶ms) + return params +} + +func (k Keeper) setParams(ctx sdk.Context, ps types.Params) { + k.paramSpace.SetParamSet(ctx, &ps) +} + +// Create uploads and compiles a WASM contract, returning a short identifier for the contract +func (k Keeper) Create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, source string, builder string, instantiateAccess *types.AccessConfig) (codeID uint64, err error) { + return k.create(ctx, creator, wasmCode, source, builder, instantiateAccess, k.authZPolicy) +} + +func (k Keeper) create(ctx sdk.Context, creator sdk.AccAddress, wasmCode []byte, source string, builder string, instantiateAccess *types.AccessConfig, authZ AuthorizationPolicy) (codeID uint64, err error) { + if !authZ.CanCreateCode(k.getUploadAccessConfig(ctx), creator) { + return 0, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not create code") + } + wasmCode, err = uncompress(wasmCode, k.GetMaxWasmCodeSize(ctx)) + if err != nil { + return 0, sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + ctx.GasMeter().ConsumeGas(CompileCost*uint64(len(wasmCode)), "Compiling WASM Bytecode") + + codeHash, err := k.wasmer.Create(wasmCode) + if err != nil { + // return 0, sdkerrors.Wrap(err, "cosmwasm create") + return 0, sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + store := ctx.KVStore(k.storeKey) + codeID = k.autoIncrementID(ctx, types.KeyLastCodeID) + if instantiateAccess == nil { + defaultAccessConfig := k.getInstantiateAccessConfig(ctx).With(creator) + instantiateAccess = &defaultAccessConfig + } + codeInfo := types.NewCodeInfo(codeHash, creator, source, builder, *instantiateAccess) + // 0x01 | codeID (uint64) -> ContractInfo + store.Set(types.GetCodeKey(codeID), k.cdc.MustMarshalBinaryBare(codeInfo)) + + return codeID, nil +} + +func (k Keeper) importCode(ctx sdk.Context, codeID uint64, codeInfo types.CodeInfo, wasmCode []byte) error { + wasmCode, err := uncompress(wasmCode, k.GetMaxWasmCodeSize(ctx)) + if err != nil { + return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + newCodeHash, err := k.wasmer.Create(wasmCode) + if err != nil { + return sdkerrors.Wrap(types.ErrCreateFailed, err.Error()) + } + if !bytes.Equal(codeInfo.CodeHash, newCodeHash) { + return sdkerrors.Wrap(types.ErrInvalid, "code hashes not same") + } + + store := ctx.KVStore(k.storeKey) + key := types.GetCodeKey(codeID) + if store.Has(key) { + return sdkerrors.Wrapf(types.ErrDuplicate, "duplicate code: %d", codeID) + } + // 0x01 | codeID (uint64) -> ContractInfo + store.Set(key, k.cdc.MustMarshalBinaryBare(codeInfo)) + return nil +} + +// Instantiate creates an instance of a WASM contract +func (k Keeper) Instantiate(ctx sdk.Context, codeID uint64, creator, admin sdk.AccAddress, initMsg []byte, label string, deposit sdk.Coins) (sdk.AccAddress, error) { + return k.instantiate(ctx, codeID, creator, admin, initMsg, label, deposit, k.authZPolicy) +} + +func (k Keeper) instantiate(ctx sdk.Context, codeID uint64, creator, admin sdk.AccAddress, initMsg []byte, label string, deposit sdk.Coins, authZ AuthorizationPolicy) (sdk.AccAddress, error) { + ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: init") + + // create contract address + contractAddress := k.generateContractAddress(ctx, codeID) + existingAcct := k.accountKeeper.GetAccount(ctx, contractAddress) + if existingAcct != nil { + return nil, sdkerrors.Wrap(types.ErrAccountExists, existingAcct.GetAddress().String()) + } + + // deposit initial contract funds + if !deposit.IsZero() { + if k.bankKeeper.BlacklistedAddr(creator) { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "blocked address can not be used") + } + sdkerr := k.bankKeeper.SendCoins(ctx, creator, contractAddress, deposit) + if sdkerr != nil { + return nil, sdkerr + } + } else { + // create an empty account (so we don't have issues later) + // TODO: can we remove this? + contractAccount := k.accountKeeper.NewAccountWithAddress(ctx, contractAddress) + k.accountKeeper.SetAccount(ctx, contractAccount) + } + + // get contact info + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.GetCodeKey(codeID)) + if bz == nil { + return nil, sdkerrors.Wrap(types.ErrNotFound, "code") + } + var codeInfo types.CodeInfo + k.cdc.MustUnmarshalBinaryBare(bz, &codeInfo) + + if !authZ.CanInstantiateContract(codeInfo.InstantiateConfig, creator) { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not instantiate") + } + + // prepare params for contract instantiate call + env := types.NewEnv(ctx, contractAddress) + info := types.NewInfo(creator, deposit) + + // create prefixed data store + // 0x03 | contractAddress (sdk.AccAddress) + prefixStoreKey := types.GetContractStorePrefixKey(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + + // prepare querier + querier := QueryHandler{ + Ctx: ctx, + Plugins: k.queryPlugins, + } + + // instantiate wasm contract + gas := gasForContract(ctx) + res, gasUsed, err := k.wasmer.Instantiate(codeInfo.CodeHash, env, info, initMsg, prefixStore, cosmwasmAPI, querier, gasMeter(ctx), gas) + consumeGas(ctx, gasUsed) + if err != nil { + return contractAddress, sdkerrors.Wrap(types.ErrInstantiateFailed, err.Error()) + } + + // emit all events from this contract itself + events := types.ParseEvents(res.Attributes, contractAddress) + ctx.EventManager().EmitEvents(events) + + err = k.dispatchMessages(ctx, contractAddress, res.Messages) + if err != nil { + return nil, err + } + + // persist instance + createdAt := types.NewAbsoluteTxPosition(ctx) + instance := types.NewContractInfo(codeID, creator, admin, label, createdAt) + store.Set(types.GetContractAddressKey(contractAddress), k.cdc.MustMarshalBinaryBare(instance)) + k.appendToContractHistory(ctx, contractAddress, instance.InitialHistory(initMsg)) + return contractAddress, nil +} + +// Execute executes the contract instance +func (k Keeper) Execute(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, msg []byte, coins sdk.Coins) (*sdk.Result, error) { + ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: execute") + + codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddress) + if err != nil { + return nil, err + } + + // add more funds + if !coins.IsZero() { + if k.bankKeeper.BlacklistedAddr(caller) { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "blocked address can not be used") + } + + sdkerr := k.bankKeeper.SendCoins(ctx, caller, contractAddress, coins) + if sdkerr != nil { + return nil, sdkerr + } + } + + env := types.NewEnv(ctx, contractAddress) + info := types.NewInfo(caller, coins) + + // prepare querier + querier := QueryHandler{ + Ctx: ctx, + Plugins: k.queryPlugins, + } + + gas := gasForContract(ctx) + res, gasUsed, execErr := k.wasmer.Execute(codeInfo.CodeHash, env, info, msg, prefixStore, cosmwasmAPI, querier, gasMeter(ctx), gas) + consumeGas(ctx, gasUsed) + if execErr != nil { + return nil, sdkerrors.Wrap(types.ErrExecuteFailed, execErr.Error()) + } + + // emit all events from this contract itself + events := types.ParseEvents(res.Attributes, contractAddress) + ctx.EventManager().EmitEvents(events) + + err = k.dispatchMessages(ctx, contractAddress, res.Messages) + if err != nil { + return nil, err + } + + return &sdk.Result{ + Data: res.Data, + }, nil +} + +// Migrate allows to upgrade a contract to a new code with data migration. +func (k Keeper) Migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte) (*sdk.Result, error) { + return k.migrate(ctx, contractAddress, caller, newCodeID, msg, k.authZPolicy) +} + +func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newCodeID uint64, msg []byte, authZ AuthorizationPolicy) (*sdk.Result, error) { + ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: migrate") + + contractInfo := k.GetContractInfo(ctx, contractAddress) + if contractInfo == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract") + } + if !authZ.CanModifyContract(contractInfo.Admin, caller) { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not migrate") + } + + newCodeInfo := k.GetCodeInfo(ctx, newCodeID) + if newCodeInfo == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown code") + } + + var noDeposit sdk.Coins + env := types.NewEnv(ctx, contractAddress) + info := types.NewInfo(caller, noDeposit) + + // prepare querier + querier := QueryHandler{ + Ctx: ctx, + Plugins: k.queryPlugins, + } + + prefixStoreKey := types.GetContractStorePrefixKey(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + gas := gasForContract(ctx) + res, gasUsed, err := k.wasmer.Migrate(newCodeInfo.CodeHash, env, info, msg, &prefixStore, cosmwasmAPI, &querier, gasMeter(ctx), gas) + consumeGas(ctx, gasUsed) + if err != nil { + return nil, sdkerrors.Wrap(types.ErrMigrationFailed, err.Error()) + } + + // emit all events from this contract itself + events := types.ParseEvents(res.Attributes, contractAddress) + ctx.EventManager().EmitEvents(events) + + historyEntry := contractInfo.AddMigration(ctx, newCodeID, msg) + k.appendToContractHistory(ctx, contractAddress, historyEntry) + k.setContractInfo(ctx, contractAddress, contractInfo) + + if err := k.dispatchMessages(ctx, contractAddress, res.Messages); err != nil { + return nil, sdkerrors.Wrap(err, "dispatch") + } + + return &sdk.Result{ + Data: res.Data, + }, nil +} + +// UpdateContractAdmin sets the admin value on the ContractInfo. It must be a valid address (use ClearContractAdmin to remove it) +func (k Keeper) UpdateContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress, newAdmin sdk.AccAddress) error { + return k.setContractAdmin(ctx, contractAddress, caller, newAdmin, k.authZPolicy) +} + +// ClearContractAdmin sets the admin value on the ContractInfo to nil, to disable further migrations/ updates. +func (k Keeper) ClearContractAdmin(ctx sdk.Context, contractAddress sdk.AccAddress, caller sdk.AccAddress) error { + return k.setContractAdmin(ctx, contractAddress, caller, nil, k.authZPolicy) +} + +func (k Keeper) setContractAdmin(ctx sdk.Context, contractAddress, caller, newAdmin sdk.AccAddress, authZ AuthorizationPolicy) error { + contractInfo := k.GetContractInfo(ctx, contractAddress) + if contractInfo == nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unknown contract") + } + if !authZ.CanModifyContract(contractInfo.Admin, caller) { + return sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "can not modify contract") + } + contractInfo.Admin = newAdmin + k.setContractInfo(ctx, contractAddress, contractInfo) + return nil +} + +func (k Keeper) appendToContractHistory(ctx sdk.Context, contractAddr sdk.AccAddress, newEntries ...types.ContractCodeHistoryEntry) { + entries := append(k.GetContractHistory(ctx, contractAddr), newEntries...) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.ContractHistoryStorePrefix) + prefixStore.Set(contractAddr, k.cdc.MustMarshalBinaryBare(&entries)) +} + +func (k Keeper) GetContractHistory(ctx sdk.Context, contractAddr sdk.AccAddress) []types.ContractCodeHistoryEntry { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.ContractHistoryStorePrefix) + var entries []types.ContractCodeHistoryEntry + bz := prefixStore.Get(contractAddr) + if bz != nil { + k.cdc.MustUnmarshalBinaryBare(bz, &entries) + } + return entries +} + +// QuerySmart queries the smart contract itself. +func (k Keeper) QuerySmart(ctx sdk.Context, contractAddr sdk.AccAddress, req []byte) ([]byte, error) { + ctx.GasMeter().ConsumeGas(InstanceCost, "Loading CosmWasm module: query") + + codeInfo, prefixStore, err := k.contractInstance(ctx, contractAddr) + if err != nil { + return nil, err + } + // prepare querier + querier := QueryHandler{ + Ctx: ctx, + Plugins: k.queryPlugins, + } + + env := types.NewEnv(ctx, contractAddr) + queryResult, gasUsed, qErr := k.wasmer.Query(codeInfo.CodeHash, env, req, prefixStore, cosmwasmAPI, querier, gasMeter(ctx), gasForContract(ctx)) + consumeGas(ctx, gasUsed) + if qErr != nil { + return nil, sdkerrors.Wrap(types.ErrQueryFailed, qErr.Error()) + } + return queryResult, nil +} + +// QueryRaw returns the contract's state for give key. Returns `nil` when key is `nil`. +func (k Keeper) QueryRaw(ctx sdk.Context, contractAddress sdk.AccAddress, key []byte) []byte { + if key == nil { + return nil + } + prefixStoreKey := types.GetContractStorePrefixKey(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + return prefixStore.Get(key) +} + +func (k Keeper) contractInstance(ctx sdk.Context, contractAddress sdk.AccAddress) (types.CodeInfo, prefix.Store, error) { + store := ctx.KVStore(k.storeKey) + + contractBz := store.Get(types.GetContractAddressKey(contractAddress)) + if contractBz == nil { + return types.CodeInfo{}, prefix.Store{}, sdkerrors.Wrap(types.ErrNotFound, "contract") + } + var contract types.ContractInfo + k.cdc.MustUnmarshalBinaryBare(contractBz, &contract) + + contractInfoBz := store.Get(types.GetCodeKey(contract.CodeID)) + if contractInfoBz == nil { + return types.CodeInfo{}, prefix.Store{}, sdkerrors.Wrap(types.ErrNotFound, "contract info") + } + var codeInfo types.CodeInfo + k.cdc.MustUnmarshalBinaryBare(contractInfoBz, &codeInfo) + prefixStoreKey := types.GetContractStorePrefixKey(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + return codeInfo, prefixStore, nil +} + +func (k Keeper) GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) *types.ContractInfo { + store := ctx.KVStore(k.storeKey) + var contract types.ContractInfo + contractBz := store.Get(types.GetContractAddressKey(contractAddress)) + if contractBz == nil { + return nil + } + k.cdc.MustUnmarshalBinaryBare(contractBz, &contract) + return &contract +} + +func (k Keeper) containsContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) bool { + store := ctx.KVStore(k.storeKey) + return store.Has(types.GetContractAddressKey(contractAddress)) +} + +func (k Keeper) setContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress, contract *types.ContractInfo) { + store := ctx.KVStore(k.storeKey) + store.Set(types.GetContractAddressKey(contractAddress), k.cdc.MustMarshalBinaryBare(contract)) +} + +func (k Keeper) IterateContractInfo(ctx sdk.Context, cb func(sdk.AccAddress, types.ContractInfo) bool) { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.ContractKeyPrefix) + iter := prefixStore.Iterator(nil, nil) + for ; iter.Valid(); iter.Next() { + var contract types.ContractInfo + k.cdc.MustUnmarshalBinaryBare(iter.Value(), &contract) + // cb returns true to stop early + if cb(iter.Key(), contract) { + break + } + } +} + +func (k Keeper) GetContractState(ctx sdk.Context, contractAddress sdk.AccAddress) sdk.Iterator { + prefixStoreKey := types.GetContractStorePrefixKey(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + return prefixStore.Iterator(nil, nil) +} + +func (k Keeper) importContractState(ctx sdk.Context, contractAddress sdk.AccAddress, models []types.Model) error { + prefixStoreKey := types.GetContractStorePrefixKey(contractAddress) + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey) + for _, model := range models { + if model.Value == nil { + model.Value = []byte{} + } + if prefixStore.Has(model.Key) { + return sdkerrors.Wrapf(types.ErrDuplicate, "duplicate key: %x", model.Key) + } + prefixStore.Set(model.Key, model.Value) + } + return nil +} + +func (k Keeper) GetCodeInfo(ctx sdk.Context, codeID uint64) *types.CodeInfo { + store := ctx.KVStore(k.storeKey) + var codeInfo types.CodeInfo + codeInfoBz := store.Get(types.GetCodeKey(codeID)) + if codeInfoBz == nil { + return nil + } + k.cdc.MustUnmarshalBinaryBare(codeInfoBz, &codeInfo) + return &codeInfo +} + +func (k Keeper) containsCodeInfo(ctx sdk.Context, codeID uint64) bool { + store := ctx.KVStore(k.storeKey) + return store.Has(types.GetCodeKey(codeID)) +} + +func (k Keeper) IterateCodeInfos(ctx sdk.Context, cb func(uint64, types.CodeInfo) bool) { + prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.CodeKeyPrefix) + iter := prefixStore.Iterator(nil, nil) + for ; iter.Valid(); iter.Next() { + var c types.CodeInfo + k.cdc.MustUnmarshalBinaryBare(iter.Value(), &c) + // cb returns true to stop early + if cb(binary.BigEndian.Uint64(iter.Key()), c) { + return + } + } +} + +func (k Keeper) GetByteCode(ctx sdk.Context, codeID uint64) ([]byte, error) { + store := ctx.KVStore(k.storeKey) + var codeInfo types.CodeInfo + codeInfoBz := store.Get(types.GetCodeKey(codeID)) + if codeInfoBz == nil { + return nil, nil + } + k.cdc.MustUnmarshalBinaryBare(codeInfoBz, &codeInfo) + return k.wasmer.GetCode(codeInfo.CodeHash) +} + +func (k Keeper) dispatchMessages(ctx sdk.Context, contractAddr sdk.AccAddress, msgs []wasmTypes.CosmosMsg) error { + for _, msg := range msgs { + if err := k.messenger.Dispatch(ctx, contractAddr, msg, k.messenger.encodeRouter); err != nil { + return err + } + } + return nil +} + +func gasForContract(ctx sdk.Context) uint64 { + meter := ctx.GasMeter() + if meter.IsOutOfGas() { + return 0 + } + remaining := (meter.Limit() - meter.GasConsumedToLimit()) * GasMultiplier + if remaining > MaxGas { + return MaxGas + } + return remaining +} + +func consumeGas(ctx sdk.Context, gas uint64) { + consumed := gas / GasMultiplier + ctx.GasMeter().ConsumeGas(consumed, "wasm contract") + // throw OutOfGas error if we ran out (got exactly to zero due to better limit enforcing) + if ctx.GasMeter().IsOutOfGas() { + panic(sdk.ErrorOutOfGas{Descriptor: "Wasmer function execution"}) + } +} + +// generates a contract address from codeID + instanceID +func (k Keeper) generateContractAddress(ctx sdk.Context, codeID uint64) sdk.AccAddress { + instanceID := k.autoIncrementID(ctx, types.KeyLastInstanceID) + return contractAddress(codeID, instanceID) +} + +func contractAddress(codeID, instanceID uint64) sdk.AccAddress { + // NOTE: It is possible to get a duplicate address if either codeID or instanceID + // overflow 32 bits. This is highly improbable, but something that could be refactored. + contractID := codeID<<32 + instanceID + return addrFromUint64(contractID) +} + +func (k Keeper) GetNextCodeID(ctx sdk.Context) uint64 { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.KeyLastCodeID) + id := uint64(1) + if bz != nil { + id = binary.BigEndian.Uint64(bz) + } + return id +} + +func (k Keeper) autoIncrementID(ctx sdk.Context, lastIDKey []byte) uint64 { + store := ctx.KVStore(k.storeKey) + bz := store.Get(lastIDKey) + id := uint64(1) + if bz != nil { + id = binary.BigEndian.Uint64(bz) + } + bz = sdk.Uint64ToBigEndian(id + 1) + store.Set(lastIDKey, bz) + return id +} + +// peekAutoIncrementID reads the current value without incrementing it. +func (k Keeper) peekAutoIncrementID(ctx sdk.Context, lastIDKey []byte) uint64 { + store := ctx.KVStore(k.storeKey) + bz := store.Get(lastIDKey) + id := uint64(1) + if bz != nil { + id = binary.BigEndian.Uint64(bz) + } + return id +} + +func (k Keeper) importAutoIncrementID(ctx sdk.Context, lastIDKey []byte, val uint64) error { + store := ctx.KVStore(k.storeKey) + if store.Has(lastIDKey) { + return sdkerrors.Wrapf(types.ErrDuplicate, "autoincrement id: %s", string(lastIDKey)) + } + bz := sdk.Uint64ToBigEndian(val) + store.Set(lastIDKey, bz) + return nil +} + +func (k Keeper) importContract(ctx sdk.Context, contractAddr sdk.AccAddress, c *types.ContractInfo, state []types.Model) error { + if !k.containsCodeInfo(ctx, c.CodeID) { + return errors.Wrapf(types.ErrNotFound, "code id: %d", c.CodeID) + } + if k.containsContractInfo(ctx, contractAddr) { + return errors.Wrapf(types.ErrDuplicate, "contract: %s", contractAddr) + } + + historyEntry := c.ResetFromGenesis(ctx) + k.appendToContractHistory(ctx, contractAddr, historyEntry) + k.setContractInfo(ctx, contractAddr, c) + return k.importContractState(ctx, contractAddr, state) +} + +func addrFromUint64(id uint64) sdk.AccAddress { + addr := make([]byte, 20) + addr[0] = 'C' + binary.PutUvarint(addr[1:], id) + return sdk.AccAddress(crypto.AddressHash(addr)) +} + +// MultipliedGasMeter wraps the GasMeter from context and multiplies all reads by out defined multiplier +type MultipiedGasMeter struct { + originalMeter sdk.GasMeter +} + +var _ wasm.GasMeter = MultipiedGasMeter{} + +func (m MultipiedGasMeter) GasConsumed() sdk.Gas { + return m.originalMeter.GasConsumed() * GasMultiplier +} + +func gasMeter(ctx sdk.Context) MultipiedGasMeter { + return MultipiedGasMeter{ + originalMeter: ctx.GasMeter(), + } +} diff --git a/x/wasm/internal/keeper/keeper_test.go b/x/wasm/internal/keeper/keeper_test.go new file mode 100644 index 0000000000..d298ea24cf --- /dev/null +++ b/x/wasm/internal/keeper/keeper_test.go @@ -0,0 +1,1203 @@ +// nolint: unparam, ineffassign, dupl, staticcheck, errcheck, scopelint +package keeper + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "errors" + "io/ioutil" + "os" + "testing" + "time" + + stypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" +) + +const SupportedFeatures = "staking" + +func TestNewKeeper(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + _, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + require.NotNil(t, keepers.WasmKeeper) +} + +func TestCreate(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "any/builder:tag", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + // and verify content + storedCode, err := keeper.GetByteCode(ctx, contractID) + require.NoError(t, err) + require.Equal(t, wasmCode, storedCode) +} + +func TestCreateStoresInstantiatePermission(t *testing.T) { + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + var ( + deposit = sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + myAddr = bytes.Repeat([]byte{1}, sdk.AddrLen) + ) + + specs := map[string]struct { + srcPermission types.AccessType + expInstConf types.AccessConfig + }{ + "default": { + srcPermission: types.DefaultParams().DefaultInstantiatePermission, + expInstConf: types.AllowEverybody, + }, + "everybody": { + srcPermission: types.Everybody, + expInstConf: types.AllowEverybody, + }, + "nobody": { + srcPermission: types.Nobody, + expInstConf: types.AllowNobody, + }, + "onlyAddress with matching address": { + srcPermission: types.OnlyAddress, + expInstConf: types.AccessConfig{Type: types.OnlyAddress, Address: myAddr}, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + keeper.setParams(ctx, types.Params{ + UploadAccess: types.AllowEverybody, + DefaultInstantiatePermission: spec.srcPermission, + MaxWasmCodeSize: types.DefaultMaxWasmCodeSize, + }) + fundAccounts(ctx, accKeeper, myAddr, deposit) + + codeID, err := keeper.Create(ctx, myAddr, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "any/builder:tag", nil) + require.NoError(t, err) + + codeInfo := keeper.GetCodeInfo(ctx, codeID) + require.NotNil(t, codeInfo) + assert.True(t, spec.expInstConf.Equals(codeInfo.InstantiateConfig), "got %#v", codeInfo.InstantiateConfig) + }) + } +} + +func TestCreateWithParamPermissions(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + otherAddr := createFakeFundedAccount(ctx, accKeeper, deposit) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + specs := map[string]struct { + srcPermission types.AccessConfig + expError *sdkerrors.Error + }{ + "default": { + srcPermission: types.DefaultUploadAccess, + }, + "everybody": { + srcPermission: types.AllowEverybody, + }, + "nobody": { + srcPermission: types.AllowNobody, + expError: sdkerrors.ErrUnauthorized, + }, + "onlyAddress with matching address": { + srcPermission: types.OnlyAddress.With(creator), + }, + "onlyAddress with non matching address": { + srcPermission: types.OnlyAddress.With(otherAddr), + expError: sdkerrors.ErrUnauthorized, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + params := types.DefaultParams() + params.UploadAccess = spec.srcPermission + keeper.setParams(ctx, params) + _, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "any/builder:tag", nil) + require.True(t, spec.expError.Is(err), err) + if spec.expError != nil { + return + } + }) + } +} + +func TestCreateDuplicate(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + // create one copy + contractID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "any/builder:tag", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + + // create second copy + duplicateID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "any/builder:tag", nil) + require.NoError(t, err) + require.Equal(t, uint64(2), duplicateID) + + // and verify both content is proper + storedCode, err := keeper.GetByteCode(ctx, contractID) + require.NoError(t, err) + require.Equal(t, wasmCode, storedCode) + storedCode, err = keeper.GetByteCode(ctx, duplicateID) + require.NoError(t, err) + require.Equal(t, wasmCode, storedCode) +} + +func TestCreateWithSimulation(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + ctx = ctx.WithBlockHeader(abci.Header{Height: 1}). + WithGasMeter(stypes.NewInfiniteGasMeter()) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + // create this once in simulation mode + contractID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "any/builder:tag", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + + // then try to create it in non-simulation mode (should not fail) + ctx, keepers = CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper = keepers.AccountKeeper, keepers.WasmKeeper + contractID, err = keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "any/builder:tag", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + + // and verify content + code, err := keeper.GetByteCode(ctx, contractID) + require.NoError(t, err) + require.Equal(t, code, wasmCode) +} + +func TestIsSimulationMode(t *testing.T) { + specs := map[string]struct { + ctx sdk.Context + exp bool + }{ + "genesis block": { + ctx: sdk.Context{}.WithBlockHeader(abci.Header{}).WithGasMeter(stypes.NewInfiniteGasMeter()), + exp: false, + }, + "any regular block": { + ctx: sdk.Context{}.WithBlockHeader(abci.Header{Height: 1}).WithGasMeter(stypes.NewGasMeter(10000000)), + exp: false, + }, + "simulation": { + ctx: sdk.Context{}.WithBlockHeader(abci.Header{Height: 1}).WithGasMeter(stypes.NewInfiniteGasMeter()), + exp: true, + }, + } + for msg := range specs { + t.Run(msg, func(t *testing.T) { + // assert.Equal(t, spec.exp, isSimulationMode(spec.ctx)) + }) + } +} + +func TestCreateWithGzippedPayload(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm.gzip") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), contractID) + // and verify content + storedCode, err := keeper.GetByteCode(ctx, contractID) + require.NoError(t, err) + rawCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + require.Equal(t, rawCode, storedCode) +} + +func TestInstantiate(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + codeID, err := keeper.Create(ctx, creator, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "", nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + _, _, fred := keyPubAddr() + + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + gasBefore := ctx.GasMeter().GasConsumed() + + // create with no balance is also legal + contractAddr, err := keeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, "demo contract 1", nil) + require.NoError(t, err) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", contractAddr.String()) + + gasAfter := ctx.GasMeter().GasConsumed() + require.Equal(t, uint64(0x10c50), gasAfter-gasBefore) + + // ensure it is stored properly + info := keeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, info) + assert.Equal(t, info.Creator, creator) + assert.Equal(t, info.CodeID, codeID) + assert.Equal(t, info.Label, "demo contract 1") + + exp := []types.ContractCodeHistoryEntry{{ + Operation: types.InitContractCodeHistoryType, + CodeID: codeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: json.RawMessage(initMsgBz), + }} + assert.Equal(t, exp, keeper.GetContractHistory(ctx, contractAddr)) +} + +func TestInstantiateWithDeposit(t *testing.T) { + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + var ( + bob = bytes.Repeat([]byte{1}, sdk.AddrLen) + fred = bytes.Repeat([]byte{2}, sdk.AddrLen) + + deposit = sdk.NewCoins(sdk.NewInt64Coin("denom", 100)) + initMsg = InitMsg{Verifier: fred, Beneficiary: bob} + ) + + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + specs := map[string]struct { + srcActor sdk.AccAddress + expError bool + fundAddr bool + }{ + "address with funds": { + srcActor: bob, + fundAddr: true, + }, + "address without funds": { + srcActor: bob, + expError: true, + }, + "blocked address": { + srcActor: supply.NewModuleAddress(auth.FeeCollectorName), + fundAddr: true, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + if spec.fundAddr { + fundAccounts(ctx, accKeeper, spec.srcActor, sdk.NewCoins(sdk.NewInt64Coin("denom", 200))) + } + contractID, err := keeper.Create(ctx, spec.srcActor, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "", nil) + require.NoError(t, err) + + // when + addr, err := keeper.Instantiate(ctx, contractID, spec.srcActor, nil, initMsgBz, "my label", deposit) + // then + if spec.expError { + require.Error(t, err) + return + } + require.NoError(t, err) + contractAccount := accKeeper.GetAccount(ctx, addr) + assert.Equal(t, deposit, contractAccount.GetCoins()) + }) + } +} + +func TestInstantiateWithPermissions(t *testing.T) { + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + var ( + deposit = sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + myAddr = bytes.Repeat([]byte{1}, sdk.AddrLen) + otherAddr = bytes.Repeat([]byte{2}, sdk.AddrLen) + anyAddr = bytes.Repeat([]byte{3}, sdk.AddrLen) + ) + + initMsg := InitMsg{ + Verifier: anyAddr, + Beneficiary: anyAddr, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + specs := map[string]struct { + srcPermission types.AccessConfig + srcActor sdk.AccAddress + expError *sdkerrors.Error + }{ + "default": { + srcPermission: types.DefaultUploadAccess, + srcActor: anyAddr, + }, + "everybody": { + srcPermission: types.AllowEverybody, + srcActor: anyAddr, + }, + "nobody": { + srcPermission: types.AllowNobody, + srcActor: myAddr, + expError: sdkerrors.ErrUnauthorized, + }, + "onlyAddress with matching address": { + srcPermission: types.OnlyAddress.With(myAddr), + srcActor: myAddr, + }, + "onlyAddress with non matching address": { + srcPermission: types.OnlyAddress.With(otherAddr), + expError: sdkerrors.ErrUnauthorized, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + fundAccounts(ctx, accKeeper, spec.srcActor, deposit) + + contractID, err := keeper.Create(ctx, myAddr, wasmCode, "https://github.com/CosmWasm/wasmd/blob/master/x/wasm/testdata/escrow.wasm", "", &spec.srcPermission) + require.NoError(t, err) + + _, err = keeper.Instantiate(ctx, contractID, spec.srcActor, nil, initMsgBz, "demo contract 1", nil) + assert.True(t, spec.expError.Is(err), "got %+v", err) + }) + } +} + +func TestInstantiateWithNonExistingCodeID(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + require.NoError(t, err) + + initMsg := InitMsg{} + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + const nonExistingCodeID = 9999 + addr, err := keeper.Instantiate(ctx, nonExistingCodeID, creator, nil, initMsgBz, "demo contract 2", nil) + require.True(t, types.ErrNotFound.Is(err), err) + require.Nil(t, addr) +} + +func TestExecute(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(ctx, accKeeper, topUp) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, err := keeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 3", deposit) + require.NoError(t, err) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", addr.String()) + + // ensure bob doesn't exist + bobAcct := accKeeper.GetAccount(ctx, bob) + require.Nil(t, bobAcct) + + // ensure funder has reduced balance + creatorAcct := accKeeper.GetAccount(ctx, creator) + require.NotNil(t, creatorAcct) + // we started at 2*deposit, should have spent one above + assert.Equal(t, deposit, creatorAcct.GetCoins()) + + // ensure contract has updated balance + contractAcct := accKeeper.GetAccount(ctx, addr) + require.NotNil(t, contractAcct) + assert.Equal(t, deposit, contractAcct.GetCoins()) + + // unauthorized - trialCtx so we don't change state + trialCtx := ctx.WithMultiStore(ctx.MultiStore().CacheWrap().(sdk.MultiStore)) + res, err := keeper.Execute(trialCtx, addr, creator, []byte(`{"release":{}}`), nil) + require.Error(t, err) + require.True(t, errors.Is(err, types.ErrExecuteFailed)) + require.Equal(t, err.Error(), "execute wasm contract failed: Unauthorized") + + // verifier can execute, and get proper gas amount + start := time.Now() + gasBefore := ctx.GasMeter().GasConsumed() + + res, err = keeper.Execute(ctx, addr, fred, []byte(`{"release":{}}`), topUp) + diff := time.Since(start) + require.NoError(t, err) + require.NotNil(t, res) + + // make sure gas is properly deducted from ctx + gasAfter := ctx.GasMeter().GasConsumed() + require.Equal(t, uint64(0x119f1), gasAfter-gasBefore) + + // ensure bob now exists and got both payments released + bobAcct = accKeeper.GetAccount(ctx, bob) + require.NotNil(t, bobAcct) + balance := bobAcct.GetCoins() + assert.Equal(t, deposit.Add(topUp...), balance) + + // ensure contract has updated balance + contractAcct = accKeeper.GetAccount(ctx, addr) + require.NotNil(t, contractAcct) + assert.Equal(t, sdk.Coins(nil), contractAcct.GetCoins()) + + t.Logf("Duration: %v (%d gas)\n", diff, gasAfter-gasBefore) +} + +func TestExecuteWithDeposit(t *testing.T) { + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + var ( + bob = bytes.Repeat([]byte{1}, sdk.AddrLen) + fred = bytes.Repeat([]byte{2}, sdk.AddrLen) + blockedAddr = supply.NewModuleAddress(auth.FeeCollectorName) + deposit = sdk.NewCoins(sdk.NewInt64Coin("denom", 100)) + ) + + specs := map[string]struct { + srcActor sdk.AccAddress + beneficiary sdk.AccAddress + expError bool + fundAddr bool + }{ + "actor with funds": { + srcActor: bob, + fundAddr: true, + beneficiary: fred, + }, + "actor without funds": { + srcActor: bob, + beneficiary: fred, + expError: true, + }, + "blocked address as actor": { + srcActor: blockedAddr, + fundAddr: true, + beneficiary: fred, + expError: true, + }, + "blocked address as beneficiary": { + srcActor: bob, + fundAddr: true, + beneficiary: blockedAddr, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + if spec.fundAddr { + fundAccounts(ctx, accKeeper, spec.srcActor, sdk.NewCoins(sdk.NewInt64Coin("denom", 200))) + } + codeID, err := keeper.Create(ctx, spec.srcActor, wasmCode, "https://example.com/escrow.wasm", "", nil) + require.NoError(t, err) + + initMsg := InitMsg{Verifier: spec.srcActor, Beneficiary: spec.beneficiary} + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + contractAddr, err := keeper.Instantiate(ctx, codeID, spec.srcActor, nil, initMsgBz, "my label", nil) + require.NoError(t, err) + + // when + _, err = keeper.Execute(ctx, contractAddr, spec.srcActor, []byte(`{"release":{}}`), deposit) + + // then + if spec.expError { + require.Error(t, err) + return + } + require.NoError(t, err) + beneficiaryAccount := accKeeper.GetAccount(ctx, spec.beneficiary) + assert.Equal(t, deposit, beneficiaryAccount.GetCoins()) + }) + } +} + +func TestExecuteWithNonExistingAddress(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + + // unauthorized - trialCtx so we don't change state + nonExistingAddress := addrFromUint64(9999) + _, err = keeper.Execute(ctx, nonExistingAddress, creator, []byte(`{}`), nil) + require.True(t, types.ErrNotFound.Is(err), err) +} + +func TestExecuteWithPanic(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(ctx, accKeeper, topUp) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, err := keeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 4", deposit) + require.NoError(t, err) + + // let's make sure we get a reasonable error, no panic/crash + _, err = keeper.Execute(ctx, addr, fred, []byte(`{"panic":{}}`), topUp) + require.Error(t, err) + require.True(t, errors.Is(err, types.ErrExecuteFailed)) + require.Equal(t, err.Error(), "execute wasm contract failed: Out of gas") +} + +func TestExecuteWithCpuLoop(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(ctx, accKeeper, topUp) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, err := keeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 5", deposit) + require.NoError(t, err) + + // make sure we set a limit before calling + var gasLimit uint64 = 400_000 + ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimit)) + require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) + + // ensure we get an out of gas panic + defer func() { + r := recover() + require.NotNil(t, r) + _, ok := r.(sdk.ErrorOutOfGas) + require.True(t, ok, "%v", r) + }() + + // this should throw out of gas exception (panic) + _, err = keeper.Execute(ctx, addr, fred, []byte(`{"cpu_loop":{}}`), nil) + require.True(t, false, "We must panic before this line") +} + +func TestExecuteWithStorageLoop(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(ctx, accKeeper, topUp) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + addr, err := keeper.Instantiate(ctx, contractID, creator, nil, initMsgBz, "demo contract 6", deposit) + require.NoError(t, err) + + // make sure we set a limit before calling + var gasLimit uint64 = 400_002 + ctx = ctx.WithGasMeter(sdk.NewGasMeter(gasLimit)) + require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) + + // ensure we get an out of gas panic + defer func() { + r := recover() + require.NotNil(t, r) + _, ok := r.(sdk.ErrorOutOfGas) + require.True(t, ok, "%v", r) + }() + + // this should throw out of gas exception (panic) + _, err = keeper.Execute(ctx, addr, fred, []byte(`{"storage_loop":{}}`), nil) + require.True(t, false, "We must panic before this line") +} + +func TestMigrate(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(ctx, accKeeper, sdk.NewCoins(sdk.NewInt64Coin("denom", 5000))) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + originalCodeID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + newCodeID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + require.NotEqual(t, originalCodeID, newCodeID) + + _, _, anyAddr := keyPubAddr() + _, _, newVerifierAddr := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: anyAddr, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + migMsg := struct { + Verifier sdk.AccAddress `json:"verifier"` + }{Verifier: newVerifierAddr} + migMsgBz, err := json.Marshal(migMsg) + require.NoError(t, err) + + specs := map[string]struct { + admin sdk.AccAddress + overrideContractAddr sdk.AccAddress + caller sdk.AccAddress + codeID uint64 + migrateMsg []byte + expErr *sdkerrors.Error + expVerifier sdk.AccAddress + }{ + "all good with same code id": { + admin: creator, + caller: creator, + codeID: originalCodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, + "all good with different code id": { + admin: creator, + caller: creator, + codeID: newCodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, + "all good with admin set": { + admin: fred, + caller: fred, + codeID: newCodeID, + migrateMsg: migMsgBz, + expVerifier: newVerifierAddr, + }, + "prevent migration when admin was not set on instantiate": { + caller: creator, + codeID: originalCodeID, + expErr: sdkerrors.ErrUnauthorized, + }, + "prevent migration when not sent by admin": { + caller: creator, + admin: fred, + codeID: originalCodeID, + expErr: sdkerrors.ErrUnauthorized, + }, + "fail with non existing code id": { + admin: creator, + caller: creator, + codeID: 99999, + expErr: sdkerrors.ErrInvalidRequest, + }, + "fail with non existing contract addr": { + admin: creator, + caller: creator, + overrideContractAddr: anyAddr, + codeID: originalCodeID, + expErr: sdkerrors.ErrInvalidRequest, + }, + "fail in contract with invalid migrate msg": { + admin: creator, + caller: creator, + codeID: originalCodeID, + migrateMsg: bytes.Repeat([]byte{0x1}, 7), + expErr: types.ErrMigrationFailed, + }, + "fail in contract without migrate msg": { + admin: creator, + caller: creator, + codeID: originalCodeID, + expErr: types.ErrMigrationFailed, + }, + } + + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + contractAddr, err := keeper.Instantiate(ctx, originalCodeID, creator, spec.admin, initMsgBz, "demo contract", nil) + require.NoError(t, err) + if spec.overrideContractAddr != nil { + contractAddr = spec.overrideContractAddr + } + _, err = keeper.Migrate(ctx, contractAddr, spec.caller, spec.codeID, spec.migrateMsg) + require.True(t, spec.expErr.Is(err), "expected %v but got %+v", spec.expErr, err) + if spec.expErr != nil { + return + } + cInfo := keeper.GetContractInfo(ctx, contractAddr) + assert.Equal(t, spec.codeID, cInfo.CodeID) + + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.InitContractCodeHistoryType, + CodeID: originalCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: initMsgBz, + }, { + Operation: types.MigrateContractCodeHistoryType, + CodeID: spec.codeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: spec.migrateMsg, + }} + assert.Equal(t, expHistory, keeper.GetContractHistory(ctx, contractAddr)) + + raw := keeper.QueryRaw(ctx, contractAddr, []byte("config")) + var stored map[string][]byte + require.NoError(t, json.Unmarshal(raw, &stored)) + require.Contains(t, stored, "verifier") + require.NoError(t, err) + assert.Equal(t, spec.expVerifier, sdk.AccAddress(stored["verifier"])) + }) + } +} + +func TestMigrateWithDispatchedMessage(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(ctx, accKeeper, sdk.NewCoins(sdk.NewInt64Coin("denom", 5000))) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + burnerCode, err := ioutil.ReadFile("./testdata/burner.wasm") + require.NoError(t, err) + + originalContractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + burnerContractID, err := keeper.Create(ctx, creator, burnerCode, "", "", nil) + require.NoError(t, err) + require.NotEqual(t, originalContractID, burnerContractID) + + _, _, myPayoutAddr := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: fred, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + contractAddr, err := keeper.Instantiate(ctx, originalContractID, creator, fred, initMsgBz, "demo contract", deposit) + require.NoError(t, err) + + migMsg := struct { + Payout sdk.AccAddress `json:"payout"` + }{Payout: myPayoutAddr} + migMsgBz, err := json.Marshal(migMsg) + require.NoError(t, err) + ctx = ctx.WithEventManager(sdk.NewEventManager()).WithBlockHeight(ctx.BlockHeight() + 1) + res, err := keeper.Migrate(ctx, contractAddr, fred, burnerContractID, migMsgBz) + require.NoError(t, err) + assert.Equal(t, "burnt 1 keys", string(res.Data)) + assert.Equal(t, "", res.Log) + type dict map[string]interface{} + expEvents := []dict{ + { + "Type": "wasm", + "Attr": []dict{ + {"contract_address": contractAddr}, + {"action": "burn"}, + {"payout": myPayoutAddr}, + }, + }, + { + "Type": "transfer", + "Attr": []dict{ + {"sender": contractAddr}, + {"recipient": myPayoutAddr}, + {"amount": "100000denom"}, + }, + }, + { + "Type": "message", + "Attr": []dict{ + {"module": "coin"}, + {"sender": contractAddr}, + }, + }, + } + expJSONEvts := string(mustMarshal(t, expEvents)) + assert.JSONEq(t, expJSONEvts, prettyEvents(t, ctx.EventManager().Events())) + + // all persistent data cleared + m := keeper.QueryRaw(ctx, contractAddr, []byte("config")) + require.Len(t, m, 0) + + // and all deposit tokens sent to myPayoutAddr + balance := accKeeper.GetAccount(ctx, myPayoutAddr).GetCoins() + assert.Equal(t, deposit, balance) +} + +func prettyEvents(t *testing.T, events sdk.Events) string { + t.Helper() + type prettyEvent struct { + Type string + Attr []map[string]string + } + + r := make([]prettyEvent, len(events)) + for i, e := range events { + attr := make([]map[string]string, len(e.Attributes)) + for j, a := range e.Attributes { + attr[j] = map[string]string{string(a.Key): string(a.Value)} + } + r[i] = prettyEvent{Type: e.Type, Attr: attr} + } + return string(mustMarshal(t, r)) +} + +func mustMarshal(t *testing.T, r interface{}) []byte { + t.Helper() + bz, err := json.Marshal(r) + require.NoError(t, err) + return bz +} + +func TestUpdateContractAdmin(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(ctx, accKeeper, topUp) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + originalContractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + _, _, anyAddr := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: anyAddr, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + specs := map[string]struct { + instAdmin sdk.AccAddress + newAdmin sdk.AccAddress + overrideContractAddr sdk.AccAddress + caller sdk.AccAddress + expErr *sdkerrors.Error + }{ + "all good with admin set": { + instAdmin: fred, + newAdmin: anyAddr, + caller: fred, + }, + "prevent update when admin was not set on instantiate": { + caller: creator, + newAdmin: fred, + expErr: sdkerrors.ErrUnauthorized, + }, + "prevent updates from non admin address": { + instAdmin: creator, + newAdmin: fred, + caller: fred, + expErr: sdkerrors.ErrUnauthorized, + }, + "fail with non existing contract addr": { + instAdmin: creator, + newAdmin: anyAddr, + caller: creator, + overrideContractAddr: anyAddr, + expErr: sdkerrors.ErrInvalidRequest, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + require.NotNil(t, spec.newAdmin) + addr, err := keeper.Instantiate(ctx, originalContractID, creator, spec.instAdmin, initMsgBz, "demo contract", nil) + require.NoError(t, err) + if spec.overrideContractAddr != nil { + addr = spec.overrideContractAddr + } + err = keeper.UpdateContractAdmin(ctx, addr, spec.caller, spec.newAdmin) + require.True(t, spec.expErr.Is(err), "expected %v but got %+v", spec.expErr, err) + if spec.expErr != nil { + return + } + cInfo := keeper.GetContractInfo(ctx, addr) + assert.Equal(t, spec.newAdmin, cInfo.Admin) + }) + } +} + +func TestClearContractAdmin(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(ctx, accKeeper, topUp) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + originalContractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + _, _, anyAddr := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: anyAddr, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + specs := map[string]struct { + instAdmin sdk.AccAddress + overrideContractAddr sdk.AccAddress + caller sdk.AccAddress + expErr *sdkerrors.Error + }{ + "all good when called by proper admin": { + instAdmin: fred, + caller: fred, + }, + "prevent update when admin was not set on instantiate": { + caller: creator, + expErr: sdkerrors.ErrUnauthorized, + }, + "prevent updates from non admin address": { + instAdmin: creator, + caller: fred, + expErr: sdkerrors.ErrUnauthorized, + }, + "fail with non existing contract addr": { + instAdmin: creator, + caller: creator, + overrideContractAddr: anyAddr, + expErr: sdkerrors.ErrInvalidRequest, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + addr, err := keeper.Instantiate(ctx, originalContractID, creator, spec.instAdmin, initMsgBz, "demo contract", nil) + require.NoError(t, err) + if spec.overrideContractAddr != nil { + addr = spec.overrideContractAddr + } + err = keeper.ClearContractAdmin(ctx, addr, spec.caller) + require.True(t, spec.expErr.Is(err), "expected %v but got %+v", spec.expErr, err) + if spec.expErr != nil { + return + } + cInfo := keeper.GetContractInfo(ctx, addr) + assert.Empty(t, cInfo.Admin) + }) + } +} + +type InitMsg struct { + Verifier sdk.AccAddress `json:"verifier"` + Beneficiary sdk.AccAddress `json:"beneficiary"` +} + +func createFakeFundedAccount(ctx sdk.Context, am auth.AccountKeeper, coins sdk.Coins) sdk.AccAddress { + _, _, addr := keyPubAddr() + fundAccounts(ctx, am, addr, coins) + return addr +} + +func fundAccounts(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, coins sdk.Coins) { + baseAcct := auth.NewBaseAccountWithAddress(addr) + _ = baseAcct.SetCoins(coins) + am.SetAccount(ctx, &baseAcct) +} + +var keyCounter uint64 = 0 + +// we need to make this deterministic (same every test run), as encoded address size and thus gas cost, +// depends on the actual bytes (due to ugly CanonicalAddress encoding) +func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.AccAddress) { + keyCounter++ + seed := make([]byte, 8) + binary.BigEndian.PutUint64(seed, keyCounter) + + key := ed25519.GenPrivKeyFromSecret(seed) + pub := key.PubKey() + addr := sdk.AccAddress(pub.Address()) + return key, pub, addr +} diff --git a/x/wasm/internal/keeper/proposal_handler.go b/x/wasm/internal/keeper/proposal_handler.go new file mode 100644 index 0000000000..9684db4bed --- /dev/null +++ b/x/wasm/internal/keeper/proposal_handler.go @@ -0,0 +1,133 @@ +package keeper + +import ( + "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/line/lbm-sdk/v2/x/wasm/internal/types" +) + +// NewWasmProposalHandler creates a new governance Handler for wasm proposals +func NewWasmProposalHandler(k Keeper, 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.MigrateContractProposal: + return handleMigrateProposal(ctx, k, c) + case types.UpdateAdminProposal: + return handleUpdateAdminProposal(ctx, k, c) + case types.ClearAdminProposal: + return handleClearAdminProposal(ctx, k, c) + default: + return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized wasm proposal content type: %T", c) + } + } +} + +func handleStoreCodeProposal(ctx sdk.Context, k Keeper, p types.StoreCodeProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + codeID, err := k.create(ctx, p.RunAs, p.WASMByteCode, p.Source, p.Builder, p.InstantiatePermission, GovAuthorizationPolicy{}) + if err != nil { + return err + } + + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyCodeID, fmt.Sprintf("%d", codeID)), + ) + ctx.EventManager().EmitEvent(ourEvent) + return nil +} + +func handleInstantiateProposal(ctx sdk.Context, k Keeper, p types.InstantiateContractProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + contractAddr, err := k.instantiate(ctx, p.CodeID, p.RunAs, p.Admin, p.InitMsg, p.Label, p.InitFunds, GovAuthorizationPolicy{}) + if err != nil { + return err + } + + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyCodeID, fmt.Sprintf("%d", p.CodeID)), + sdk.NewAttribute(types.AttributeKeyContract, contractAddr.String()), + ) + ctx.EventManager().EmitEvent(ourEvent) + return nil +} + +func handleMigrateProposal(ctx sdk.Context, k Keeper, p types.MigrateContractProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + res, err := k.migrate(ctx, p.Contract, p.RunAs, p.CodeID, p.MigrateMsg, GovAuthorizationPolicy{}) + if err != nil { + return err + } + + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyContract, p.Contract.String()), + ) + ctx.EventManager().EmitEvents(append(res.Events, ourEvent)) + return nil +} + +func handleUpdateAdminProposal(ctx sdk.Context, k Keeper, p types.UpdateAdminProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + if err := k.setContractAdmin(ctx, p.Contract, nil, p.NewAdmin, GovAuthorizationPolicy{}); err != nil { + return err + } + + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyContract, p.Contract.String()), + ) + ctx.EventManager().EmitEvent(ourEvent) + return nil +} + +func handleClearAdminProposal(ctx sdk.Context, k Keeper, p types.ClearAdminProposal) error { + if err := p.ValidateBasic(); err != nil { + return err + } + + if err := k.setContractAdmin(ctx, p.Contract, nil, nil, GovAuthorizationPolicy{}); err != nil { + return err + } + ourEvent := sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeKeyContract, p.Contract.String()), + ) + ctx.EventManager().EmitEvent(ourEvent) + return nil +} diff --git a/x/wasm/internal/keeper/proposal_integration_test.go b/x/wasm/internal/keeper/proposal_integration_test.go new file mode 100644 index 0000000000..f89f3bea37 --- /dev/null +++ b/x/wasm/internal/keeper/proposal_integration_test.go @@ -0,0 +1,394 @@ +// nolint: scopelint +package keeper + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "io/ioutil" + "os" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStoreCodeProposal(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.setParams(ctx, types.Params{ + UploadAccess: types.AllowNobody, + DefaultInstantiatePermission: types.Nobody, + MaxWasmCodeSize: types.DefaultMaxWasmCodeSize, + }) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + var anyAddress sdk.AccAddress = make([]byte, sdk.AddrLen) + + src := types.StoreCodeProposalFixture(func(p *types.StoreCodeProposal) { + p.RunAs = anyAddress + p.WASMByteCode = wasmCode + p.Source = "https://example.com/mysource" + p.Builder = "foo/bar:v0.0.0" + }) + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.Content) + require.NoError(t, err) + + // then + cInfo := wasmKeeper.GetCodeInfo(ctx, 1) + require.NotNil(t, cInfo) + assert.Equal(t, anyAddress, cInfo.Creator) + assert.Equal(t, "foo/bar:v0.0.0", cInfo.Builder) + assert.Equal(t, "https://example.com/mysource", cInfo.Source) + + storedCode, err := wasmKeeper.GetByteCode(ctx, 1) + require.NoError(t, err) + assert.Equal(t, wasmCode, storedCode) +} + +func TestInstantiateProposal(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.setParams(ctx, types.Params{ + UploadAccess: types.AllowNobody, + DefaultInstantiatePermission: types.Nobody, + MaxWasmCodeSize: types.DefaultMaxWasmCodeSize, + }) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + require.NoError(t, wasmKeeper.importCode(ctx, 1, + types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)), + wasmCode), + ) + + var ( + oneAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, sdk.AddrLen) + ) + src := types.InstantiateContractProposalFixture(func(p *types.InstantiateContractProposal) { + p.CodeID = firstCodeID + p.RunAs = oneAddress + p.Admin = otherAddress + p.Label = "testing" + }) + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.Content) + require.NoError(t, err) + + // then + contractAddr, err := sdk.AccAddressFromBech32("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5") + require.NoError(t, err) + + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, uint64(1), cInfo.CodeID) + assert.Equal(t, oneAddress, cInfo.Creator) + assert.Equal(t, otherAddress, cInfo.Admin) + assert.Equal(t, "testing", cInfo.Label) + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.InitContractCodeHistoryType, + CodeID: src.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: src.InitMsg, + }} + assert.Equal(t, expHistory, wasmKeeper.GetContractHistory(ctx, contractAddr)) +} + +func TestMigrateProposal(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.setParams(ctx, types.Params{ + UploadAccess: types.AllowNobody, + DefaultInstantiatePermission: types.Nobody, + MaxWasmCodeSize: types.DefaultMaxWasmCodeSize, + }) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + codeInfoFixture := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)) + require.NoError(t, wasmKeeper.importCode(ctx, 1, codeInfoFixture, wasmCode)) + require.NoError(t, wasmKeeper.importCode(ctx, 2, codeInfoFixture, wasmCode)) + + var ( + anyAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, sdk.AddrLen) + contractAddr = contractAddress(1, 1) + ) + + contractInfoFixture := types.ContractInfoFixture(func(c *types.ContractInfo) { + c.Label = "testing" + c.Admin = anyAddress + }) + key, err := hex.DecodeString("636F6E666967") + require.NoError(t, err) + m := types.Model{Key: key, Value: []byte(`{"verifier":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","beneficiary":"AAAAAAAAAAAAAAAAAAAAAAAAAAA=","funder":"AQEBAQEBAQEBAQEBAQEBAQEBAQE="}`)} + require.NoError(t, wasmKeeper.importContract(ctx, contractAddr, &contractInfoFixture, []types.Model{m})) + + migMsg := struct { + Verifier sdk.AccAddress `json:"verifier"` + }{Verifier: otherAddress} + migMsgBz, err := json.Marshal(migMsg) + require.NoError(t, err) + + src := types.MigrateContractProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + CodeID: 2, + Contract: contractAddr, + MigrateMsg: migMsgBz, + RunAs: otherAddress, + } + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, src) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.Content) + require.NoError(t, err) + + // then + require.NoError(t, err) + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, uint64(2), cInfo.CodeID) + assert.Equal(t, anyAddress, cInfo.Admin) + assert.Equal(t, "testing", cInfo.Label) + expHistory := []types.ContractCodeHistoryEntry{{ + Operation: types.GenesisContractCodeHistoryType, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + }, { + Operation: types.MigrateContractCodeHistoryType, + CodeID: src.CodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: src.MigrateMsg, + }} + assert.Equal(t, expHistory, wasmKeeper.GetContractHistory(ctx, contractAddr)) +} + +func TestAdminProposals(t *testing.T) { + var ( + otherAddress sdk.AccAddress = bytes.Repeat([]byte{0x2}, sdk.AddrLen) + contractAddr = contractAddress(1, 1) + ) + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + specs := map[string]struct { + state types.ContractInfo + srcProposal gov.Content + expAdmin sdk.AccAddress + }{ + "update with different admin": { + state: types.ContractInfoFixture(), + srcProposal: types.UpdateAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + NewAdmin: otherAddress, + }, + expAdmin: otherAddress, + }, + "update with old admin empty": { + state: types.ContractInfoFixture(func(info *types.ContractInfo) { + info.Admin = nil + }), + srcProposal: types.UpdateAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + NewAdmin: otherAddress, + }, + expAdmin: otherAddress, + }, + "clear admin": { + state: types.ContractInfoFixture(), + srcProposal: types.ClearAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + }, + expAdmin: nil, + }, + "clear with old admin empty": { + state: types.ContractInfoFixture(func(info *types.ContractInfo) { + info.Admin = nil + }), + srcProposal: types.ClearAdminProposal{ + WasmProposal: types.WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + }, + expAdmin: nil, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + wasmKeeper.setParams(ctx, types.Params{ + UploadAccess: types.AllowNobody, + DefaultInstantiatePermission: types.Nobody, + MaxWasmCodeSize: types.DefaultMaxWasmCodeSize, + }) + + codeInfoFixture := types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)) + require.NoError(t, wasmKeeper.importCode(ctx, 1, codeInfoFixture, wasmCode)) + + require.NoError(t, wasmKeeper.importContract(ctx, contractAddr, &spec.state, []types.Model{})) + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, spec.srcProposal) + require.NoError(t, err) + + // and execute proposal + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.Content) + require.NoError(t, err) + + // then + cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr) + require.NotNil(t, cInfo) + assert.Equal(t, spec.expAdmin, cInfo.Admin) + }) + } +} + +func TestUpdateParamsProposal(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + govKeeper, wasmKeeper := keepers.GovKeeper, keepers.WasmKeeper + + var ( + cdc = keepers.WasmKeeper.cdc + myAddress sdk.AccAddress = make([]byte, sdk.AddrLen) + oneAddressAccessConfig = types.OnlyAddress.With(myAddress) + newMaxWasmCodeSize = uint64(42) + defaultParams = types.DefaultParams() + ) + + specs := map[string]struct { + src params.ParamChange + expUploadConfig types.AccessConfig + expInstantiateType types.AccessType + expMaxWasmCodeSize uint64 + }{ + "update upload permission param": { + src: params.ParamChange{ + Subspace: types.DefaultParamspace, + Key: string(types.ParamStoreKeyUploadAccess), + Value: string(cdc.MustMarshalJSON(&types.AllowNobody)), + }, + expUploadConfig: types.AllowNobody, + expInstantiateType: defaultParams.DefaultInstantiatePermission, + expMaxWasmCodeSize: defaultParams.MaxWasmCodeSize, + }, + "update upload permission param with address": { + src: params.ParamChange{ + Subspace: types.DefaultParamspace, + Key: string(types.ParamStoreKeyUploadAccess), + Value: string(cdc.MustMarshalJSON(&oneAddressAccessConfig)), + }, + expUploadConfig: oneAddressAccessConfig, + expInstantiateType: defaultParams.DefaultInstantiatePermission, + expMaxWasmCodeSize: defaultParams.MaxWasmCodeSize, + }, + "update instantiate param": { + src: params.ParamChange{ + Subspace: types.DefaultParamspace, + Key: string(types.ParamStoreKeyInstantiateAccess), + Value: string(cdc.MustMarshalJSON(types.Nobody)), + }, + expUploadConfig: defaultParams.UploadAccess, + expInstantiateType: types.Nobody, + expMaxWasmCodeSize: defaultParams.MaxWasmCodeSize, + }, + "update max wasm code size": { + src: params.ParamChange{ + Subspace: types.DefaultParamspace, + Key: string(types.ParamStoreKeyMaxWasmCodeSize), + Value: string(cdc.MustMarshalJSON(newMaxWasmCodeSize)), + }, + expUploadConfig: defaultParams.UploadAccess, + expInstantiateType: defaultParams.DefaultInstantiatePermission, + expMaxWasmCodeSize: newMaxWasmCodeSize, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + wasmKeeper.setParams(ctx, types.DefaultParams()) + + proposal := params.ParameterChangeProposal{ + Title: "Foo", + Description: "Bar", + Changes: []params.ParamChange{spec.src}, + } + + // when stored + storedProposal, err := govKeeper.SubmitProposal(ctx, proposal) + require.NoError(t, err) + + // and proposal execute + handler := govKeeper.Router().GetRoute(storedProposal.ProposalRoute()) + err = handler(ctx, storedProposal.Content) + require.NoError(t, err) + + // then + assert.True(t, spec.expUploadConfig.Equals(wasmKeeper.getUploadAccessConfig(ctx)), + "got %#v not %#v", wasmKeeper.getUploadAccessConfig(ctx), spec.expUploadConfig) + assert.Equal(t, spec.expInstantiateType, wasmKeeper.getInstantiateAccessConfig(ctx)) + assert.Equal(t, spec.expMaxWasmCodeSize, wasmKeeper.GetMaxWasmCodeSize(ctx)) + }) + } +} diff --git a/x/wasm/internal/keeper/querier.go b/x/wasm/internal/keeper/querier.go new file mode 100644 index 0000000000..b1c5e36fe8 --- /dev/null +++ b/x/wasm/internal/keeper/querier.go @@ -0,0 +1,202 @@ +package keeper + +import ( + "sort" + "strconv" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/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" +) + +// NewQuerier creates a new querier +func NewQuerier(keeper Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) { + switch path[0] { + case QueryGetContract: + return queryContractInfo(ctx, path[1], keeper) + case QueryListContractByCode: + return queryContractListByCode(ctx, path[1], 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, keeper) + case QueryGetCode: + return queryCode(ctx, path[1], keeper) + case QueryListCode: + return queryCodeList(ctx, keeper) + case QueryContractHistory: + return queryContractHistory(ctx, path[1], keeper) + default: + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown data query endpoint") + } + } +} + +func queryContractInfo(ctx sdk.Context, bech string, keeper Keeper) ([]byte, error) { + addr, err := sdk.AccAddressFromBech32(bech) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error()) + } + info := keeper.GetContractInfo(ctx, addr) + if info == nil { + return []byte("null"), nil + } + redact(info) + infoWithAddress := types.NewContractInfoResponse(*info, addr) + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, infoWithAddress) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil +} + +// redact clears all fields not in the public api +func redact(info *types.ContractInfo) { + info.Created = nil +} + +func queryContractListByCode(ctx sdk.Context, codeIDstr string, keeper Keeper) ([]byte, error) { + codeID, err := strconv.ParseUint(codeIDstr, 10, 64) + if err != nil { + return nil, err + } + + var contracts []types.ContractInfoResponse + keeper.IterateContractInfo(ctx, func(addr sdk.AccAddress, info types.ContractInfo) bool { + if info.CodeID == codeID { + // and add the address + infoWithAddress := types.NewContractInfoResponse(info, addr) + contracts = append(contracts, infoWithAddress) + } + return false + }) + + // now we sort them by AbsoluteTxPosition + sort.Slice(contracts, func(i, j int) bool { + return contracts[i].LessThan(contracts[j]) + }) + + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, contracts) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil +} + +func queryContractState(ctx sdk.Context, bech, queryMethod string, req abci.RequestQuery, keeper Keeper) ([]byte, error) { + contractAddr, err := sdk.AccAddressFromBech32(bech) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, bech) + } + + var resultData []types.Model + switch queryMethod { + case QueryMethodContractStateAll: + // this returns a serialized json object (which internally encoded binary fields properly) + for iter := keeper.GetContractState(ctx, contractAddr); iter.Valid(); iter.Next() { + resultData = append(resultData, types.Model{ + Key: iter.Key(), + Value: iter.Value(), + }) + } + if resultData == nil { + resultData = make([]types.Model, 0) + } + case QueryMethodContractStateRaw: + // this returns the raw data from the state, base64-encoded + return keeper.QueryRaw(ctx, contractAddr, req.Data), nil + case QueryMethodContractStateSmart: + // we enforce a subjective gas limit on all queries to avoid infinite loops + ctx = ctx.WithGasMeter(sdk.NewGasMeter(keeper.queryGasLimit)) + // this returns raw bytes (must be base64-encoded) + return keeper.QuerySmart(ctx, contractAddr, req.Data) + default: + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, queryMethod) + } + bz, err := types.ModuleCdc.MarshalJSON(resultData) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil +} + +func queryCode(ctx sdk.Context, codeIDstr string, keeper Keeper) ([]byte, error) { + codeID, err := strconv.ParseUint(codeIDstr, 10, 64) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "invalid codeID: "+err.Error()) + } + + res := keeper.GetCodeInfo(ctx, codeID) + if res == nil { + // nil, nil leads to 404 in rest handler + return nil, nil + } + + code, err := keeper.GetByteCode(ctx, codeID) + if err != nil { + return nil, sdkerrors.Wrap(err, "loading wasm code") + } + + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, types.NewCodeInfoResponse(codeID, *res, code)) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil +} + +func queryCodeList(ctx sdk.Context, keeper Keeper) ([]byte, error) { + var info []types.CodeInfoResponse + keeper.IterateCodeInfos(ctx, func(i uint64, res types.CodeInfo) bool { + info = append(info, types.NewCodeInfoResponse(i, res, nil)) + return false + }) + + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, info) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil +} + +func queryContractHistory(ctx sdk.Context, bech string, keeper Keeper) ([]byte, error) { + contractAddr, err := sdk.AccAddressFromBech32(bech) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error()) + } + entries := keeper.GetContractHistory(ctx, contractAddr) + if entries == nil { + // nil, nil leads to 404 in rest handler + return nil, nil + } + + histories := make([]types.ContractHistoryResponse, len(entries)) + for i, entry := range entries { + histories[i] = types.NewContractHistoryResponse(entry) + } + + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, histories) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil +} diff --git a/x/wasm/internal/keeper/querier_test.go b/x/wasm/internal/keeper/querier_test.go new file mode 100644 index 0000000000..d377a974b1 --- /dev/null +++ b/x/wasm/internal/keeper/querier_test.go @@ -0,0 +1,387 @@ +// nolint: errcheck, scopelint +package keeper + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkErrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" +) + +func TestQueryContractState(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + anyAddr := createFakeFundedAccount(ctx, accKeeper, topUp) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + contractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := InitMsg{ + Verifier: anyAddr, + Beneficiary: bob, + } + initMsgBz, err := types.ModuleCdc.MarshalJSON(initMsg) + require.NoError(t, err) + + addr, err := keeper.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 + q := NewQuerier(keeper) + 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 *sdkErrors.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.ErrQueryFailed, + }, + "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}, + 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, spec.expErr.Is(err), 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, types.ModuleCdc.UnmarshalJSON(binResult, &r)) + if spec.expModelLen == 0 { + require.Nil(t, r) + } else { + 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 TestListContractByCodeOrdering(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 1000000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 500)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + anyAddr := createFakeFundedAccount(ctx, accKeeper, topUp) + + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + + codeID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + _, _, bob := keyPubAddr() + initMsg := InitMsg{ + Verifier: anyAddr, + Beneficiary: bob, + } + initMsgBz, err := types.ModuleCdc.MarshalJSON(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 = keeper.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 := NewQuerier(keeper) + query := []string{QueryListContractByCode, fmt.Sprintf("%d", codeID)} + data := abci.RequestQuery{} + res, err := q(ctx, query, data) + require.NoError(t, err) + + var contracts []types.ContractInfoResponse + err = types.ModuleCdc.UnmarshalJSON(res, &contracts) + require.NoError(t, err) + + require.Equal(t, 10, len(contracts)) + + for i, contract := range contracts { + assert.Equal(t, fmt.Sprintf("contract %d", i), contract.GetLabel()) + assert.NotEmpty(t, contract.GetAddress()) + } + assert.NotContains(t, string(res), "create") + assert.NotContains(t, string(res), "Create") +} + +func TestQueryContractHistory(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + keeper := keepers.WasmKeeper + + var ( + otherAddr sdk.AccAddress = bytes.Repeat([]byte{0x2}, sdk.AddrLen) + ) + + specs := map[string]struct { + srcQueryAddr sdk.AccAddress + srcHistory []types.ContractCodeHistoryEntry + expContent []types.ContractCodeHistoryEntry + }{ + "response with internal fields cleared": { + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.GenesisContractCodeHistoryType, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"init message"`), + }}, + expContent: []types.ContractCodeHistoryEntry{{ + Operation: types.GenesisContractCodeHistoryType, + CodeID: firstCodeID, + Msg: []byte(`"init message"`), + }}, + }, + "response with multiple entries": { + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.InitContractCodeHistoryType, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"init message"`), + }, { + Operation: types.MigrateContractCodeHistoryType, + CodeID: 2, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"migrate message 1"`), + }, { + Operation: types.MigrateContractCodeHistoryType, + CodeID: 3, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"migrate message 2"`), + }}, + expContent: []types.ContractCodeHistoryEntry{{ + Operation: types.InitContractCodeHistoryType, + CodeID: firstCodeID, + Msg: []byte(`"init message"`), + }, { + Operation: types.MigrateContractCodeHistoryType, + CodeID: 2, + Msg: []byte(`"migrate message 1"`), + }, { + Operation: types.MigrateContractCodeHistoryType, + CodeID: 3, + Msg: []byte(`"migrate message 2"`), + }}, + }, + "unknown contract address": { + srcQueryAddr: otherAddr, + srcHistory: []types.ContractCodeHistoryEntry{{ + Operation: types.GenesisContractCodeHistoryType, + CodeID: firstCodeID, + Updated: types.NewAbsoluteTxPosition(ctx), + Msg: []byte(`"init message"`), + }}, + expContent: nil, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + _, _, myContractAddr := keyPubAddr() + keeper.appendToContractHistory(ctx, myContractAddr, spec.srcHistory...) + q := NewQuerier(keeper) + 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) + if spec.expContent == nil { + require.Nil(t, resData) + return + } + var got []types.ContractHistoryResponse + err = types.ModuleCdc.UnmarshalJSON(resData, &got) + require.NoError(t, err) + + assertContractHistory(t, spec.expContent, got) + }) + } +} + +func assertContractHistory(t *testing.T, expected []types.ContractCodeHistoryEntry, actual []types.ContractHistoryResponse) { + assert.Equal(t, len(expected), len(actual)) + + for i, entry := range expected { + expectedResponse := types.NewContractHistoryResponse(entry) + assert.Equal(t, expectedResponse, actual[i]) + } +} + +func TestQueryCodeList(t *testing.T) { + wasmCode, err := ioutil.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) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + keeper := keepers.WasmKeeper + + for _, codeID := range spec.codeIDs { + require.NoError(t, keeper.importCode(ctx, codeID, + types.CodeInfoFixture(types.WithSHA256CodeHash(wasmCode)), + wasmCode), + ) + } + q := NewQuerier(keeper) + // when + query := []string{QueryListCode} + data := abci.RequestQuery{} + resData, err := q(ctx, query, data) + + // then + require.NoError(t, err) + + var got []types.CodeInfoResponse + err = types.ModuleCdc.UnmarshalJSON(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].GetID()) + } + }) + } +} diff --git a/x/wasm/internal/keeper/query_plugins.go b/x/wasm/internal/keeper/query_plugins.go new file mode 100644 index 0000000000..bf6e1205a9 --- /dev/null +++ b/x/wasm/internal/keeper/query_plugins.go @@ -0,0 +1,334 @@ +package keeper + +import ( + "encoding/json" + + wasmTypes "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/x/bank" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/staking" + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +type QueryHandler struct { + Ctx sdk.Context + Plugins QueryPlugins +} + +var _ wasmTypes.Querier = QueryHandler{} + +func (q QueryHandler) Query(request wasmTypes.QueryRequest, gasLimit uint64) ([]byte, error) { + // set a limit for a subctx + sdkGas := gasLimit / GasMultiplier + subctx := q.Ctx.WithGasMeter(sdk.NewGasMeter(sdkGas)) + + // make sure we charge the higher level context even on panic + defer func() { + q.Ctx.GasMeter().ConsumeGas(subctx.GasMeter().GasConsumed(), "contract sub-query") + }() + + // do the query + if request.Bank != nil { + return q.Plugins.Bank(subctx, request.Bank) + } + if request.Custom != nil { + return q.Plugins.Custom(subctx, request.Custom) + } + if request.Staking != nil { + return q.Plugins.Staking(subctx, request.Staking) + } + if request.Wasm != nil { + return q.Plugins.Wasm(subctx, request.Wasm) + } + return nil, wasmTypes.Unknown{} +} + +func (q QueryHandler) GasConsumed() uint64 { + return q.Ctx.GasMeter().GasConsumed() +} + +type QueryPlugins struct { + Bank func(ctx sdk.Context, request *wasmTypes.BankQuery) ([]byte, error) + Custom func(ctx sdk.Context, request json.RawMessage) ([]byte, error) + Staking func(ctx sdk.Context, request *wasmTypes.StakingQuery) ([]byte, error) + Wasm func(ctx sdk.Context, request *wasmTypes.WasmQuery) ([]byte, error) +} + +func DefaultQueryPlugins(bank bank.ViewKeeper, staking staking.Keeper, distKeeper distribution.Keeper, queryRouter types.QueryRouter, wasm *Keeper) QueryPlugins { + return QueryPlugins{ + Bank: BankQuerier(bank), + Custom: CustomQuerier(queryRouter), + Staking: StakingQuerier(staking, distKeeper), + 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.Staking != nil { + e.Staking = o.Staking + } + if o.Wasm != nil { + e.Wasm = o.Wasm + } + return e +} + +func BankQuerier(bank bank.ViewKeeper) func(ctx sdk.Context, request *wasmTypes.BankQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmTypes.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 := bank.GetCoins(ctx, addr) + res := wasmTypes.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) + } + coins := bank.GetCoins(ctx, addr) + amount := coins.AmountOf(request.Balance.Denom) + res := wasmTypes.BalanceResponse{ + Amount: wasmTypes.Coin{ + Denom: request.Balance.Denom, + Amount: amount.String(), + }, + } + return json.Marshal(res) + } + return nil, wasmTypes.UnsupportedRequest{Kind: "unknown BankQuery variant"} + } +} + +func CustomQuerier(queryRouter types.QueryRouter) func(ctx sdk.Context, querierJson json.RawMessage) ([]byte, error) { + return func(ctx sdk.Context, querierJson json.RawMessage) ([]byte, error) { + var linkQueryWrapper types.LinkQueryWrapper + err := json.Unmarshal(querierJson, &linkQueryWrapper) + if err != nil { + return nil, err + } + querier := queryRouter.GetRoute(linkQueryWrapper.Module) + if querier == nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Unknown encode module") + } + return querier(ctx, linkQueryWrapper.QueryData) + } +} + +func StakingQuerier(keeper staking.Keeper, distKeeper distribution.Keeper) func(ctx sdk.Context, request *wasmTypes.StakingQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmTypes.StakingQuery) ([]byte, error) { + if request.BondedDenom != nil { + denom := keeper.BondDenom(ctx) + res := wasmTypes.BondedDenomResponse{ + Denom: denom, + } + return json.Marshal(res) + } + if request.Validators != nil { + validators := keeper.GetBondedValidatorsByPower(ctx) + // validators := keeper.GetAllValidators(ctx) + wasmVals := make([]wasmTypes.Validator, len(validators)) + for i, v := range validators { + wasmVals[i] = wasmTypes.Validator{ + Address: v.OperatorAddress.String(), + Commission: v.Commission.Rate.String(), + MaxCommission: v.Commission.MaxRate.String(), + MaxChangeRate: v.Commission.MaxChangeRate.String(), + } + } + res := wasmTypes.ValidatorsResponse{ + Validators: wasmVals, + } + 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 := wasmTypes.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 wasmTypes.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, wasmTypes.UnsupportedRequest{Kind: "unknown Staking variant"} + } +} + +func sdkToDelegations(ctx sdk.Context, keeper staking.Keeper, delegations []staking.Delegation) (wasmTypes.Delegations, error) { + result := make([]wasmTypes.Delegation, len(delegations)) + bondDenom := keeper.BondDenom(ctx) + + for i, d := range delegations { + // 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, d.ValidatorAddress) + if !found { + return nil, sdkerrors.Wrap(staking.ErrNoValidatorFound, "can't load validator for delegation") + } + amount := sdk.NewCoin(bondDenom, val.TokensFromShares(d.Shares).TruncateInt()) + + result[i] = wasmTypes.Delegation{ + Delegator: d.DelegatorAddress.String(), + Validator: d.ValidatorAddress.String(), + Amount: convertSdkCoinToWasmCoin(amount), + } + } + return result, nil +} + +func sdkToFullDelegation(ctx sdk.Context, keeper staking.Keeper, distKeeper distribution.Keeper, delegation staking.Delegation) (*wasmTypes.FullDelegation, error) { + val, found := keeper.GetValidator(ctx, delegation.ValidatorAddress) + if !found { + return nil, sdkerrors.Wrap(staking.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 := wasmTypes.NewCoin(0, bondDenom) + if !keeper.HasReceivingRedelegation(ctx, delegation.DelegatorAddress, delegation.ValidatorAddress) { + 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 &wasmTypes.FullDelegation{ + Delegator: delegation.DelegatorAddress.String(), + Validator: delegation.ValidatorAddress.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 distribution.Keeper, delegation staking.Delegation) ([]wasmTypes.Coin, error) { + // Try to get *delegator* reward info! + params := distribution.QueryDelegationRewardsParams{ + DelegatorAddress: delegation.DelegatorAddress, + ValidatorAddress: delegation.ValidatorAddress, + } + data, err := json.Marshal(params) + if err != nil { + return nil, err + } + req := abci.RequestQuery{Data: data} + + // just to be safe... ensure we do not accidentally write in the querier (which does some funky things) + cache, _ := ctx.CacheContext() + qres, err := distribution.NewQuerier(distKeeper)(cache, []string{distribution.QueryDelegationRewards}, req) + if err != nil { + return nil, err + } + + var decRewards sdk.DecCoins + err = json.Unmarshal(qres, &decRewards) + if err != nil { + return nil, err + } + // **** all this above should be ONE method call + + // now we have it, convert it into wasmTypes + rewards := make([]wasmTypes.Coin, len(decRewards)) + for i, r := range decRewards { + rewards[i] = wasmTypes.Coin{ + Denom: r.Denom, + Amount: r.Amount.TruncateInt().String(), + } + } + return rewards, nil +} + +func WasmQuerier(wasm *Keeper) func(ctx sdk.Context, request *wasmTypes.WasmQuery) ([]byte, error) { + return func(ctx sdk.Context, request *wasmTypes.WasmQuery) ([]byte, error) { + if request.Smart != nil { + addr, err := sdk.AccAddressFromBech32(request.Smart.ContractAddr) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Smart.ContractAddr) + } + return wasm.QuerySmart(ctx, addr, request.Smart.Msg) + } + if request.Raw != nil { + addr, err := sdk.AccAddressFromBech32(request.Raw.ContractAddr) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, request.Raw.ContractAddr) + } + return wasm.QueryRaw(ctx, addr, request.Raw.Key), nil + } + return nil, wasmTypes.UnsupportedRequest{Kind: "unknown WasmQuery variant"} + } +} + +func convertSdkCoinsToWasmCoins(coins []sdk.Coin) wasmTypes.Coins { + converted := make(wasmTypes.Coins, len(coins)) + for i, c := range coins { + converted[i] = convertSdkCoinToWasmCoin(c) + } + return converted +} + +func convertSdkCoinToWasmCoin(coin sdk.Coin) wasmTypes.Coin { + return wasmTypes.Coin{ + Denom: coin.Denom, + Amount: coin.Amount.String(), + } +} diff --git a/x/wasm/internal/keeper/recurse_test.go b/x/wasm/internal/keeper/recurse_test.go new file mode 100644 index 0000000000..9a277a64db --- /dev/null +++ b/x/wasm/internal/keeper/recurse_test.go @@ -0,0 +1,334 @@ +// nolint: scopelint +package keeper + +import ( + "encoding/json" + "io/ioutil" + "os" + "testing" + + wasmTypes "github.com/CosmWasm/wasmvm/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +type Recurse struct { + Depth uint32 `json:"depth"` + Work uint32 `json:"work"` + Contract sdk.AccAddress `json:"contract"` +} + +type recurseWrapper struct { + Recurse Recurse `json:"recurse"` +} + +func buildRecurseQuery(t *testing.T, msg Recurse) []byte { + wrapper := recurseWrapper{Recurse: msg} + bz, err := json.Marshal(wrapper) + require.NoError(t, err) + return bz +} + +type recurseResponse struct { + Hashed []byte `json:"hashed"` +} + +// number os wasm queries called from a contract +var totalWasmQueryCounter int + +func initRecurseContract(t *testing.T) (contract sdk.AccAddress, creator sdk.AccAddress, ctx sdk.Context, keeper Keeper, cleanup func()) { + // we do one basic setup before all test cases (which are read-only and don't change state) + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + cleanup = func() { os.RemoveAll(tempDir) } + + var realWasmQuerier func(ctx sdk.Context, request *wasmTypes.WasmQuery) ([]byte, error) + countingQuerier := &QueryPlugins{ + Wasm: func(ctx sdk.Context, request *wasmTypes.WasmQuery) ([]byte, error) { + totalWasmQueryCounter++ + return realWasmQuerier(ctx, request) + }, + } + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, countingQuerier) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + realWasmQuerier = WasmQuerier(&keeper) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator = createFakeFundedAccount(ctx, accKeeper, deposit.Add(deposit...)) + + // store the code + wasmCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + codeID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil) + require.NoError(t, err) + + // instantiate the contract + _, _, bob := keyPubAddr() + _, _, fred := keyPubAddr() + initMsg := InitMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + contractAddr, err := keeper.Instantiate(ctx, codeID, creator, nil, initMsgBz, "recursive contract", deposit) + require.NoError(t, err) + + return contractAddr, creator, ctx, keeper, cleanup +} + +func TestGasCostOnQuery(t *testing.T) { + const ( + GasNoWork uint64 = InstanceCost + 2_953 + // Note: about 100 SDK gas (10k wasmer gas) for each round of sha256 + GasWork50 uint64 = InstanceCost + 8_661 // this is a little shy of 50k gas - to keep an eye on the limit + + GasReturnUnhashed uint64 = 393 + GasReturnHashed uint64 = 342 + ) + + cases := map[string]struct { + gasLimit uint64 + msg Recurse + expectedGas uint64 + }{ + "no recursion, no work": { + gasLimit: 400_000, + msg: Recurse{}, + expectedGas: GasNoWork, + }, + "no recursion, some work": { + gasLimit: 400_000, + msg: Recurse{ + Work: 50, // 50 rounds of sha256 inside the contract + }, + expectedGas: GasWork50, + }, + "recursion 1, no work": { + gasLimit: 400_000, + msg: Recurse{ + Depth: 1, + }, + expectedGas: 2*GasNoWork + GasReturnUnhashed, + }, + "recursion 1, some work": { + gasLimit: 400_000, + msg: Recurse{ + Depth: 1, + Work: 50, + }, + expectedGas: 2*GasWork50 + GasReturnHashed, + }, + "recursion 4, some work": { + gasLimit: 400_000, + msg: Recurse{ + Depth: 4, + Work: 50, + }, + // FIXME: why -6... confused a bit by calculations, seems like rounding issues + expectedGas: 5*GasWork50 + 4*GasReturnHashed - 6, + }, + } + + contractAddr, creator, ctx, keeper, cleanup := initRecurseContract(t) + defer cleanup() + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // external limit has no effect (we get a panic if this is enforced) + keeper.queryGasLimit = 1000 + + // make sure we set a limit before calling + ctx = ctx.WithGasMeter(sdk.NewGasMeter(tc.gasLimit)) + require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) + + // do the query + recurse := tc.msg + recurse.Contract = contractAddr + msg := buildRecurseQuery(t, recurse) + data, err := keeper.QuerySmart(ctx, contractAddr, msg) + require.NoError(t, err) + + // check the gas is what we expected + assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed()) + + // assert result is 32 byte sha256 hash (if hashed), or contractAddr if not + var resp recurseResponse + err = json.Unmarshal(data, &resp) + require.NoError(t, err) + if recurse.Work == 0 { + assert.Equal(t, len(resp.Hashed), len(creator.String())) + } else { + assert.Equal(t, len(resp.Hashed), 32) + } + }) + } +} + +func TestGasOnExternalQuery(t *testing.T) { + const ( + GasWork50 uint64 = InstanceCost + 8_464 + ) + + cases := map[string]struct { + gasLimit uint64 + msg Recurse + expectPanic bool + }{ + "no recursion, plenty gas": { + gasLimit: 400_000, + msg: Recurse{ + Work: 50, // 50 rounds of sha256 inside the contract + }, + }, + "recursion 4, plenty gas": { + // this uses 244708 gas + gasLimit: 400_000, + msg: Recurse{ + Depth: 4, + Work: 50, + }, + }, + "no recursion, external gas limit": { + gasLimit: 5000, // this is not enough + msg: Recurse{ + Work: 50, + }, + expectPanic: true, + }, + "recursion 4, external gas limit": { + // this uses 244708 gas but give less + gasLimit: 4 * GasWork50, + msg: Recurse{ + Depth: 4, + Work: 50, + }, + expectPanic: true, + }, + } + + contractAddr, _, ctx, keeper, cleanup := initRecurseContract(t) + defer cleanup() + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // set the external gas limit (normally from config file) + keeper.queryGasLimit = tc.gasLimit + + recurse := tc.msg + recurse.Contract = contractAddr + msg := buildRecurseQuery(t, recurse) + + // do the query + path := []string{QueryGetContractState, contractAddr.String(), QueryMethodContractStateSmart} + req := abci.RequestQuery{Data: msg} + if tc.expectPanic { + require.Panics(t, func() { + // this should run out of gas + _, err := NewQuerier(keeper)(ctx, path, req) + t.Logf("%v", err) + }) + } else { + // otherwise, make sure we get a good success + _, err := NewQuerier(keeper)(ctx, path, req) + require.NoError(t, err) + } + }) + } +} + +func TestLimitRecursiveQueryGas(t *testing.T) { + // The point of this test from https://github.com/CosmWasm/cosmwasm/issues/456 + // Basically, if I burn 90% of gas in CPU loop, then query out (to my self) + // the sub-query will have all the original gas (minus the 40k instance charge) + // and can burn 90% and call a sub-contract again... + // This attack would allow us to use far more than the provided gas before + // eventually hitting an OutOfGas panic. + + const ( + // Note: about 100 SDK gas (10k wasmer gas) for each round of sha256 + GasWork2k uint64 = InstanceCost + 233_575 // we have 6x gas used in cpu than in the instance + // This is overhead for calling into a sub-contract + GasReturnHashed uint64 = 349 + ) + + cases := map[string]struct { + gasLimit uint64 + msg Recurse + expectQueriesFromContract int + expectedGas uint64 + expectOutOfGas bool + }{ + "no recursion, lots of work": { + gasLimit: 4_000_000, + msg: Recurse{ + Depth: 0, + Work: 2000, + }, + expectQueriesFromContract: 0, + expectedGas: GasWork2k, + }, + "recursion 5, lots of work": { + gasLimit: 4_000_000, + msg: Recurse{ + Depth: 5, + Work: 2000, + }, + expectQueriesFromContract: 5, + // FIXME: why -3... confused a bit by calculations, seems like rounding issues + expectedGas: GasWork2k + 5*(GasWork2k+GasReturnHashed) - 2, + }, + // this is where we expect an error... + // it has enough gas to run 4 times and die on the 5th (4th time dispatching to sub-contract) + // however, if we don't charge the cpu gas before sub-dispatching, we can recurse over 20 times + // TODO: figure out how to asset how deep it went + "deep recursion, should die on 5th level": { + gasLimit: 1_200_000, + msg: Recurse{ + Depth: 50, + Work: 2000, + }, + expectQueriesFromContract: 4, + expectOutOfGas: true, + }, + } + + contractAddr, _, ctx, keeper, cleanup := initRecurseContract(t) + defer cleanup() + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // reset the counter before test + totalWasmQueryCounter = 0 + + // make sure we set a limit before calling + ctx = ctx.WithGasMeter(sdk.NewGasMeter(tc.gasLimit)) + require.Equal(t, uint64(0), ctx.GasMeter().GasConsumed()) + + // prepare the query + recurse := tc.msg + recurse.Contract = contractAddr + msg := buildRecurseQuery(t, recurse) + + // if we expect out of gas, make sure this panics + if tc.expectOutOfGas { + require.Panics(t, func() { + _, err := keeper.QuerySmart(ctx, contractAddr, msg) + t.Logf("Got error not panic: %#v", err) + }) + assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter) + return + } + + // otherwise, we expect a successful call + _, err := keeper.QuerySmart(ctx, contractAddr, msg) + require.NoError(t, err) + assert.Equal(t, tc.expectedGas, ctx.GasMeter().GasConsumed()) + + assert.Equal(t, tc.expectQueriesFromContract, totalWasmQueryCounter) + }) + } +} diff --git a/x/wasm/internal/keeper/reflect_test.go b/x/wasm/internal/keeper/reflect_test.go new file mode 100644 index 0000000000..6f074c632f --- /dev/null +++ b/x/wasm/internal/keeper/reflect_test.go @@ -0,0 +1,547 @@ +// nolint: staticcheck, unused, deadcode +package keeper + +import ( + "encoding/json" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth" + + wasmTypes "github.com/CosmWasm/wasmvm/types" + + "github.com/line/lbm-sdk/v2/x/coin" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +// MaskInitMsg is {} + +// MaskHandleMsg is used to encode handle messages +type MaskHandleMsg struct { + Reflect *reflectPayload `json:"reflect_msg,omitempty"` + Change *ownerPayload `json:"change_owner,omitempty"` +} + +type ownerPayload struct { + Owner sdk.Address `json:"owner"` +} + +type reflectPayload struct { + Msgs []wasmTypes.CosmosMsg `json:"msgs"` +} + +// MaskQueryMsg is used to encode query messages +type MaskQueryMsg struct { + Owner *struct{} `json:"owner,omitempty"` + Capitalized *Text `json:"capitalized,omitempty"` + Chain *ChainQuery `json:"chain,omitempty"` +} + +type ChainQuery struct { + Request *wasmTypes.QueryRequest `json:"request,omitempty"` +} + +type Text struct { + Text string `json:"text"` +} + +type OwnerResponse struct { + Owner string `json:"owner,omitempty"` +} + +type ChainResponse struct { + Data []byte `json:"data,omitempty"` +} + +func buildMaskQuery(t *testing.T, query *MaskQueryMsg) []byte { + bz, err := json.Marshal(query) + require.NoError(t, err) + return bz +} + +func mustParse(t *testing.T, data []byte, res interface{}) { + err := json.Unmarshal(data, res) + require.NoError(t, err) +} + +const MaskFeatures = "staking,mask" + +func TestMaskReflectContractSend(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, MaskFeatures, maskEncoders(MakeTestCodec()), nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + _, _, bob := keyPubAddr() + + // upload mask code + maskCode, err := ioutil.ReadFile("./testdata/reflect.wasm") + require.NoError(t, err) + maskID, err := keeper.Create(ctx, creator, maskCode, "", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), maskID) + + // upload hackatom escrow code + escrowCode, err := ioutil.ReadFile("./testdata/hackatom.wasm") + require.NoError(t, err) + escrowID, err := keeper.Create(ctx, creator, escrowCode, "", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(2), escrowID) + + // creator instantiates a contract and gives it tokens + maskStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + maskAddr, err := keeper.Instantiate(ctx, maskID, creator, nil, []byte("{}"), "mask contract 2", maskStart) + require.NoError(t, err) + require.NotEmpty(t, maskAddr) + + // now we set contract as verifier of an escrow + initMsg := InitMsg{ + Verifier: maskAddr, + Beneficiary: bob, + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + escrowStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 25000)) + escrowAddr, err := keeper.Instantiate(ctx, escrowID, creator, nil, initMsgBz, "escrow contract 2", escrowStart) + require.NoError(t, err) + require.NotEmpty(t, escrowAddr) + + // let's make sure all balances make sense + checkAccount(t, ctx, accKeeper, creator, sdk.NewCoins(sdk.NewInt64Coin("denom", 35000))) // 100k - 40k - 25k + checkAccount(t, ctx, accKeeper, maskAddr, maskStart) + checkAccount(t, ctx, accKeeper, escrowAddr, escrowStart) + checkAccount(t, ctx, accKeeper, bob, nil) + + // now for the trick.... we reflect a message through the mask to call the escrow + // we also send an additional 14k tokens there. + // this should reduce the mask balance by 14k (to 26k) + // this 14k is added to the escrow, then the entire balance is sent to bob (total: 39k) + approveMsg := []byte(`{"release":{}}`) + msgs := []wasmTypes.CosmosMsg{{ + Wasm: &wasmTypes.WasmMsg{ + Execute: &wasmTypes.ExecuteMsg{ + ContractAddr: escrowAddr.String(), + Msg: approveMsg, + Send: []wasmTypes.Coin{{ + Denom: "denom", + Amount: "14000", + }}, + }, + }, + }} + reflectSend := MaskHandleMsg{ + Reflect: &reflectPayload{ + Msgs: msgs, + }, + } + reflectSendBz, err := json.Marshal(reflectSend) + require.NoError(t, err) + _, err = keeper.Execute(ctx, maskAddr, creator, reflectSendBz, nil) + require.NoError(t, err) + + // did this work??? + checkAccount(t, ctx, accKeeper, creator, sdk.NewCoins(sdk.NewInt64Coin("denom", 35000))) // same as before + checkAccount(t, ctx, accKeeper, maskAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 26000))) // 40k - 14k (from send) + checkAccount(t, ctx, accKeeper, escrowAddr, sdk.Coins{}) // emptied reserved + checkAccount(t, ctx, accKeeper, bob, sdk.NewCoins(sdk.NewInt64Coin("denom", 39000))) // all escrow of 25k + 14k +} + +func TestMaskReflectCustomMsg(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, MaskFeatures, maskEncoders(MakeTestCodec()), maskPlugins()) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + bob := createFakeFundedAccount(ctx, accKeeper, deposit) + _, _, fred := keyPubAddr() + + // upload code + maskCode, err := ioutil.ReadFile("./testdata/reflect.wasm") + require.NoError(t, err) + codeID, err := keeper.Create(ctx, creator, maskCode, "", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), codeID) + + // creator instantiates a contract and gives it tokens + contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + contractAddr, err := keeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "mask contract 1", contractStart) + require.NoError(t, err) + require.NotEmpty(t, contractAddr) + + // set owner to bob + transfer := MaskHandleMsg{ + Change: &ownerPayload{ + Owner: bob, + }, + } + transferBz, err := json.Marshal(transfer) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, creator, transferBz, nil) + require.NoError(t, err) + + // check some account values + checkAccount(t, ctx, accKeeper, contractAddr, contractStart) + checkAccount(t, ctx, accKeeper, bob, deposit) + checkAccount(t, ctx, accKeeper, fred, nil) + + // bob can send contract's tokens to fred (using SendMsg) + msgs := []wasmTypes.CosmosMsg{{ + Bank: &wasmTypes.BankMsg{ + Send: &wasmTypes.SendMsg{ + FromAddress: contractAddr.String(), + ToAddress: fred.String(), + Amount: []wasmTypes.Coin{{ + Denom: "denom", + Amount: "15000", + }}, + }, + }, + }} + reflectSend := MaskHandleMsg{ + Reflect: &reflectPayload{ + Msgs: msgs, + }, + } + reflectSendBz, err := json.Marshal(reflectSend) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, bob, reflectSendBz, nil) + require.NoError(t, err) + + // fred got coins + checkAccount(t, ctx, accKeeper, fred, sdk.NewCoins(sdk.NewInt64Coin("denom", 15000))) + // contract lost them + checkAccount(t, ctx, accKeeper, contractAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 25000))) + checkAccount(t, ctx, accKeeper, bob, deposit) + + // construct an opaque message + var sdkSendMsg sdk.Msg = &coin.MsgSend{ + From: contractAddr, + To: fred, + Amount: sdk.NewCoins(sdk.NewInt64Coin("denom", 23000)), + } + opaque, err := toMaskRawMsg(keeper.cdc, sdkSendMsg) + require.NoError(t, err) + reflectOpaque := MaskHandleMsg{ + Reflect: &reflectPayload{ + Msgs: []wasmTypes.CosmosMsg{opaque}, + }, + } + reflectOpaqueBz, err := json.Marshal(reflectOpaque) + require.NoError(t, err) + + _, err = keeper.Execute(ctx, contractAddr, bob, reflectOpaqueBz, nil) + require.NoError(t, err) + + // fred got more coins + checkAccount(t, ctx, accKeeper, fred, sdk.NewCoins(sdk.NewInt64Coin("denom", 38000))) + // contract lost them + checkAccount(t, ctx, accKeeper, contractAddr, sdk.NewCoins(sdk.NewInt64Coin("denom", 2000))) + checkAccount(t, ctx, accKeeper, bob, deposit) +} + +func TestMaskReflectCustomQuery(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, MaskFeatures, maskEncoders(MakeTestCodec()), maskPlugins()) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + // upload code + maskCode, err := ioutil.ReadFile("./testdata/reflect.wasm") + require.NoError(t, err) + codeID, err := keeper.Create(ctx, creator, maskCode, "", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), codeID) + + // creator instantiates a contract and gives it tokens + contractStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + contractAddr, err := keeper.Instantiate(ctx, codeID, creator, nil, []byte("{}"), "mask contract 1", contractStart) + require.NoError(t, err) + require.NotEmpty(t, contractAddr) + + // let's perform a normal query of state + ownerQuery := MaskQueryMsg{ + Owner: &struct{}{}, + } + ownerQueryBz, err := json.Marshal(ownerQuery) + require.NoError(t, err) + ownerRes, err := keeper.QuerySmart(ctx, contractAddr, ownerQueryBz) + require.NoError(t, err) + var res OwnerResponse + err = json.Unmarshal(ownerRes, &res) + require.NoError(t, err) + assert.Equal(t, res.Owner, creator.String()) + + // and now making use of the custom querier callbacks + customQuery := MaskQueryMsg{ + Capitalized: &Text{ + Text: "all Caps noW", + }, + } + customQueryBz, err := json.Marshal(customQuery) + require.NoError(t, err) + custom, err := keeper.QuerySmart(ctx, contractAddr, customQueryBz) + require.NoError(t, err) + var resp capitalizedResponse + err = json.Unmarshal(custom, &resp) + require.NoError(t, err) + assert.Equal(t, resp.Text, "ALL CAPS NOW") +} + +type maskState struct { + Owner []byte `json:"owner"` +} + +func TestMaskReflectWasmQueries(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, MaskFeatures, maskEncoders(MakeTestCodec()), nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + // upload mask code + maskCode, err := ioutil.ReadFile("./testdata/reflect.wasm") + require.NoError(t, err) + maskID, err := keeper.Create(ctx, creator, maskCode, "", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), maskID) + + // creator instantiates a contract and gives it tokens + maskStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + maskAddr, err := keeper.Instantiate(ctx, maskID, creator, nil, []byte("{}"), "mask contract 2", maskStart) + require.NoError(t, err) + require.NotEmpty(t, maskAddr) + + // for control, let's make some queries directly on the mask + ownerQuery := buildMaskQuery(t, &MaskQueryMsg{Owner: &struct{}{}}) + res, err := keeper.QuerySmart(ctx, maskAddr, ownerQuery) + require.NoError(t, err) + var ownerRes OwnerResponse + mustParse(t, res, &ownerRes) + require.Equal(t, ownerRes.Owner, creator.String()) + + // and a raw query: cosmwasm_storage::Singleton uses 2 byte big-endian length-prefixed to store data + configKey := append([]byte{0, 6}, []byte("config")...) + raw := keeper.QueryRaw(ctx, maskAddr, configKey) + var stateRes maskState + mustParse(t, raw, &stateRes) + require.Equal(t, stateRes.Owner, []byte(creator)) + + // now, let's reflect a smart query into the x/wasm handlers and see if we get the same result + reflectOwnerQuery := MaskQueryMsg{Chain: &ChainQuery{Request: &wasmTypes.QueryRequest{Wasm: &wasmTypes.WasmQuery{ + Smart: &wasmTypes.SmartQuery{ + ContractAddr: maskAddr.String(), + Msg: ownerQuery, + }, + }}}} + reflectOwnerBin := buildMaskQuery(t, &reflectOwnerQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectOwnerBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + var reflectRes ChainResponse + mustParse(t, res, &reflectRes) + var reflectOwnerRes OwnerResponse + mustParse(t, reflectRes.Data, &reflectOwnerRes) + require.Equal(t, reflectOwnerRes.Owner, creator.String()) + + // and with queryRaw + reflectStateQuery := MaskQueryMsg{Chain: &ChainQuery{Request: &wasmTypes.QueryRequest{Wasm: &wasmTypes.WasmQuery{ + Raw: &wasmTypes.RawQuery{ + ContractAddr: maskAddr.String(), + Key: configKey, + }, + }}}} + reflectStateBin := buildMaskQuery(t, &reflectStateQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectStateBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + var reflectRawRes ChainResponse + mustParse(t, res, &reflectRawRes) + // now, with the raw data, we can parse it into state + var reflectStateRes maskState + mustParse(t, reflectRawRes.Data, &reflectStateRes) + require.Equal(t, reflectStateRes.Owner, []byte(creator)) +} + +func TestWasmRawQueryWithNil(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, MaskFeatures, maskEncoders(MakeTestCodec()), nil) + accKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + // upload mask code + maskCode, err := ioutil.ReadFile("./testdata/reflect.wasm") + require.NoError(t, err) + maskID, err := keeper.Create(ctx, creator, maskCode, "", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), maskID) + + // creator instantiates a contract and gives it tokens + maskStart := sdk.NewCoins(sdk.NewInt64Coin("denom", 40000)) + maskAddr, err := keeper.Instantiate(ctx, maskID, creator, nil, []byte("{}"), "mask contract 2", maskStart) + require.NoError(t, err) + require.NotEmpty(t, maskAddr) + + // control: query directly + missingKey := []byte{0, 1, 2, 3, 4} + raw := keeper.QueryRaw(ctx, maskAddr, missingKey) + require.Nil(t, raw) + + // and with queryRaw + reflectQuery := MaskQueryMsg{Chain: &ChainQuery{Request: &wasmTypes.QueryRequest{Wasm: &wasmTypes.WasmQuery{ + Raw: &wasmTypes.RawQuery{ + ContractAddr: maskAddr.String(), + Key: missingKey, + }, + }}}} + reflectStateBin := buildMaskQuery(t, &reflectQuery) + res, err := keeper.QuerySmart(ctx, maskAddr, reflectStateBin) + require.NoError(t, err) + + // first we pull out the data from chain response, before parsing the original response + var reflectRawRes ChainResponse + mustParse(t, res, &reflectRawRes) + // and make sure there is no data + require.Empty(t, reflectRawRes.Data) + // we get an empty byte slice not nil (if anyone care in go-land) + require.Equal(t, []byte{}, reflectRawRes.Data) +} + +func checkAccount(t *testing.T, ctx sdk.Context, accKeeper auth.AccountKeeper, addr sdk.AccAddress, expected sdk.Coins) { + acct := accKeeper.GetAccount(ctx, addr) + if expected == nil { + assert.Nil(t, acct) + } else { + assert.NotNil(t, acct) + if expected.Empty() { + // there is confusion between nil and empty slice... let's just treat them the same + assert.True(t, acct.GetCoins().Empty()) + } else { + assert.Equal(t, acct.GetCoins(), expected) + } + } +} + +/**** Code to support custom messages *****/ + +type maskCustomMsg struct { + Debug string `json:"debug,omitempty"` + Raw []byte `json:"raw,omitempty"` +} + +// toMaskRawMsg encodes an sdk msg using amino json encoding. +// Then wraps it as an opaque message +func toMaskRawMsg(cdc *codec.Codec, msg sdk.Msg) (wasmTypes.CosmosMsg, error) { + rawBz, err := cdc.MarshalJSON(msg) + if err != nil { + return wasmTypes.CosmosMsg{}, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + customMsg, err := json.Marshal(maskCustomMsg{ + Raw: rawBz, + }) + res := wasmTypes.CosmosMsg{ + Custom: customMsg, + } + return res, nil +} + +// maskEncoders needs to be registered in test setup to handle custom message callbacks +func maskEncoders(cdc *codec.Codec) *MessageEncoders { + return &MessageEncoders{ + Custom: fromMaskRawMsg(cdc), + } +} + +// fromMaskRawMsg decodes msg.Data to an sdk.Msg using amino json encoding. +// this needs to be registered on the Encoders +func fromMaskRawMsg(cdc *codec.Codec) CustomEncoder { + return func(_sender sdk.AccAddress, msg json.RawMessage, router types.Router) ([]sdk.Msg, error) { + var custom maskCustomMsg + err := json.Unmarshal(msg, &custom) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + if custom.Raw != nil { + var sdkMsg sdk.Msg + err := cdc.UnmarshalJSON(custom.Raw, &sdkMsg) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + return []sdk.Msg{sdkMsg}, nil + } + if custom.Debug != "" { + return nil, sdkerrors.Wrapf(types.ErrInvalidMsg, "Custom Debug: %s", custom.Debug) + } + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown Custom message variant") + } +} + +type maskCustomQuery struct { + Ping *struct{} `json:"ping,omitempty"` + Capitalized *Text `json:"capitalized,omitempty"` +} + +// this is from the go code back to the contract (capitalized or ping) +type customQueryResponse struct { + Msg string `json:"msg"` +} + +// these are the return values from contract -> go depending on type of query +type ownerResponse struct { + Owner string `json:"owner"` +} + +type capitalizedResponse struct { + Text string `json:"text"` +} + +type chainResponse struct { + Data []byte `json:"data"` +} + +// maskPlugins needs to be registered in test setup to handle custom query callbacks +func maskPlugins() *QueryPlugins { + return &QueryPlugins{ + Custom: performCustomQuery, + } +} + +func performCustomQuery(_ sdk.Context, request json.RawMessage) ([]byte, error) { + var custom maskCustomQuery + err := json.Unmarshal(request, &custom) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) + } + if custom.Capitalized != nil { + msg := strings.ToUpper(custom.Capitalized.Text) + return json.Marshal(customQueryResponse{Msg: msg}) + } + if custom.Ping != nil { + return json.Marshal(customQueryResponse{Msg: "pong"}) + } + return nil, sdkerrors.Wrap(types.ErrInvalidMsg, "Unknown Custom query variant") +} diff --git a/x/wasm/internal/keeper/staking_test.go b/x/wasm/internal/keeper/staking_test.go new file mode 100644 index 0000000000..b04faea0d9 --- /dev/null +++ b/x/wasm/internal/keeper/staking_test.go @@ -0,0 +1,715 @@ +// nolint: unused +package keeper + +import ( + "encoding/json" + "io/ioutil" + "os" + "testing" + + wasmTypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type StakingInitMsg struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + Decimals uint8 `json:"decimals"` + Validator sdk.ValAddress `json:"validator"` + ExitTax sdk.Dec `json:"exit_tax"` + // MinWithdrawal is uint128 encoded as a string (use sdk.Int?) + MinWithdrawl string `json:"min_withdrawal"` +} + +// StakingHandleMsg is used to encode handle messages +type StakingHandleMsg struct { + Transfer *transferPayload `json:"transfer,omitempty"` + Bond *struct{} `json:"bond,omitempty"` + Unbond *unbondPayload `json:"unbond,omitempty"` + Claim *struct{} `json:"claim,omitempty"` + Reinvest *struct{} `json:"reinvest,omitempty"` + Change *ownerPayload `json:"change_owner,omitempty"` +} + +type transferPayload struct { + Recipient sdk.Address `json:"recipient"` + // uint128 encoded as string + Amount string `json:"amount"` +} + +type unbondPayload struct { + // uint128 encoded as string + Amount string `json:"amount"` +} + +// StakingQueryMsg is used to encode query messages +type StakingQueryMsg struct { + Balance *addressQuery `json:"balance,omitempty"` + Claims *addressQuery `json:"claims,omitempty"` + TokenInfo *struct{} `json:"token_info,omitempty"` + Investment *struct{} `json:"investment,omitempty"` +} + +type addressQuery struct { + Address sdk.AccAddress `json:"address"` +} + +type BalanceResponse struct { + Balance string `json:"balance,omitempty"` +} + +type ClaimsResponse struct { + Claims string `json:"claims,omitempty"` +} + +type TokenInfoResponse struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + Decimals uint8 `json:"decimals"` +} + +type InvestmentResponse struct { + TokenSupply string `json:"token_supply"` + StakedTokens sdk.Coin `json:"staked_tokens"` + NominalValue sdk.Dec `json:"nominal_value"` + Owner sdk.AccAddress `json:"owner"` + Validator sdk.ValAddress `json:"validator"` + ExitTax sdk.Dec `json:"exit_tax"` + // MinWithdrwal is uint128 encoded as a string (use sdk.Int?) + MinWithdrwal string `json:"min_withdrwal"` +} + +func TestInitializeStaking(t *testing.T) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, stakingKeeper, keeper := keepers.AccountKeeper, keepers.StakingKeeper, keepers.WasmKeeper + + valAddr := addValidator(ctx, stakingKeeper, accKeeper, sdk.NewInt64Coin("stake", 1234567)) + ctx = nextBlock(ctx, stakingKeeper) + v, found := stakingKeeper.GetValidator(ctx, valAddr) + assert.True(t, found) + assert.Equal(t, v.GetDelegatorShares(), sdk.NewDec(1234567)) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000), sdk.NewInt64Coin("stake", 500000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + // upload staking derivates code + stakingCode, err := ioutil.ReadFile("./testdata/staking.wasm") + require.NoError(t, err) + stakingID, err := keeper.Create(ctx, creator, stakingCode, "", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), stakingID) + + // register to a valid address + initMsg := StakingInitMsg{ + Name: "Staking Derivatives", + Symbol: "DRV", + Decimals: 0, + Validator: valAddr, + ExitTax: sdk.MustNewDecFromStr("0.10"), + MinWithdrawl: "100", + } + initBz, err := json.Marshal(&initMsg) + require.NoError(t, err) + + stakingAddr, err := keeper.Instantiate(ctx, stakingID, creator, nil, initBz, "staking derivates - DRV", nil) + require.NoError(t, err) + require.NotEmpty(t, stakingAddr) + + // nothing spent here + checkAccount(t, ctx, accKeeper, creator, deposit) + + // try to register with a validator not on the list and it fails + _, _, bob := keyPubAddr() + badInitMsg := StakingInitMsg{ + Name: "Missing Validator", + Symbol: "MISS", + Decimals: 0, + Validator: sdk.ValAddress(bob), + ExitTax: sdk.MustNewDecFromStr("0.10"), + MinWithdrawl: "100", + } + badBz, err := json.Marshal(&badInitMsg) + require.NoError(t, err) + + _, err = keeper.Instantiate(ctx, stakingID, creator, nil, badBz, "missing validator", nil) + require.Error(t, err) + + // no changes to bonding shares + val, _ := stakingKeeper.GetValidator(ctx, valAddr) + assert.Equal(t, val.GetDelegatorShares(), sdk.NewDec(1234567)) +} + +type initInfo struct { + valAddr sdk.ValAddress + creator sdk.AccAddress + contractAddr sdk.AccAddress + + ctx sdk.Context + accKeeper auth.AccountKeeper + stakingKeeper staking.Keeper + distKeeper distribution.Keeper + wasmKeeper Keeper + + cleanup func() +} + +func initializeStaking(t *testing.T) initInfo { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + ctx, keepers := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil) + accKeeper, stakingKeeper, keeper := keepers.AccountKeeper, keepers.StakingKeeper, keepers.WasmKeeper + + valAddr := addValidator(ctx, stakingKeeper, accKeeper, sdk.NewInt64Coin("stake", 1000000)) + ctx = nextBlock(ctx, stakingKeeper) + + // set some baseline - this seems to be needed + keepers.DistKeeper.SetValidatorHistoricalRewards(ctx, valAddr, 0, distribution.ValidatorHistoricalRewards{ + CumulativeRewardRatio: sdk.DecCoins{}, + ReferenceCount: 1, + }) + + v, found := stakingKeeper.GetValidator(ctx, valAddr) + assert.True(t, found) + assert.Equal(t, v.GetDelegatorShares(), sdk.NewDec(1000000)) + assert.Equal(t, v.Status, sdk.Bonded) + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000), sdk.NewInt64Coin("stake", 500000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + // upload staking derivates code + stakingCode, err := ioutil.ReadFile("./testdata/staking.wasm") + require.NoError(t, err) + stakingID, err := keeper.Create(ctx, creator, stakingCode, "", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(1), stakingID) + + // register to a valid address + initMsg := StakingInitMsg{ + Name: "Staking Derivatives", + Symbol: "DRV", + Decimals: 0, + Validator: valAddr, + ExitTax: sdk.MustNewDecFromStr("0.10"), + MinWithdrawl: "100", + } + initBz, err := json.Marshal(&initMsg) + require.NoError(t, err) + + stakingAddr, err := keeper.Instantiate(ctx, stakingID, creator, nil, initBz, "staking derivates - DRV", nil) + require.NoError(t, err) + require.NotEmpty(t, stakingAddr) + + return initInfo{ + valAddr: valAddr, + creator: creator, + contractAddr: stakingAddr, + ctx: ctx, + accKeeper: accKeeper, + stakingKeeper: stakingKeeper, + wasmKeeper: keeper, + distKeeper: keepers.DistKeeper, + cleanup: func() { os.RemoveAll(tempDir) }, + } +} + +func TestBonding(t *testing.T) { + initInfo := initializeStaking(t) + defer initInfo.cleanup() + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + keeper, stakingKeeper, accKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + initPower := val.GetDelegatorShares() + + // bob has 160k, putting 80k into the contract + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 160000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 80000)) + bob := createFakeFundedAccount(ctx, accKeeper, full) + + // check contract state before + assertBalance(t, ctx, keeper, contractAddr, bob, "0") + assertClaims(t, ctx, keeper, contractAddr, bob, "0") + assertSupply(t, ctx, keeper, contractAddr, "0", sdk.NewInt64Coin("stake", 0)) + + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // check some account values - the money is on neither account (cuz it is bonded) + checkAccount(t, ctx, accKeeper, contractAddr, sdk.Coins{}) + checkAccount(t, ctx, accKeeper, bob, funds) + + // make sure the proper number of tokens have been bonded + val, _ = stakingKeeper.GetValidator(ctx, valAddr) + finalPower := val.GetDelegatorShares() + assert.Equal(t, sdk.NewInt(80000), finalPower.Sub(initPower).TruncateInt()) + + // check the delegation itself + d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr) + require.True(t, found) + assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("80000")) + + // check we have the desired balance + assertBalance(t, ctx, keeper, contractAddr, bob, "80000") + assertClaims(t, ctx, keeper, contractAddr, bob, "0") + assertSupply(t, ctx, keeper, contractAddr, "80000", sdk.NewInt64Coin("stake", 80000)) +} + +func TestUnbonding(t *testing.T) { + initInfo := initializeStaking(t) + defer initInfo.cleanup() + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + keeper, stakingKeeper, accKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + initPower := val.GetDelegatorShares() + + // bob has 160k, putting 80k into the contract + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 160000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 80000)) + bob := createFakeFundedAccount(ctx, accKeeper, full) + + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // update height a bit + ctx = nextBlock(ctx, stakingKeeper) + + // now unbond 30k - note that 3k (10%) goes to the owner as a tax, 27k unbonded and available as claims + unbond := StakingHandleMsg{ + Unbond: &unbondPayload{ + Amount: "30000", + }, + } + unbondBz, err := json.Marshal(unbond) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, bob, unbondBz, nil) + require.NoError(t, err) + + // check some account values - the money is on neither account (cuz it is bonded) + // Note: why is this immediate? just test setup? + checkAccount(t, ctx, accKeeper, contractAddr, sdk.Coins{}) + checkAccount(t, ctx, accKeeper, bob, funds) + + // make sure the proper number of tokens have been bonded (80k - 27k = 53k) + val, _ = stakingKeeper.GetValidator(ctx, valAddr) + finalPower := val.GetDelegatorShares() + assert.Equal(t, sdk.NewInt(53000), finalPower.Sub(initPower).TruncateInt(), finalPower.String()) + + // check the delegation itself + d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr) + require.True(t, found) + assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("53000")) + + // check there is unbonding in progress + un, found := stakingKeeper.GetUnbondingDelegation(ctx, contractAddr, valAddr) + require.True(t, found) + require.Equal(t, 1, len(un.Entries)) + assert.Equal(t, "27000", un.Entries[0].Balance.String()) + + // check we have the desired balance + assertBalance(t, ctx, keeper, contractAddr, bob, "50000") + assertBalance(t, ctx, keeper, contractAddr, initInfo.creator, "3000") + assertClaims(t, ctx, keeper, contractAddr, bob, "27000") + assertSupply(t, ctx, keeper, contractAddr, "53000", sdk.NewInt64Coin("stake", 53000)) +} + +func TestReinvest(t *testing.T) { + initInfo := initializeStaking(t) + defer initInfo.cleanup() + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + keeper, stakingKeeper, accKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper + distKeeper := initInfo.distKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + initPower := val.GetDelegatorShares() + assert.Equal(t, val.Tokens, sdk.NewInt(1000000), "%s", val.Tokens) + + // full is 2x funds, 1x goes to the contract, other stays on his wallet + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 400000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 200000)) + bob := createFakeFundedAccount(ctx, accKeeper, full) + + // we will stake 200k to a validator with 1M self-bond + // this means we should get 1/6 of the rewards + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // update height a bit to solidify the delegation + ctx = nextBlock(ctx, stakingKeeper) + // we get 1/6, our share should be 40k minus 10% commission = 36k + setValidatorRewards(ctx, stakingKeeper, distKeeper, valAddr, "240000") + + // this should withdraw our outstanding 36k of rewards and reinvest them in the same delegation + reinvest := StakingHandleMsg{ + Reinvest: &struct{}{}, + } + reinvestBz, err := json.Marshal(reinvest) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, bob, reinvestBz, nil) + require.NoError(t, err) + + // check some account values - the money is on neither account (cuz it is bonded) + // Note: why is this immediate? just test setup? + checkAccount(t, ctx, accKeeper, contractAddr, sdk.Coins{}) + checkAccount(t, ctx, accKeeper, bob, funds) + + // check the delegation itself + d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr) + require.True(t, found) + // we started with 200k and added 36k + assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("236000")) + + // make sure the proper number of tokens have been bonded (80k + 40k = 120k) + val, _ = stakingKeeper.GetValidator(ctx, valAddr) + finalPower := val.GetDelegatorShares() + assert.Equal(t, sdk.NewInt(236000), finalPower.Sub(initPower).TruncateInt(), finalPower.String()) + + // check there is no unbonding in progress + un, found := stakingKeeper.GetUnbondingDelegation(ctx, contractAddr, valAddr) + assert.False(t, found, "%#v", un) + + // check we have the desired balance + assertBalance(t, ctx, keeper, contractAddr, bob, "200000") + assertBalance(t, ctx, keeper, contractAddr, initInfo.creator, "0") + assertClaims(t, ctx, keeper, contractAddr, bob, "0") + assertSupply(t, ctx, keeper, contractAddr, "200000", sdk.NewInt64Coin("stake", 236000)) +} + +func TestQueryStakingInfo(t *testing.T) { + // STEP 1: take a lot of setup from TestReinvest so we have non-zero info + initInfo := initializeStaking(t) + defer initInfo.cleanup() + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + keeper, stakingKeeper, accKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper + distKeeper := initInfo.distKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + assert.Equal(t, sdk.NewInt(1000000), val.Tokens) + + // full is 2x funds, 1x goes to the contract, other stays on his wallet + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 400000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 200000)) + bob := createFakeFundedAccount(ctx, accKeeper, full) + + // we will stake 200k to a validator with 1M self-bond + // this means we should get 1/6 of the rewards + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // update height a bit to solidify the delegation + ctx = nextBlock(ctx, stakingKeeper) + // we get 1/6, our share should be 40k minus 10% commission = 36k + setValidatorRewards(ctx, stakingKeeper, distKeeper, valAddr, "240000") + + // see what the current rewards are + origReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr) + + // STEP 2: Prepare the mask contract + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(ctx, accKeeper, deposit) + + // upload mask code + maskCode, err := ioutil.ReadFile("./testdata/reflect.wasm") + require.NoError(t, err) + maskID, err := keeper.Create(ctx, creator, maskCode, "", "", nil) + require.NoError(t, err) + require.Equal(t, uint64(2), maskID) + + // creator instantiates a contract and gives it tokens + maskAddr, err := keeper.Instantiate(ctx, maskID, creator, nil, []byte("{}"), "mask contract 2", nil) + require.NoError(t, err) + require.NotEmpty(t, maskAddr) + + // STEP 3: now, let's reflect some queries. + // let's get the bonded denom + reflectBondedQuery := MaskQueryMsg{Chain: &ChainQuery{Request: &wasmTypes.QueryRequest{Staking: &wasmTypes.StakingQuery{ + BondedDenom: &struct{}{}, + }}}} + reflectBondedBin := buildMaskQuery(t, &reflectBondedQuery) + res, err := keeper.QuerySmart(ctx, maskAddr, reflectBondedBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + var reflectRes ChainResponse + mustParse(t, res, &reflectRes) + var bondedRes wasmTypes.BondedDenomResponse + mustParse(t, reflectRes.Data, &bondedRes) + assert.Equal(t, "stake", bondedRes.Denom) + + // now, let's reflect a smart query into the x/wasm handlers and see if we get the same result + reflectValidatorsQuery := MaskQueryMsg{Chain: &ChainQuery{Request: &wasmTypes.QueryRequest{Staking: &wasmTypes.StakingQuery{ + Validators: &wasmTypes.ValidatorsQuery{}, + }}}} + reflectValidatorsBin := buildMaskQuery(t, &reflectValidatorsQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectValidatorsBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + mustParse(t, res, &reflectRes) + var validatorRes wasmTypes.ValidatorsResponse + mustParse(t, reflectRes.Data, &validatorRes) + require.Len(t, validatorRes.Validators, 1) + valInfo := validatorRes.Validators[0] + // Note: this ValAddress not AccAddress, may change with #264 + require.Equal(t, valAddr.String(), valInfo.Address) + require.Contains(t, valInfo.Commission, "0.100") + require.Contains(t, valInfo.MaxCommission, "0.200") + require.Contains(t, valInfo.MaxChangeRate, "0.010") + + // test to get all my delegations + reflectAllDelegationsQuery := MaskQueryMsg{Chain: &ChainQuery{Request: &wasmTypes.QueryRequest{Staking: &wasmTypes.StakingQuery{ + AllDelegations: &wasmTypes.AllDelegationsQuery{ + Delegator: contractAddr.String(), + }, + }}}} + reflectAllDelegationsBin := buildMaskQuery(t, &reflectAllDelegationsQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectAllDelegationsBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + mustParse(t, res, &reflectRes) + var allDelegationsRes wasmTypes.AllDelegationsResponse + mustParse(t, reflectRes.Data, &allDelegationsRes) + require.Len(t, allDelegationsRes.Delegations, 1) + delInfo := allDelegationsRes.Delegations[0] + // Note: this ValAddress not AccAddress, may change with #264 + require.Equal(t, valAddr.String(), delInfo.Validator) + // note this is not bob (who staked to the contract), but the contract itself + require.Equal(t, contractAddr.String(), delInfo.Delegator) + // this is a different Coin type, with String not BigInt, compare field by field + require.Equal(t, funds[0].Denom, delInfo.Amount.Denom) + require.Equal(t, funds[0].Amount.String(), delInfo.Amount.Amount) + + // test to get one delegations + reflectDelegationQuery := MaskQueryMsg{Chain: &ChainQuery{Request: &wasmTypes.QueryRequest{Staking: &wasmTypes.StakingQuery{ + Delegation: &wasmTypes.DelegationQuery{ + Validator: valAddr.String(), + Delegator: contractAddr.String(), + }, + }}}} + reflectDelegationBin := buildMaskQuery(t, &reflectDelegationQuery) + res, err = keeper.QuerySmart(ctx, maskAddr, reflectDelegationBin) + require.NoError(t, err) + // first we pull out the data from chain response, before parsing the original response + mustParse(t, res, &reflectRes) + var delegationRes wasmTypes.DelegationResponse + mustParse(t, reflectRes.Data, &delegationRes) + assert.NotEmpty(t, delegationRes.Delegation) + delInfo2 := delegationRes.Delegation + // Note: this ValAddress not AccAddress, may change with #264 + require.Equal(t, valAddr.String(), delInfo2.Validator) + // note this is not bob (who staked to the contract), but the contract itself + require.Equal(t, contractAddr.String(), delInfo2.Delegator) + // this is a different Coin type, with String not BigInt, compare field by field + require.Equal(t, funds[0].Denom, delInfo2.Amount.Denom) + require.Equal(t, funds[0].Amount.String(), delInfo2.Amount.Amount) + + require.Equal(t, wasmTypes.NewCoin(200000, "stake"), delInfo2.CanRedelegate) + require.Len(t, delInfo2.AccumulatedRewards, 1) + // see bonding above to see how we calculate 36000 (240000 / 6 - 10% commission) + require.Equal(t, wasmTypes.NewCoin(36000, "stake"), delInfo2.AccumulatedRewards[0]) + + // ensure rewards did not change when querying (neither amount nor period) + finalReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr) + require.Equal(t, origReward, finalReward) +} + +func TestQueryStakingPlugin(t *testing.T) { + // STEP 1: take a lot of setup from TestReinvest so we have non-zero info + initInfo := initializeStaking(t) + defer initInfo.cleanup() + ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr + keeper, stakingKeeper, accKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper + distKeeper := initInfo.distKeeper + + // initial checks of bonding state + val, found := stakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + assert.Equal(t, sdk.NewInt(1000000), val.Tokens) + + // full is 2x funds, 1x goes to the contract, other stays on his wallet + full := sdk.NewCoins(sdk.NewInt64Coin("stake", 400000)) + funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 200000)) + bob := createFakeFundedAccount(ctx, accKeeper, full) + + // we will stake 200k to a validator with 1M self-bond + // this means we should get 1/6 of the rewards + bond := StakingHandleMsg{ + Bond: &struct{}{}, + } + bondBz, err := json.Marshal(bond) + require.NoError(t, err) + _, err = keeper.Execute(ctx, contractAddr, bob, bondBz, funds) + require.NoError(t, err) + + // update height a bit to solidify the delegation + ctx = nextBlock(ctx, stakingKeeper) + // we get 1/6, our share should be 40k minus 10% commission = 36k + setValidatorRewards(ctx, stakingKeeper, distKeeper, valAddr, "240000") + + // see what the current rewards are + origReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr) + + // Step 2: Try out the query plugins + query := wasmTypes.StakingQuery{ + Delegation: &wasmTypes.DelegationQuery{ + Delegator: contractAddr.String(), + Validator: valAddr.String(), + }, + } + raw, err := StakingQuerier(stakingKeeper, distKeeper)(ctx, &query) + require.NoError(t, err) + var res wasmTypes.DelegationResponse + mustParse(t, raw, &res) + assert.NotEmpty(t, res.Delegation) + delInfo := res.Delegation + // Note: this ValAddress not AccAddress, may change with #264 + require.Equal(t, valAddr.String(), delInfo.Validator) + // note this is not bob (who staked to the contract), but the contract itself + require.Equal(t, contractAddr.String(), delInfo.Delegator) + // this is a different Coin type, with String not BigInt, compare field by field + require.Equal(t, funds[0].Denom, delInfo.Amount.Denom) + require.Equal(t, funds[0].Amount.String(), delInfo.Amount.Amount) + + require.Equal(t, wasmTypes.NewCoin(200000, "stake"), delInfo.CanRedelegate) + require.Len(t, delInfo.AccumulatedRewards, 1) + // see bonding above to see how we calculate 36000 (240000 / 6 - 10% commission) + require.Equal(t, wasmTypes.NewCoin(36000, "stake"), delInfo.AccumulatedRewards[0]) + + // ensure rewards did not change when querying (neither amount nor period) + finalReward := distKeeper.GetValidatorCurrentRewards(ctx, valAddr) + require.Equal(t, origReward, finalReward) +} + +// adds a few validators and returns a list of validators that are registered +func addValidator(ctx sdk.Context, stakingKeeper staking.Keeper, accountKeeper auth.AccountKeeper, value sdk.Coin) sdk.ValAddress { + _, pub, accAddr := keyPubAddr() + + addr := sdk.ValAddress(accAddr) + + owner := createFakeFundedAccount(ctx, accountKeeper, sdk.Coins{value}) + + msg := staking.MsgCreateValidator{ + Description: types.Description{ + Moniker: "Validator power", + }, + Commission: types.CommissionRates{ + Rate: sdk.MustNewDecFromStr("0.1"), + MaxRate: sdk.MustNewDecFromStr("0.2"), + MaxChangeRate: sdk.MustNewDecFromStr("0.01"), + }, + MinSelfDelegation: sdk.OneInt(), + DelegatorAddress: owner, + ValidatorAddress: addr, + PubKey: pub, + Value: value, + } + + h := staking.NewHandler(stakingKeeper) + _, err := h(ctx, msg) + if err != nil { + panic(err) + } + return addr +} + +// this will commit the current set, update the block height and set historic info +// basically, letting two blocks pass +func nextBlock(ctx sdk.Context, stakingKeeper staking.Keeper) sdk.Context { + staking.EndBlocker(ctx, stakingKeeper) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + staking.BeginBlocker(ctx, stakingKeeper) + return ctx +} + +func setValidatorRewards(ctx sdk.Context, stakingKeeper staking.Keeper, distKeeper distribution.Keeper, valAddr sdk.ValAddress, reward string) { + // allocate some rewards + vali := stakingKeeper.Validator(ctx, valAddr) + amount, err := sdk.NewDecFromStr(reward) + if err != nil { + panic(err) + } + payout := sdk.DecCoins{{Denom: "stake", Amount: amount}} + distKeeper.AllocateTokensToValidator(ctx, vali, payout) +} + +func assertBalance(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, addr sdk.AccAddress, expected string) { + query := StakingQueryMsg{ + Balance: &addressQuery{ + Address: addr, + }, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + res, err := keeper.QuerySmart(ctx, contract, queryBz) + require.NoError(t, err) + var balance BalanceResponse + err = json.Unmarshal(res, &balance) + require.NoError(t, err) + assert.Equal(t, expected, balance.Balance) +} + +func assertClaims(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, addr sdk.AccAddress, expected string) { + query := StakingQueryMsg{ + Claims: &addressQuery{ + Address: addr, + }, + } + queryBz, err := json.Marshal(query) + require.NoError(t, err) + res, err := keeper.QuerySmart(ctx, contract, queryBz) + require.NoError(t, err) + var claims ClaimsResponse + err = json.Unmarshal(res, &claims) + require.NoError(t, err) + assert.Equal(t, expected, claims.Claims) +} + +func assertSupply(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, expectedIssued string, expectedBonded sdk.Coin) { + query := StakingQueryMsg{Investment: &struct{}{}} + queryBz, err := json.Marshal(query) + require.NoError(t, err) + res, err := keeper.QuerySmart(ctx, contract, queryBz) + require.NoError(t, err) + var invest InvestmentResponse + err = json.Unmarshal(res, &invest) + require.NoError(t, err) + assert.Equal(t, expectedIssued, invest.TokenSupply) + assert.Equal(t, expectedBonded, invest.StakedTokens) +} diff --git a/x/wasm/internal/keeper/test_common.go b/x/wasm/internal/keeper/test_common.go new file mode 100644 index 0000000000..f04bc834a9 --- /dev/null +++ b/x/wasm/internal/keeper/test_common.go @@ -0,0 +1,258 @@ +// nolint: deadcode, varcheck, unused +package keeper + +import ( + "fmt" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/gov" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + "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/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/supply" + + "github.com/line/lbm-sdk/v2/x/coin" + wasmtypes "github.com/line/lbm-sdk/v2/x/wasm/internal/types" +) + +const flagLRUCacheSize = "lru_size" +const flagQueryGasLimit = "query_gas_limit" + +func MakeTestCodec() *codec.Codec { + var cdc = codec.New() + + // Register AppAccount + // cdc.RegisterInterface((*authexported.Account)(nil), nil) + // cdc.RegisterConcrete(&auth.BaseAccount{}, "test/wasm/BaseAccount", nil) + auth.AppModuleBasic{}.RegisterCodec(cdc) + bank.AppModuleBasic{}.RegisterCodec(cdc) + coin.AppModuleBasic{}.RegisterCodec(cdc) + supply.AppModuleBasic{}.RegisterCodec(cdc) + staking.AppModuleBasic{}.RegisterCodec(cdc) + distribution.AppModuleBasic{}.RegisterCodec(cdc) + gov.RegisterCodec(cdc) + wasmtypes.RegisterCodec(cdc) + sdk.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + params.RegisterCodec(cdc) + return cdc +} + +var TestingStakeParams = staking.Params{ + UnbondingTime: 100, + MaxValidators: 10, + MaxEntries: 10, + HistoricalEntries: 10, + BondDenom: "stake", +} + +type TestKeepers struct { + AccountKeeper auth.AccountKeeper + StakingKeeper staking.Keeper + WasmKeeper Keeper + DistKeeper distribution.Keeper + SupplyKeeper supply.Keeper + GovKeeper gov.Keeper + BankKeeper wasmtypes.BankKeeper +} + +// encoders can be nil to accept the defaults, or set it to override some of the message handlers (like default) +func CreateTestInput(t *testing.T, isCheckTx bool, tempDir string, supportedFeatures string, encoders *MessageEncoders, queriers *QueryPlugins) (sdk.Context, TestKeepers) { + keyWasm := sdk.NewKVStoreKey(wasmtypes.StoreKey) + keyAcc := sdk.NewKVStoreKey(auth.StoreKey) + keyStaking := sdk.NewKVStoreKey(staking.StoreKey) + keySupply := sdk.NewKVStoreKey(supply.StoreKey) + keyDistro := sdk.NewKVStoreKey(distribution.StoreKey) + keyParams := sdk.NewKVStoreKey(params.StoreKey) + tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) + keyGov := sdk.NewKVStoreKey(govtypes.StoreKey) + keyCoin := sdk.NewKVStoreKey(coin.StoreKey) + + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyWasm, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyStaking, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keySupply, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyDistro, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + ms.MountStoreWithDB(keyGov, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyCoin, sdk.StoreTypeIAVL, db) + err := ms.LoadLatestVersion() + require.Nil(t, err) + + ctx := sdk.NewContext(ms, abci.Header{ + Height: 1234567, + Time: time.Date(2020, time.April, 22, 12, 0, 0, 0, time.UTC), + }, isCheckTx, log.NewNopLogger()) + cdc := MakeTestCodec() + + paramsKeeper := params.NewKeeper(cdc, keyParams, tkeyParams) + + accountKeeper := auth.NewAccountKeeper( + cdc, // amino codec + keyAcc, // target store + paramsKeeper.Subspace(auth.DefaultParamspace), + auth.ProtoBaseAccount, // prototype + ) + + // this is also used to initialize module accounts (so nil is meaningful here) + maccPerms := map[string][]string{ + auth.FeeCollectorName: nil, + distribution.ModuleName: nil, + // mint.ModuleName: {supply.Minter}, + staking.BondedPoolName: {supply.Burner, supply.Staking}, + staking.NotBondedPoolName: {supply.Burner, supply.Staking}, + gov.ModuleName: {supply.Burner}, + } + blockedAddr := make(map[string]bool, len(maccPerms)) + for acc := range maccPerms { + blockedAddr[supply.NewModuleAddress(acc).String()] = true + } + bankKeeper := bank.NewBaseKeeper( + accountKeeper, + paramsKeeper.Subspace(bank.DefaultParamspace), + blockedAddr, + ) + bankKeeper.SetSendEnabled(ctx, true) + + coinKeeper := coin.NewKeeper(bankKeeper, keyCoin) + coinKeeper.BlacklistAccountAction(ctx, supply.NewModuleAddress(auth.FeeCollectorName), coin.ActionTransferTo) + + supplyKeeper := supply.NewKeeper(cdc, keySupply, accountKeeper, bankKeeper, maccPerms) + stakingKeeper := staking.NewKeeper(cdc, keyStaking, supplyKeeper, paramsKeeper.Subspace(staking.DefaultParamspace)) + stakingKeeper.SetParams(ctx, TestingStakeParams) + + distKeeper := distribution.NewKeeper(cdc, keyDistro, paramsKeeper.Subspace(distribution.DefaultParamspace), stakingKeeper, supplyKeeper, auth.FeeCollectorName, nil) + distKeeper.SetParams(ctx, distribution.DefaultParams()) + stakingKeeper.SetHooks(distKeeper.Hooks()) + + // set genesis items required for distribution + distKeeper.SetFeePool(ctx, distribution.InitialFeePool()) + + // total supply to track this + totalSupply := sdk.NewCoins(sdk.NewInt64Coin("stake", 100000000)) + supplyKeeper.SetSupply(ctx, supply.NewSupply(totalSupply)) + + // set up initial accounts + for name, perms := range maccPerms { + mod := supply.NewEmptyModuleAccount(name, perms...) + if name == staking.NotBondedPoolName { + err = mod.SetCoins(totalSupply) + require.NoError(t, err) + } else if name == distribution.ModuleName { + // some big pot to pay out + err = mod.SetCoins(sdk.NewCoins(sdk.NewInt64Coin("stake", 500000))) + require.NoError(t, err) + } + supplyKeeper.SetModuleAccount(ctx, mod) + } + + stakeAddr := supply.NewModuleAddress(staking.BondedPoolName) + moduleAcct := accountKeeper.GetAccount(ctx, stakeAddr) + require.NotNil(t, moduleAcct) + + router := baseapp.NewRouter() + ch := coin.NewHandler(coinKeeper) + router.AddRoute(coin.RouterKey, ch) + sh := staking.NewHandler(stakingKeeper) + router.AddRoute(staking.RouterKey, sh) + dh := distribution.NewHandler(distKeeper) + router.AddRoute(distribution.RouterKey, dh) + + // Load default wasm config + wasmConfig := wasmtypes.DefaultWasmConfig() + keeper := NewKeeper(cdc, keyWasm, paramsKeeper.Subspace(wasmtypes.DefaultParamspace), + accountKeeper, coinKeeper, stakingKeeper, distKeeper, router, nil, nil, tempDir, wasmConfig, + supportedFeatures, encoders, queriers, + ) + keeper.setParams(ctx, wasmtypes.DefaultParams()) + // add wasm handler so we can loop-back (contracts calling contracts) + router.AddRoute(wasmtypes.RouterKey, TestHandler(keeper)) + + govRouter := gov.NewRouter(). + AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(paramsKeeper)). + AddRoute(govtypes.RouterKey, govtypes.ProposalHandler). + AddRoute(wasmtypes.RouterKey, NewWasmProposalHandler(keeper, wasmtypes.EnableAllProposals)) + + govKeeper := gov.NewKeeper( + cdc, keyGov, paramsKeeper.Subspace(govtypes.DefaultParamspace).WithKeyTable(gov.ParamKeyTable()), supplyKeeper, stakingKeeper, govRouter, + ) + + govKeeper.SetProposalID(ctx, govtypes.DefaultStartingProposalID) + govKeeper.SetDepositParams(ctx, govtypes.DefaultDepositParams()) + govKeeper.SetVotingParams(ctx, govtypes.DefaultVotingParams()) + govKeeper.SetTallyParams(ctx, govtypes.DefaultTallyParams()) + + keepers := TestKeepers{ + AccountKeeper: accountKeeper, + SupplyKeeper: supplyKeeper, + StakingKeeper: stakingKeeper, + DistKeeper: distKeeper, + WasmKeeper: keeper, + GovKeeper: govKeeper, + BankKeeper: coinKeeper, + } + return ctx, keepers +} + +// TestHandler returns a wasm handler for tests (to avoid circular imports) +func TestHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + ctx = ctx.WithEventManager(sdk.NewEventManager()) + + switch msg := msg.(type) { + case wasmtypes.MsgInstantiateContract: + return handleInstantiate(ctx, k, &msg) + case *wasmtypes.MsgInstantiateContract: + return handleInstantiate(ctx, k, msg) + + case wasmtypes.MsgExecuteContract: + return handleExecute(ctx, k, &msg) + case *wasmtypes.MsgExecuteContract: + return handleExecute(ctx, k, msg) + + default: + errMsg := fmt.Sprintf("unrecognized wasm message type: %T", msg) + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg) + } + } +} + +func handleInstantiate(ctx sdk.Context, k Keeper, msg *wasmtypes.MsgInstantiateContract) (*sdk.Result, error) { + contractAddr, err := k.Instantiate(ctx, msg.CodeID, msg.Sender, msg.Admin, msg.InitMsg, msg.Label, msg.InitFunds) + if err != nil { + return nil, err + } + + return &sdk.Result{ + Data: contractAddr, + Events: ctx.EventManager().Events(), + }, nil +} + +func handleExecute(ctx sdk.Context, k Keeper, msg *wasmtypes.MsgExecuteContract) (*sdk.Result, error) { + res, err := k.Execute(ctx, msg.Contract, msg.Sender, msg.Msg, msg.SentFunds) + if err != nil { + return nil, err + } + + res.Events = ctx.EventManager().Events() + return res, nil +} diff --git a/x/wasm/internal/keeper/test_fuzz.go b/x/wasm/internal/keeper/test_fuzz.go new file mode 100644 index 0000000000..5277b4940a --- /dev/null +++ b/x/wasm/internal/keeper/test_fuzz.go @@ -0,0 +1,66 @@ +package keeper + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + fuzz "github.com/google/gofuzz" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" + tmBytes "github.com/tendermint/tendermint/libs/bytes" +) + +var ModelFuzzers = []interface{}{FuzzAddr, FuzzAbsoluteTxPosition, FuzzContractInfo, FuzzStateModel, FuzzAccessType, FuzzAccessConfig, FuzzContractCodeHistory} + +func FuzzAddr(m *sdk.AccAddress, c fuzz.Continue) { + *m = make([]byte, 20) + c.Read(*m) +} + +func FuzzAbsoluteTxPosition(m *types.AbsoluteTxPosition, c fuzz.Continue) { + m.BlockHeight = int64(c.RandUint64()) // can't be negative + m.TxIndex = c.RandUint64() +} + +func FuzzContractInfo(m *types.ContractInfo, c fuzz.Continue) { + m.CodeID = c.RandUint64() + FuzzAddr(&m.Creator, c) + FuzzAddr(&m.Admin, c) + m.Label = c.RandString() + c.Fuzz(&m.Created) +} + +func FuzzContractCodeHistory(m *types.ContractCodeHistoryEntry, c fuzz.Continue) { + const maxMsgSize = 128 + m.CodeID = c.RandUint64() + msg := make([]byte, c.RandUint64()%maxMsgSize) + c.Read(msg) + var err error + if m.Msg, err = json.Marshal(msg); err != nil { + panic(err) + } + c.Fuzz(&m.Updated) + m.Operation = types.AllCodeHistoryTypes[c.Int()%len(types.AllCodeHistoryTypes)] +} + +func FuzzStateModel(m *types.Model, c fuzz.Continue) { + m.Key = tmBytes.HexBytes(c.RandString()) + c.Fuzz(&m.Value) +} + +func FuzzAccessType(m *types.AccessType, c fuzz.Continue) { + pos := c.Int() % len(types.AllAccessTypes) + for k := range types.AllAccessTypes { + if pos == 0 { + *m = k + return + } + pos-- + } +} + +func FuzzAccessConfig(m *types.AccessConfig, c fuzz.Continue) { + FuzzAccessType(&m.Type, c) + var add sdk.AccAddress + FuzzAddr(&add, c) + *m = m.Type.With(add) +} diff --git a/x/wasm/internal/keeper/testdata/burner.wasm b/x/wasm/internal/keeper/testdata/burner.wasm new file mode 100644 index 0000000000..824b23a419 Binary files /dev/null and b/x/wasm/internal/keeper/testdata/burner.wasm differ diff --git a/x/wasm/internal/keeper/testdata/download_releases.sh b/x/wasm/internal/keeper/testdata/download_releases.sh new file mode 100755 index 0000000000..ab08331f42 --- /dev/null +++ b/x/wasm/internal/keeper/testdata/download_releases.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -o errexit -o nounset -o pipefail +command -v shellcheck > /dev/null && shellcheck "$0" + +if [ $# -ne 1 ]; then + echo "Usage: ./download_releases.sh RELEASE_TAG" + exit 1 +fi + +tag="$1" + +for contract in burner hackatom reflect staking; do + url="https://github.com/CosmWasm/cosmwasm/releases/download/$tag/${contract}.wasm" + echo "Downloading $url ..." + wget -O "${contract}.wasm" "$url" +done + +# create the zip variant +gzip -k hackatom.wasm +mv hackatom.wasm.gz hackatom.wasm.gzip diff --git a/x/wasm/internal/keeper/testdata/genesis.json b/x/wasm/internal/keeper/testdata/genesis.json new file mode 100644 index 0000000000..08969c7dd2 --- /dev/null +++ b/x/wasm/internal/keeper/testdata/genesis.json @@ -0,0 +1,219 @@ +{ + "genesis_time": "2020-07-13T07:49:08.2945876Z", + "chain_id": "testing", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age_num_blocks": "100000", + "max_age_duration": "172800000000000" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + } + }, + "app_hash": "", + "app_state": { + "upgrade": {}, + "evidence": { + "params": { + "max_evidence_age": "120000000000" + }, + "evidence": [] + }, + "supply": { + "supply": [] + }, + "mint": { + "minter": { + "inflation": "0.130000000000000000", + "annual_provisions": "0.000000000000000000" + }, + "params": { + "mint_denom": "ustake", + "inflation_rate_change": "0.130000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "goal_bonded": "0.670000000000000000", + "blocks_per_year": "6311520" + } + }, + "gov": { + "starting_proposal_id": "1", + "deposits": null, + "votes": null, + "proposals": null, + "deposit_params": { + "min_deposit": [ + { + "denom": "ustake", + "amount": "1" + } + ], + "max_deposit_period": "172800000000000" + }, + "voting_params": { + "voting_period": "60000000000", + "voting_period_desc": "1minute" + }, + "tally_params": { + "quorum": "0.000000000000000001", + "threshold": "0.000000000000000001", + "veto": "0.334000000000000000" + } + }, + "slashing": { + "params": { + "signed_blocks_window": "100", + "min_signed_per_window": "0.500000000000000000", + "downtime_jail_duration": "600000000000", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": {}, + "missed_blocks": {} + }, + "wasm": { + "params": { + "upload_access": { + "type": 3, + "address": "" + }, + "instantiate_default_permission": 3 + }, + "codes": null, + "contracts": null, + "sequences": null + }, + "bank": { + "send_enabled": true + }, + "distribution": { + "params": { + "community_tax": "0.020000000000000000", + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "withdraw_addr_enabled": true + }, + "fee_pool": { + "community_pool": [] + }, + "delegator_withdraw_infos": [], + "previous_proposer": "", + "outstanding_rewards": [], + "validator_accumulated_commissions": [], + "validator_historical_rewards": [], + "validator_current_rewards": [], + "delegator_starting_infos": [], + "validator_slash_events": [] + }, + "crisis": { + "constant_fee": { + "denom": "ustake", + "amount": "1000" + } + }, + "genutil": { + "gentxs": [ + { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgCreateValidator", + "value": { + "description": { + "moniker": "testing", + "identity": "", + "website": "", + "security_contact": "", + "details": "" + }, + "commission": { + "rate": "0.100000000000000000", + "max_rate": "0.200000000000000000", + "max_change_rate": "0.010000000000000000" + }, + "min_self_delegation": "1", + "delegator_address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "validator_address": "cosmosvaloper1ve557a5g9yw2g2z57js3pdmcvd5my6g88d76lj", + "pubkey": "cosmosvalconspub1zcjduepqddfln4tujr2p8actpgqz4h2xnls9y7tu9c9tu5lqkdglmdjalzuqah4neg", + "value": { + "denom": "ustake", + "amount": "250000000" + } + } + } + ], + "fee": { + "amount": [], + "gas": "200000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A//cqZxkpH1re0VrHBtH308nb5t8K+Y/hF0GeRdRBmaJ" + }, + "signature": "5QEEIuUVQTEBMuAtOOHnnKo6rPsIbmfzUxUqRnDFERVqwVr1Kg+ex4f/UGIK0yrOAvOG8zDADwFP4yF8lw+o5g==" + } + ], + "memo": "836fc54e9cad58f4ed6420223ec6290f75342afa@172.17.0.2:26656" + } + } + ] + }, + "auth": { + "params": { + "max_memo_characters": "256", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000" + }, + "accounts": [ + { + "type": "cosmos-sdk/Account", + "value": { + "address": "cosmos1ve557a5g9yw2g2z57js3pdmcvd5my6g8ze20np", + "coins": [ + { + "denom": "ucosm", + "amount": "1000000000" + }, + { + "denom": "ustake", + "amount": "1000000000" + } + ], + "public_key": "", + "account_number": 0, + "sequence": 0 + } + } + ] + }, + "params": null, + "staking": { + "params": { + "unbonding_time": "1814400000000000", + "max_validators": 100, + "max_entries": 7, + "historical_entries": 0, + "bond_denom": "ustake" + }, + "last_total_power": "0", + "last_validator_powers": null, + "validators": null, + "delegations": null, + "unbonding_delegations": null, + "redelegations": null, + "exported": false + } + } +} \ No newline at end of file diff --git a/x/wasm/internal/keeper/testdata/hackatom.wasm b/x/wasm/internal/keeper/testdata/hackatom.wasm new file mode 100644 index 0000000000..7dd3189928 Binary files /dev/null and b/x/wasm/internal/keeper/testdata/hackatom.wasm differ diff --git a/x/wasm/internal/keeper/testdata/hackatom.wasm.gzip b/x/wasm/internal/keeper/testdata/hackatom.wasm.gzip new file mode 100644 index 0000000000..f7e55cf17a Binary files /dev/null and b/x/wasm/internal/keeper/testdata/hackatom.wasm.gzip differ diff --git a/x/wasm/internal/keeper/testdata/reflect.wasm b/x/wasm/internal/keeper/testdata/reflect.wasm new file mode 100644 index 0000000000..cee23e21d8 Binary files /dev/null and b/x/wasm/internal/keeper/testdata/reflect.wasm differ diff --git a/x/wasm/internal/keeper/testdata/staking.wasm b/x/wasm/internal/keeper/testdata/staking.wasm new file mode 100644 index 0000000000..2312043548 Binary files /dev/null and b/x/wasm/internal/keeper/testdata/staking.wasm differ diff --git a/x/wasm/internal/types/codec.go b/x/wasm/internal/types/codec.go new file mode 100644 index 0000000000..c401d96298 --- /dev/null +++ b/x/wasm/internal/types/codec.go @@ -0,0 +1,42 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// RegisterCodec registers the account types and interface +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(MsgStoreCode{}, "wasm/MsgStoreCode", nil) + cdc.RegisterConcrete(MsgInstantiateContract{}, "wasm/MsgInstantiateContract", nil) + cdc.RegisterConcrete(MsgExecuteContract{}, "wasm/MsgExecuteContract", nil) + cdc.RegisterConcrete(MsgMigrateContract{}, "wasm/MsgMigrateContract", nil) + cdc.RegisterConcrete(MsgUpdateAdmin{}, "wasm/MsgUpdateAdmin", nil) + cdc.RegisterConcrete(MsgClearAdmin{}, "wasm/MsgClearAdmin", nil) + + cdc.RegisterConcrete(StoreCodeProposal{}, "wasm/StoreCodeProposal", nil) + cdc.RegisterConcrete(InstantiateContractProposal{}, "wasm/InstantiateContractProposal", nil) + cdc.RegisterConcrete(MigrateContractProposal{}, "wasm/MigrateContractProposal", nil) + cdc.RegisterConcrete(UpdateAdminProposal{}, "wasm/UpdateAdminProposal", nil) + cdc.RegisterConcrete(ClearAdminProposal{}, "wasm/ClearAdminProposal", nil) + + // query responses + + // For the type-tags in case of a slice item or a nested property. + cdc.RegisterInterface((*CodeInfoResponse)(nil), nil) + cdc.RegisterInterface((*ContractInfoResponse)(nil), nil) + cdc.RegisterInterface((*ContractHistoryResponse)(nil), nil) + + cdc.RegisterConcrete(codeInfo{}, "wasm/CodeInfo", nil) + cdc.RegisterConcrete(contractInfo{}, "wasm/ContractInfo", nil) + cdc.RegisterConcrete(contractHistory{}, "wasm/ContractHistory", nil) +} + +// ModuleCdc generic sealed codec to be used throughout module +var ModuleCdc *codec.Codec + +func init() { + cdc := codec.New() + RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + ModuleCdc = cdc.Seal() +} diff --git a/x/wasm/internal/types/encoder.go b/x/wasm/internal/types/encoder.go new file mode 100644 index 0000000000..4f38696eaa --- /dev/null +++ b/x/wasm/internal/types/encoder.go @@ -0,0 +1,22 @@ +package types + +import ( + "encoding/json" +) + +type EncodingModule string + +const ( + TokenM = EncodingModule("token") + CollectionM = EncodingModule("collection") +) + +type LinkMsgWrapper struct { + Module string `json:"module"` + MsgData json.RawMessage `json:"msg_data"` +} + +type LinkQueryWrapper struct { + Module string `json:"module"` + QueryData json.RawMessage `json:"query_data"` +} diff --git a/x/wasm/internal/types/errors.go b/x/wasm/internal/types/errors.go new file mode 100644 index 0000000000..a07dedaa10 --- /dev/null +++ b/x/wasm/internal/types/errors.go @@ -0,0 +1,54 @@ +package types + +import ( + sdkErrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// Codes for wasm contract errors +var ( + DefaultCodespace = ModuleName + + // Note: never use code 1 for any errors - that is reserved for ErrInternal in the core cosmos sdk + + // ErrCreateFailed error for wasm code that has already been uploaded or failed + ErrCreateFailed = sdkErrors.Register(DefaultCodespace, 2, "create wasm contract failed") + + // ErrAccountExists error for a contract account that already exists + ErrAccountExists = sdkErrors.Register(DefaultCodespace, 3, "contract account already exists") + + // ErrInstantiateFailed error for rust instantiate contract failure + ErrInstantiateFailed = sdkErrors.Register(DefaultCodespace, 4, "instantiate wasm contract failed") + + // ErrExecuteFailed error for rust execution contract failure + ErrExecuteFailed = sdkErrors.Register(DefaultCodespace, 5, "execute wasm contract failed") + + // ErrGasLimit error for out of gas + ErrGasLimit = sdkErrors.Register(DefaultCodespace, 6, "insufficient gas") + + // ErrInvalidGenesis error for invalid genesis file syntax + ErrInvalidGenesis = sdkErrors.Register(DefaultCodespace, 7, "invalid genesis") + + // ErrNotFound error for an entry not found in the store + ErrNotFound = sdkErrors.Register(DefaultCodespace, 8, "not found") + + // ErrQueryFailed error for rust smart query contract failure + ErrQueryFailed = sdkErrors.Register(DefaultCodespace, 9, "query wasm contract failed") + + // ErrInvalidMsg error when we cannot process the error returned from the contract + ErrInvalidMsg = sdkErrors.Register(DefaultCodespace, 10, "invalid CosmosMsg from the contract") + + // ErrMigrationFailed error for rust execution contract failure + ErrMigrationFailed = sdkErrors.Register(DefaultCodespace, 11, "migrate wasm contract failed") + + // ErrEmpty error for empty content + ErrEmpty = sdkErrors.Register(DefaultCodespace, 12, "empty") + + // ErrLimit error for content that exceeds a limit + ErrLimit = sdkErrors.Register(DefaultCodespace, 13, "exceeds limit") + + // ErrInvalid error for content that is invalid in this context + ErrInvalid = sdkErrors.Register(DefaultCodespace, 14, "invalid") + + // ErrDuplicate error for content that exsists + ErrDuplicate = sdkErrors.Register(DefaultCodespace, 15, "duplicate") +) diff --git a/x/wasm/internal/types/expected_keeper.go b/x/wasm/internal/types/expected_keeper.go new file mode 100644 index 0000000000..f4f5c45cbd --- /dev/null +++ b/x/wasm/internal/types/expected_keeper.go @@ -0,0 +1,10 @@ +package types + +import sdk "github.com/cosmos/cosmos-sdk/types" + +type BankKeeper interface { + GetCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) bool + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error + BlacklistedAddr(creator sdk.AccAddress) bool +} diff --git a/x/wasm/internal/types/genesis.go b/x/wasm/internal/types/genesis.go new file mode 100644 index 0000000000..14a546c02d --- /dev/null +++ b/x/wasm/internal/types/genesis.go @@ -0,0 +1,101 @@ +package types + +import "C" +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +type Sequence struct { + IDKey []byte `json:"id_key"` + Value uint64 `json:"value"` +} + +func (s Sequence) ValidateBasic() error { + if len(s.IDKey) == 0 { + return sdkerrors.Wrap(ErrEmpty, "id key") + } + return nil +} + +// GenesisState is the struct representation of the export genesis +type GenesisState struct { + Params Params `json:"params"` + Codes []Code `json:"codes,omitempty"` + Contracts []Contract `json:"contracts,omitempty"` + Sequences []Sequence `json:"sequences,omitempty"` +} + +func (s GenesisState) ValidateBasic() error { + if err := s.Params.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "params") + } + for i := range s.Codes { + if err := s.Codes[i].ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "code: %d", i) + } + } + for i := range s.Contracts { + if err := s.Contracts[i].ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "contract: %d", i) + } + } + for i := range s.Sequences { + if err := s.Sequences[i].ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "sequence: %d", i) + } + } + return nil +} + +// Code struct encompasses CodeInfo and CodeBytes +type Code struct { + CodeID uint64 `json:"code_id"` + CodeInfo CodeInfo `json:"code_info"` + CodesBytes []byte `json:"code_bytes"` +} + +func (c Code) ValidateBasic() error { + if c.CodeID == 0 { + return sdkerrors.Wrap(ErrEmpty, "code id") + } + if err := c.CodeInfo.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "code info") + } + if err := validateWasmCode(c.CodesBytes); err != nil { + return sdkerrors.Wrap(err, "code bytes") + } + return nil +} + +// Contract struct encompasses ContractAddress, ContractInfo, and ContractState +type Contract struct { + ContractAddress sdk.AccAddress `json:"contract_address"` + ContractInfo ContractInfo `json:"contract_info"` + ContractState []Model `json:"contract_state"` +} + +func (c Contract) ValidateBasic() error { + if err := sdk.VerifyAddressFormat(c.ContractAddress); err != nil { + return sdkerrors.Wrap(err, "contract address") + } + if err := c.ContractInfo.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "contract info") + } + + if c.ContractInfo.Created != nil { + return sdkerrors.Wrap(ErrInvalid, "created must be empty") + } + for i := range c.ContractState { + if err := c.ContractState[i].ValidateBasic(); err != nil { + return sdkerrors.Wrapf(err, "contract state %d", i) + } + } + return nil +} + +// ValidateGenesis performs basic validation of supply genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { + return data.ValidateBasic() +} diff --git a/x/wasm/internal/types/genesis_test.go b/x/wasm/internal/types/genesis_test.go new file mode 100644 index 0000000000..5695c6460c --- /dev/null +++ b/x/wasm/internal/types/genesis_test.go @@ -0,0 +1,149 @@ +// nolint: scopelint +package types + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateGenesisState(t *testing.T) { + specs := map[string]struct { + srcMutator func(*GenesisState) + expError bool + }{ + "all good": { + srcMutator: func(s *GenesisState) {}, + }, + "params invalid": { + srcMutator: func(s *GenesisState) { + s.Params = Params{} + }, + expError: true, + }, + "codeinfo invalid": { + srcMutator: func(s *GenesisState) { + s.Codes[0].CodeInfo.CodeHash = nil + }, + expError: true, + }, + "contract invalid": { + srcMutator: func(s *GenesisState) { + s.Contracts[0].ContractAddress = nil + }, + expError: true, + }, + "sequence invalid": { + srcMutator: func(s *GenesisState) { + s.Sequences[0].IDKey = nil + }, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := GenesisFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} + +func TestCodeValidateBasic(t *testing.T) { + specs := map[string]struct { + srcMutator func(*Code) + expError bool + }{ + "all good": {srcMutator: func(_ *Code) {}}, + "code id invalid": { + srcMutator: func(c *Code) { + c.CodeID = 0 + }, + expError: true, + }, + "codeinfo invalid": { + srcMutator: func(c *Code) { + c.CodeInfo.CodeHash = nil + }, + expError: true, + }, + "codeBytes empty": { + srcMutator: func(c *Code) { + c.CodesBytes = []byte{} + }, + expError: true, + }, + "codeBytes nil": { + srcMutator: func(c *Code) { + c.CodesBytes = nil + }, + expError: true, + }, + "codeBytes greater limit": { + srcMutator: func(c *Code) { + c.CodesBytes = bytes.Repeat([]byte{0x1}, MaxWasmSize+1) + }, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := CodeFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} + +func TestContractValidateBasic(t *testing.T) { + specs := map[string]struct { + srcMutator func(*Contract) + expError bool + }{ + "all good": {srcMutator: func(_ *Contract) {}}, + "contract address invalid": { + srcMutator: func(c *Contract) { + c.ContractAddress = nil + }, + expError: true, + }, + "contract info invalid": { + srcMutator: func(c *Contract) { + c.ContractInfo.Creator = nil + }, + expError: true, + }, + "contract with created set": { + srcMutator: func(c *Contract) { + c.ContractInfo.Created = &AbsoluteTxPosition{} + }, + expError: true, + }, + "contract state invalid": { + srcMutator: func(c *Contract) { + c.ContractState = append(c.ContractState, Model{}) + }, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := ContractFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} diff --git a/x/wasm/internal/types/keys.go b/x/wasm/internal/types/keys.go new file mode 100644 index 0000000000..fed97627a3 --- /dev/null +++ b/x/wasm/internal/types/keys.go @@ -0,0 +1,63 @@ +package types + +import ( + "encoding/binary" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // ModuleName is the name of the contract module + ModuleName = "wasm" + + // StoreKey is the string store representation + StoreKey = ModuleName + + // TStoreKey is the string transient store representation + TStoreKey = "transient_" + ModuleName + + // QuerierRoute is the querier route for the staking module + QuerierRoute = ModuleName + + // RouterKey is the msg router key for the staking module + RouterKey = ModuleName +) + +const ( // event attributes + AttributeKeyContract = "contract_address" + AttributeKeyCodeID = "code_id" + AttributeKeySigner = "signer" +) + +// nolint +var ( + CodeKeyPrefix = []byte{0x01} + ContractKeyPrefix = []byte{0x02} + ContractStorePrefix = []byte{0x03} + SequenceKeyPrefix = []byte{0x04} + ContractHistoryStorePrefix = []byte{0x05} + + KeyLastCodeID = append(SequenceKeyPrefix, []byte("lastCodeId")...) + KeyLastInstanceID = append(SequenceKeyPrefix, []byte("lastContractId")...) +) + +// GetCodeKey constructs the key for retreiving the ID for the WASM code +func GetCodeKey(codeID uint64) []byte { + contractIDBz := sdk.Uint64ToBigEndian(codeID) + return append(CodeKeyPrefix, contractIDBz...) +} + +// nolint: deadcode, unused +func decodeCodeKey(src []byte) uint64 { + return binary.BigEndian.Uint64(src[len(CodeKeyPrefix):]) +} + +// GetContractAddressKey returns the key for the WASM contract instance +func GetContractAddressKey(addr sdk.AccAddress) []byte { + return append(ContractKeyPrefix, addr...) +} + +// GetContractStorePrefixKey returns the store prefix for the WASM contract instance +func GetContractStorePrefixKey(addr sdk.AccAddress) []byte { + return append(ContractStorePrefix, addr...) +} diff --git a/x/wasm/internal/types/msg.go b/x/wasm/internal/types/msg.go new file mode 100644 index 0000000000..43ee4be6b7 --- /dev/null +++ b/x/wasm/internal/types/msg.go @@ -0,0 +1,263 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +type MsgStoreCode struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + // WASMByteCode can be raw or gzip compressed + WASMByteCode []byte `json:"wasm_byte_code" yaml:"wasm_byte_code"` + // Source is a valid absolute HTTPS URI to the contract's source code, optional + Source string `json:"source" yaml:"source"` + // Builder is a valid docker image name with tag, optional + Builder string `json:"builder" yaml:"builder"` + // InstantiatePermission to apply on contract creation, optional + InstantiatePermission *AccessConfig `json:"instantiate_permission,omitempty" yaml:"instantiate_permission"` +} + +func (msg MsgStoreCode) Route() string { + return RouterKey +} + +func (msg MsgStoreCode) Type() string { + return "store-code" +} + +func (msg MsgStoreCode) ValidateBasic() error { + if err := sdk.VerifyAddressFormat(msg.Sender); err != nil { + return err + } + + if err := validateWasmCode(msg.WASMByteCode); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "code bytes %s", err.Error()) + } + + if err := validateSourceURL(msg.Source); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "source %s", err.Error()) + } + + if err := validateBuilder(msg.Builder); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "builder %s", err.Error()) + } + if msg.InstantiatePermission != nil { + if err := msg.InstantiatePermission.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "instantiate permission") + } + } + return nil +} + +func (msg MsgStoreCode) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgStoreCode) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +type MsgInstantiateContract struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + // Admin is an optional address that can execute migrations + Admin sdk.AccAddress `json:"admin,omitempty" yaml:"admin"` + CodeID uint64 `json:"code_id" yaml:"code_id"` + Label string `json:"label" yaml:"label"` + InitMsg json.RawMessage `json:"init_msg" yaml:"init_msg"` + InitFunds sdk.Coins `json:"init_funds" yaml:"init_funds"` +} + +func (msg MsgInstantiateContract) Route() string { + return RouterKey +} + +func (msg MsgInstantiateContract) Type() string { + return "instantiate" +} + +func (msg MsgInstantiateContract) ValidateBasic() error { + if err := sdk.VerifyAddressFormat(msg.Sender); err != nil { + return err + } + + if msg.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code_id is required") + } + + if err := validateLabel(msg.Label); err != nil { + return err + } + + if !msg.InitFunds.IsValid() { + return sdkerrors.ErrInvalidCoins + } + + if len(msg.Admin) != 0 { + if err := sdk.VerifyAddressFormat(msg.Admin); err != nil { + return err + } + } + if !json.Valid(msg.InitMsg) { + return sdkerrors.Wrap(ErrInvalid, "init msg json") + } + return nil +} + +func (msg MsgInstantiateContract) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgInstantiateContract) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +type MsgExecuteContract struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Contract sdk.AccAddress `json:"contract" yaml:"contract"` + Msg json.RawMessage `json:"msg" yaml:"msg"` + SentFunds sdk.Coins `json:"sent_funds" yaml:"sent_funds"` +} + +func (msg MsgExecuteContract) Route() string { + return RouterKey +} + +func (msg MsgExecuteContract) Type() string { + return "execute" +} + +func (msg MsgExecuteContract) ValidateBasic() error { + if err := sdk.VerifyAddressFormat(msg.Sender); err != nil { + return err + } + if err := sdk.VerifyAddressFormat(msg.Contract); err != nil { + return err + } + + if !msg.SentFunds.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, "sentFunds") + } + if !json.Valid(msg.Msg) { + return sdkerrors.Wrap(ErrInvalid, "msg json") + } + return nil +} + +func (msg MsgExecuteContract) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgExecuteContract) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +type MsgMigrateContract struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Contract sdk.AccAddress `json:"contract" yaml:"contract"` + CodeID uint64 `json:"code_id" yaml:"code_id"` + MigrateMsg json.RawMessage `json:"msg" yaml:"msg"` +} + +func (msg MsgMigrateContract) Route() string { + return RouterKey +} + +func (msg MsgMigrateContract) Type() string { + return "migrate" +} + +func (msg MsgMigrateContract) ValidateBasic() error { + if msg.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code_id is required") + } + if err := sdk.VerifyAddressFormat(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + if err := sdk.VerifyAddressFormat(msg.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if !json.Valid(msg.MigrateMsg) { + return sdkerrors.Wrap(ErrInvalid, "migrate msg json") + } + + return nil +} + +func (msg MsgMigrateContract) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgMigrateContract) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +type MsgUpdateAdmin struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + NewAdmin sdk.AccAddress `json:"new_admin" yaml:"new_admin"` + Contract sdk.AccAddress `json:"contract" yaml:"contract"` +} + +func (msg MsgUpdateAdmin) Route() string { + return RouterKey +} + +func (msg MsgUpdateAdmin) Type() string { + return "update-contract-admin" +} + +func (msg MsgUpdateAdmin) ValidateBasic() error { + if err := sdk.VerifyAddressFormat(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + if err := sdk.VerifyAddressFormat(msg.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if err := sdk.VerifyAddressFormat(msg.NewAdmin); err != nil { + return sdkerrors.Wrap(err, "new admin") + } + if msg.Sender.Equals(msg.NewAdmin) { + return sdkerrors.Wrap(ErrInvalidMsg, "new admin is the same as the old") + } + return nil +} + +func (msg MsgUpdateAdmin) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgUpdateAdmin) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} + +type MsgClearAdmin struct { + Sender sdk.AccAddress `json:"sender" yaml:"sender"` + Contract sdk.AccAddress `json:"contract" yaml:"contract"` +} + +func (msg MsgClearAdmin) Route() string { + return RouterKey +} + +func (msg MsgClearAdmin) Type() string { + return "clear-contract-admin" +} + +func (msg MsgClearAdmin) ValidateBasic() error { + if err := sdk.VerifyAddressFormat(msg.Sender); err != nil { + return sdkerrors.Wrap(err, "sender") + } + if err := sdk.VerifyAddressFormat(msg.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + return nil +} + +func (msg MsgClearAdmin) GetSignBytes() []byte { + return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg)) +} + +func (msg MsgClearAdmin) GetSigners() []sdk.AccAddress { + return []sdk.AccAddress{msg.Sender} +} diff --git a/x/wasm/internal/types/msg_test.go b/x/wasm/internal/types/msg_test.go new file mode 100644 index 0000000000..9cacb747a9 --- /dev/null +++ b/x/wasm/internal/types/msg_test.go @@ -0,0 +1,553 @@ +// nolint: scopelint +package types + +import ( + "bytes" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const firstCodeID = 1 + +func TestBuilderRegexp(t *testing.T) { + cases := map[string]struct { + example string + valid bool + }{ + "normal": {"fedora/httpd:version1.0", true}, + "another valid org": {"confio/js-builder-1:test", true}, + "no org name": {"cosmwasm-opt:0.6.3", false}, + "invalid trailing char": {"someone/cosmwasm-opt-:0.6.3", false}, + "invalid leading char": {"confio/.builder-1:manual", false}, + "multiple orgs": {"confio/assembly-script/optimizer:v0.9.1", true}, + "too long": {"over-128-character-limit/some-long-sub-path/and-yet-another-long-name/testtesttesttesttesttesttest/foobarfoobar/foobarfoobar:randomstringrandomstringrandomstringrandomstring", false}, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := validateBuilder(tc.example) + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestStoreCodeValidation(t *testing.T) { + badAddress, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)) + + cases := map[string]struct { + msg MsgStoreCode + valid bool + }{ + "empty": { + msg: MsgStoreCode{}, + valid: false, + }, + "correct minimal": { + msg: MsgStoreCode{ + Sender: goodAddress, + WASMByteCode: []byte("foo"), + }, + valid: true, + }, + "missing code": { + msg: MsgStoreCode{ + Sender: goodAddress, + }, + valid: false, + }, + "bad sender minimal": { + msg: MsgStoreCode{ + Sender: badAddress, + WASMByteCode: []byte("foo"), + }, + valid: false, + }, + "correct maximal": { + msg: MsgStoreCode{ + Sender: goodAddress, + WASMByteCode: []byte("foo"), + Builder: "confio/cosmwasm-opt:0.6.2", + Source: "https://crates.io/api/v1/crates/cw-erc20/0.1.0/download", + }, + valid: true, + }, + "invalid builder": { + msg: MsgStoreCode{ + Sender: goodAddress, + WASMByteCode: []byte("foo"), + Builder: "-bad-opt:0.6.2", + Source: "https://crates.io/api/v1/crates/cw-erc20/0.1.0/download", + }, + valid: false, + }, + "invalid source scheme": { + msg: MsgStoreCode{ + Sender: goodAddress, + WASMByteCode: []byte("foo"), + Builder: "cosmwasm-opt:0.6.2", + Source: "ftp://crates.io/api/download.tar.gz", + }, + valid: false, + }, + "invalid source format": { + msg: MsgStoreCode{ + Sender: goodAddress, + WASMByteCode: []byte("foo"), + Builder: "cosmwasm-opt:0.6.2", + Source: "/api/download-ss", + }, + valid: false, + }, + "invalid InstantiatePermission": { + msg: MsgStoreCode{ + Sender: goodAddress, + WASMByteCode: []byte("foo"), + InstantiatePermission: &AccessConfig{Type: OnlyAddress, Address: badAddress}, + }, + valid: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestInstantiateContractValidation(t *testing.T) { + badAddress, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)) + + cases := map[string]struct { + msg MsgInstantiateContract + valid bool + }{ + "empty": { + msg: MsgInstantiateContract{}, + valid: false, + }, + "correct minimal": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + InitMsg: []byte("{}"), + }, + valid: true, + }, + "missing code": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + Label: "foo", + InitMsg: []byte("{}"), + }, + valid: false, + }, + "missing label": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + InitMsg: []byte("{}"), + }, + valid: false, + }, + "label too long": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + Label: strings.Repeat("food", 33), + }, + valid: false, + }, + "bad sender minimal": { + msg: MsgInstantiateContract{ + Sender: badAddress, + CodeID: firstCodeID, + Label: "foo", + InitMsg: []byte("{}"), + }, + valid: false, + }, + "correct maximal": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + InitMsg: []byte(`{"some": "data"}`), + InitFunds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(200)}}, + }, + valid: true, + }, + "negative funds": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + InitMsg: []byte(`{"some": "data"}`), + // we cannot use sdk.NewCoin() constructors as they panic on creating invalid data (before we can test) + InitFunds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(-200)}}, + }, + valid: false, + }, + "non json init msg": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + InitMsg: []byte("invalid-json"), + }, + valid: false, + }, + "empty init msg": { + msg: MsgInstantiateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + Label: "foo", + }, + valid: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestExecuteContractValidation(t *testing.T) { + badAddress, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)) + + cases := map[string]struct { + msg MsgExecuteContract + valid bool + }{ + "empty": { + msg: MsgExecuteContract{}, + valid: false, + }, + "correct minimal": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte("{}"), + }, + valid: true, + }, + "correct all": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + SentFunds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(200)}}, + }, + valid: true, + }, + "bad sender": { + msg: MsgExecuteContract{ + Sender: badAddress, + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + }, + valid: false, + }, + "empty sender": { + msg: MsgExecuteContract{ + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + }, + valid: false, + }, + "bad contract": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: badAddress, + Msg: []byte(`{"some": "data"}`), + }, + valid: false, + }, + "empty contract": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Msg: []byte(`{"some": "data"}`), + }, + valid: false, + }, + "negative funds": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + SentFunds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(-1)}}, + }, + valid: false, + }, + "duplicate funds": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte(`{"some": "data"}`), + SentFunds: sdk.Coins{sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(1)}, sdk.Coin{Denom: "foobar", Amount: sdk.NewInt(1)}}, + }, + valid: false, + }, + "non json msg": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + Msg: []byte("invalid-json"), + }, + valid: false, + }, + "empty msg": { + msg: MsgExecuteContract{ + Sender: goodAddress, + Contract: goodAddress, + }, + valid: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.msg.ValidateBasic() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestMsgUpdateAdministrator(t *testing.T) { + badAddress, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)) + otherGoodAddress := sdk.AccAddress(bytes.Repeat([]byte{0x1}, 20)) + anotherGoodAddress := sdk.AccAddress(bytes.Repeat([]byte{0x2}, 20)) + + specs := map[string]struct { + src MsgUpdateAdmin + expErr bool + }{ + "all good": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + NewAdmin: otherGoodAddress, + Contract: anotherGoodAddress, + }, + }, + "new admin required": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad sender": { + src: MsgUpdateAdmin{ + Sender: badAddress, + NewAdmin: otherGoodAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad new admin": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + NewAdmin: badAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad contract addr": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + NewAdmin: otherGoodAddress, + Contract: badAddress, + }, + expErr: true, + }, + "new admin same as old admin": { + src: MsgUpdateAdmin{ + Sender: goodAddress, + NewAdmin: goodAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestMsgClearAdministrator(t *testing.T) { + badAddress, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)) + anotherGoodAddress := sdk.AccAddress(bytes.Repeat([]byte{0x2}, 20)) + + specs := map[string]struct { + src MsgClearAdmin + expErr bool + }{ + "all good": { + src: MsgClearAdmin{ + Sender: goodAddress, + Contract: anotherGoodAddress, + }, + }, + "bad sender": { + src: MsgClearAdmin{ + Sender: badAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad contract addr": { + src: MsgClearAdmin{ + Sender: goodAddress, + Contract: badAddress, + }, + expErr: true, + }, + "contract missing": { + src: MsgClearAdmin{ + Sender: goodAddress, + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestMsgMigrateContract(t *testing.T) { + badAddress, err := sdk.AccAddressFromHex("012345") + require.NoError(t, err) + // proper address size + goodAddress := sdk.AccAddress(make([]byte, 20)) + anotherGoodAddress := sdk.AccAddress(bytes.Repeat([]byte{0x2}, 20)) + + specs := map[string]struct { + src MsgMigrateContract + expErr bool + }{ + "all good": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: anotherGoodAddress, + CodeID: firstCodeID, + MigrateMsg: []byte("{}"), + }, + }, + "bad sender": { + src: MsgMigrateContract{ + Sender: badAddress, + Contract: anotherGoodAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + "empty sender": { + src: MsgMigrateContract{ + Contract: anotherGoodAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + "empty code": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: anotherGoodAddress, + }, + expErr: true, + }, + "bad contract addr": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: badAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + "empty contract addr": { + src: MsgMigrateContract{ + Sender: goodAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + "non json migrateMsg": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: anotherGoodAddress, + CodeID: firstCodeID, + MigrateMsg: []byte("invalid json"), + }, + expErr: true, + }, + "empty migrateMsg": { + src: MsgMigrateContract{ + Sender: goodAddress, + Contract: anotherGoodAddress, + CodeID: firstCodeID, + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/x/wasm/internal/types/params.go b/x/wasm/internal/types/params.go new file mode 100644 index 0000000000..6c09fa64e7 --- /dev/null +++ b/x/wasm/internal/types/params.go @@ -0,0 +1,197 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +const ( + // DefaultParamspace for params keeper + DefaultParamspace = ModuleName + // DefaultMaxWasmCodeSize limit max bytes read to prevent gzip bombs + DefaultMaxWasmCodeSize = 600 * 1024 +) + +var ParamStoreKeyUploadAccess = []byte("uploadAccess") +var ParamStoreKeyInstantiateAccess = []byte("instantiateAccess") +var ParamStoreKeyMaxWasmCodeSize = []byte("maxWasmCodeSize") + +type AccessType string + +const ( + Undefined AccessType = "Undefined" + Nobody AccessType = "Nobody" + OnlyAddress AccessType = "OnlyAddress" + Everybody AccessType = "Everybody" +) + +var AllAccessTypes = map[AccessType]struct{}{ + Nobody: {}, + OnlyAddress: {}, + Everybody: {}, +} + +func (a AccessType) With(addr sdk.AccAddress) AccessConfig { + switch a { + case Nobody: + return AllowNobody + case OnlyAddress: + if err := sdk.VerifyAddressFormat(addr); err != nil { + panic(err) + } + return AccessConfig{Type: OnlyAddress, Address: addr} + case Everybody: + return AllowEverybody + } + panic("unsupported access type") +} + +func (a *AccessType) UnmarshalText(text []byte) error { + s := AccessType(text) + if _, ok := AllAccessTypes[s]; ok { + *a = s + return nil + } + *a = Undefined + return nil +} + +func (a AccessType) MarshalText() ([]byte, error) { + if _, ok := AllAccessTypes[a]; ok { + return []byte(a), nil + } + return []byte(Undefined), nil +} + +type AccessConfig struct { + Type AccessType `json:"permission" yaml:"permission"` + Address sdk.AccAddress `json:"address,omitempty" yaml:"address"` +} + +func (v AccessConfig) Equals(o AccessConfig) bool { + return v.Type == o.Type && v.Address.Equals(o.Address) +} + +var ( + DefaultUploadAccess = AllowEverybody + AllowEverybody = AccessConfig{Type: Everybody} + AllowNobody = AccessConfig{Type: Nobody} +) + +// Params defines the set of wasm parameters. +type Params struct { + UploadAccess AccessConfig `json:"code_upload_access" yaml:"code_upload_access"` + DefaultInstantiatePermission AccessType `json:"instantiate_default_permission" yaml:"instantiate_default_permission"` + MaxWasmCodeSize uint64 `json:"max_wasm_code_size" yaml:"max_wasm_code_size"` +} + +// ParamKeyTable returns the parameter key table. +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable().RegisterParamSet(&Params{}) +} + +// DefaultParams returns default wasm parameters +func DefaultParams() Params { + return Params{ + UploadAccess: AllowEverybody, + DefaultInstantiatePermission: Everybody, + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + } +} + +func (p Params) String() string { + out, err := yaml.Marshal(p) + if err != nil { + panic(err) + } + return string(out) +} + +// ParamSetPairs returns the parameter set pairs. +func (p *Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ + params.NewParamSetPair(ParamStoreKeyUploadAccess, &p.UploadAccess, validateAccessConfig), + params.NewParamSetPair(ParamStoreKeyInstantiateAccess, &p.DefaultInstantiatePermission, validateAccessType), + params.NewParamSetPair(ParamStoreKeyMaxWasmCodeSize, &p.MaxWasmCodeSize, validateMaxWasmCodeSize), + } +} + +// ValidateBasic performs basic validation on wasm parameters +func (p Params) ValidateBasic() error { + if err := validateAccessType(p.DefaultInstantiatePermission); err != nil { + return errors.Wrap(err, "instantiate default permission") + } + if err := validateAccessConfig(p.UploadAccess); err != nil { + return errors.Wrap(err, "upload access") + } + if err := validateMaxWasmCodeSize(p.MaxWasmCodeSize); err != nil { + return errors.Wrap(err, "max wasm code size") + } + return nil +} + +func validateAccessConfig(i interface{}) error { + v, ok := i.(AccessConfig) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + return v.ValidateBasic() +} + +func validateAccessType(i interface{}) error { + v, ok := i.(AccessType) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + if v == Undefined { + return sdkerrors.Wrap(ErrEmpty, "type") + } + if _, ok := AllAccessTypes[v]; !ok { + return sdkerrors.Wrapf(ErrInvalid, "unknown type: %q", v) + } + return nil +} + +func validateMaxWasmCodeSize(i interface{}) error { + a, ok := i.(uint64) + if !ok { + return sdkerrors.Wrapf(ErrInvalid, "type: %T", i) + } + if a == 0 { + return sdkerrors.Wrap(ErrInvalid, "must be greater 0") + } + return nil +} + +func (v AccessConfig) ValidateBasic() error { + switch v.Type { + case Undefined, "": + return sdkerrors.Wrap(ErrEmpty, "type") + case Nobody, Everybody: + if len(v.Address) != 0 { + return sdkerrors.Wrap(ErrInvalid, "address not allowed for this type") + } + return nil + case OnlyAddress: + return sdk.VerifyAddressFormat(v.Address) + } + return sdkerrors.Wrapf(ErrInvalid, "unknown type: %q", v.Type) +} + +func (v AccessConfig) Allowed(actor sdk.AccAddress) bool { + switch v.Type { + case Nobody: + return false + case Everybody: + return true + case OnlyAddress: + return v.Address.Equals(actor) + default: + panic("unknown type") + } +} diff --git a/x/wasm/internal/types/params_test.go b/x/wasm/internal/types/params_test.go new file mode 100644 index 0000000000..4154284c35 --- /dev/null +++ b/x/wasm/internal/types/params_test.go @@ -0,0 +1,183 @@ +// nolint: scopelint +package types + +import ( + "encoding/json" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateParams(t *testing.T) { + var ( + anyAddress = make([]byte, sdk.AddrLen) + invalidAddress = make([]byte, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src Params + expErr bool + }{ + "all good with defaults": { + src: DefaultParams(), + }, + "all good with nobody": { + src: Params{ + UploadAccess: AllowNobody, + DefaultInstantiatePermission: Nobody, + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + }, + "all good with everybody": { + src: Params{ + UploadAccess: AllowEverybody, + DefaultInstantiatePermission: Everybody, + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + }, + "all good with only address": { + src: Params{ + UploadAccess: OnlyAddress.With(anyAddress), + DefaultInstantiatePermission: OnlyAddress, + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + }, + "reject empty type in instantiate permission": { + src: Params{ + UploadAccess: AllowNobody, + DefaultInstantiatePermission: "", + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + expErr: true, + }, + "reject unknown type in instantiate": { + src: Params{ + UploadAccess: AllowNobody, + DefaultInstantiatePermission: "Undefined", + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + expErr: true, + }, + "reject invalid address in only address": { + src: Params{ + UploadAccess: AccessConfig{Type: OnlyAddress, Address: invalidAddress}, + DefaultInstantiatePermission: OnlyAddress, + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + expErr: true, + }, + "reject UploadAccess Everybody with obsolete address": { + src: Params{ + UploadAccess: AccessConfig{Type: Everybody, Address: anyAddress}, + DefaultInstantiatePermission: OnlyAddress, + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + expErr: true, + }, + "reject UploadAccess Nobody with obsolete address": { + src: Params{ + UploadAccess: AccessConfig{Type: Nobody, Address: anyAddress}, + DefaultInstantiatePermission: OnlyAddress, + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + expErr: true, + }, + "reject empty UploadAccess": { + src: Params{ + DefaultInstantiatePermission: OnlyAddress, + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + expErr: true, + }, "reject undefined permission in UploadAccess": { + src: Params{ + UploadAccess: AccessConfig{Type: Undefined}, + DefaultInstantiatePermission: OnlyAddress, + MaxWasmCodeSize: DefaultMaxWasmCodeSize, + }, + expErr: true, + }, + "reject empty max wasm code size": { + src: Params{ + UploadAccess: AllowNobody, + DefaultInstantiatePermission: Nobody, + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestAccessTypeMarshalJson(t *testing.T) { + specs := map[string]struct { + src AccessType + exp string + }{ + "Undefined": {src: Undefined, exp: `"Undefined"`}, + "Nobody": {src: Nobody, exp: `"Nobody"`}, + "OnlyAddress": {src: OnlyAddress, exp: `"OnlyAddress"`}, + "Everybody": {src: Everybody, exp: `"Everybody"`}, + "unknown": {src: "", exp: `"Undefined"`}, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + got, err := json.Marshal(spec.src) + require.NoError(t, err) + assert.Equal(t, []byte(spec.exp), got) + }) + } +} +func TestAccessTypeUnMarshalJson(t *testing.T) { + specs := map[string]struct { + src string + exp AccessType + }{ + "Undefined": {src: `"Undefined"`, exp: Undefined}, + "Nobody": {src: `"Nobody"`, exp: Nobody}, + "OnlyAddress": {src: `"OnlyAddress"`, exp: OnlyAddress}, + "Everybody": {src: `"Everybody"`, exp: Everybody}, + "unknown": {src: `""`, exp: Undefined}, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + var got AccessType + err := json.Unmarshal([]byte(spec.src), &got) + require.NoError(t, err) + assert.Equal(t, spec.exp, got) + }) + } +} + +func TestParamsUnmarshalJson(t *testing.T) { + specs := map[string]struct { + src string + exp Params + }{ + + "defaults": { + src: `{"code_upload_access": {"permission": "Everybody"}, + "instantiate_default_permission": "Everybody", + "max_wasm_code_size": 614400}`, + exp: DefaultParams(), + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + var val Params + + err := json.Unmarshal([]byte(spec.src), &val) + require.NoError(t, err) + assert.Equal(t, spec.exp, val) + }) + } +} diff --git a/x/wasm/internal/types/proposal.go b/x/wasm/internal/types/proposal.go new file mode 100644 index 0000000000..f28422d2ce --- /dev/null +++ b/x/wasm/internal/types/proposal.go @@ -0,0 +1,382 @@ +package types + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" +) + +type ProposalType string + +const ( + ProposalTypeStoreCode ProposalType = "StoreCode" + ProposalTypeInstantiateContract ProposalType = "InstantiateContract" + ProposalTypeMigrateContract ProposalType = "MigrateContract" + ProposalTypeUpdateAdmin ProposalType = "UpdateAdmin" + ProposalTypeClearAdmin ProposalType = "ClearAdmin" +) + +// DisableAllProposals contains no wasm gov types. +var DisableAllProposals []ProposalType + +// EnableAllProposals contains all wasm gov types as keys. +var EnableAllProposals = []ProposalType{ + ProposalTypeStoreCode, + ProposalTypeInstantiateContract, + ProposalTypeMigrateContract, + ProposalTypeUpdateAdmin, + ProposalTypeClearAdmin, +} + +// ConvertToProposals maps each key to a ProposalType and returns a typed list. +// If any string is not a valid type (in this file), then return an error +func ConvertToProposals(keys []string) ([]ProposalType, error) { + valid := make(map[string]bool, len(EnableAllProposals)) + for _, key := range EnableAllProposals { + valid[string(key)] = true + } + + proposals := make([]ProposalType, len(keys)) + for i, key := range keys { + if _, ok := valid[key]; !ok { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "'%s' is not a valid ProposalType", key) + } + proposals[i] = ProposalType(key) + } + return proposals, nil +} + +func init() { // register new content types with the sdk + govtypes.RegisterProposalType(string(ProposalTypeStoreCode)) + govtypes.RegisterProposalType(string(ProposalTypeInstantiateContract)) + govtypes.RegisterProposalType(string(ProposalTypeMigrateContract)) + govtypes.RegisterProposalType(string(ProposalTypeUpdateAdmin)) + govtypes.RegisterProposalType(string(ProposalTypeClearAdmin)) + govtypes.RegisterProposalTypeCodec(StoreCodeProposal{}, "wasm/StoreCodeProposal") + govtypes.RegisterProposalTypeCodec(InstantiateContractProposal{}, "wasm/InstantiateContractProposal") + govtypes.RegisterProposalTypeCodec(MigrateContractProposal{}, "wasm/MigrateContractProposal") + govtypes.RegisterProposalTypeCodec(UpdateAdminProposal{}, "wasm/UpdateAdminProposal") + govtypes.RegisterProposalTypeCodec(ClearAdminProposal{}, "wasm/ClearAdminProposal") +} + +// WasmProposal contains common proposal data. +type WasmProposal struct { + Title string `json:"title" yaml:"title"` + Description string `json:"description" yaml:"description"` +} + +// GetTitle returns the title of a parameter change proposal. +func (p WasmProposal) GetTitle() string { return p.Title } + +// GetDescription returns the description of a parameter change proposal. +func (p WasmProposal) GetDescription() string { return p.Description } + +// ProposalRoute returns the routing key of a parameter change proposal. +func (p WasmProposal) ProposalRoute() string { return RouterKey } + +// ValidateBasic validates the proposal +func (p WasmProposal) ValidateBasic() error { + if strings.TrimSpace(p.Title) != p.Title { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal title must not start/end with white spaces") + } + if len(p.Title) == 0 { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal title cannot be blank") + } + if len(p.Title) > govtypes.MaxTitleLength { + return sdkerrors.Wrapf(govtypes.ErrInvalidProposalContent, "proposal title is longer than max length of %d", govtypes.MaxTitleLength) + } + if strings.TrimSpace(p.Description) != p.Description { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal description must not start/end with white spaces") + } + if len(p.Description) == 0 { + return sdkerrors.Wrap(govtypes.ErrInvalidProposalContent, "proposal description cannot be blank") + } + if len(p.Description) > govtypes.MaxDescriptionLength { + return sdkerrors.Wrapf(govtypes.ErrInvalidProposalContent, "proposal description is longer than max length of %d", govtypes.MaxDescriptionLength) + } + return nil +} + +// StoreCodeProposal gov proposal content type to store wasm code. +type StoreCodeProposal struct { + WasmProposal + // RunAs is the address that "owns" the code object + RunAs sdk.AccAddress `json:"run_as"` + // WASMByteCode can be raw or gzip compressed + WASMByteCode []byte `json:"wasm_byte_code"` + // Source is a valid absolute HTTPS URI to the contract's source code, optional + Source string `json:"source"` + // Builder is a valid docker image name with tag, optional + Builder string `json:"builder"` + // InstantiatePermission to apply on contract creation, optional + InstantiatePermission *AccessConfig `json:"instantiate_permission"` +} + +// ProposalType returns the type +func (p StoreCodeProposal) ProposalType() string { return string(ProposalTypeStoreCode) } + +// ValidateBasic validates the proposal +func (p StoreCodeProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if err := sdk.VerifyAddressFormat(p.RunAs); err != nil { + return sdkerrors.Wrap(err, "run as") + } + + if err := validateWasmCode(p.WASMByteCode); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "code bytes %s", err.Error()) + } + + if err := validateSourceURL(p.Source); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "source %s", err.Error()) + } + + if err := validateBuilder(p.Builder); err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "builder %s", err.Error()) + } + if p.InstantiatePermission != nil { + if err := p.InstantiatePermission.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "instantiate permission") + } + } + return nil +} + +// String implements the Stringer interface. +func (p StoreCodeProposal) String() string { + return fmt.Sprintf(`Store Code Proposal: + Title: %s + Description: %s + Run as: %s + WasmCode: %X + Source: %s + Builder: %s +`, p.Title, p.Description, p.RunAs, p.WASMByteCode, p.Source, p.Builder) +} + +func (p StoreCodeProposal) MarshalYAML() (interface{}, error) { + return struct { + WasmProposal `yaml:",inline"` + RunAs sdk.AccAddress `yaml:"run_as"` + WASMByteCode string `yaml:"wasm_byte_code"` + Source string `yaml:"source"` + Builder string `yaml:"builder"` + InstantiatePermission *AccessConfig `yaml:"instantiate_permission"` + }{ + WasmProposal: p.WasmProposal, + RunAs: p.RunAs, + WASMByteCode: base64.StdEncoding.EncodeToString(p.WASMByteCode), + Source: p.Source, + Builder: p.Builder, + InstantiatePermission: p.InstantiatePermission, + }, nil +} + +// InstantiateContractProposal gov proposal content type to instantiate a contract. +type InstantiateContractProposal struct { + WasmProposal + // RunAs is the address that pays the init funds + RunAs sdk.AccAddress `json:"run_as"` + // Admin is an optional address that can execute migrations + Admin sdk.AccAddress `json:"admin,omitempty"` + CodeID uint64 `json:"code_id"` + Label string `json:"label"` + InitMsg json.RawMessage `json:"init_msg"` + InitFunds sdk.Coins `json:"init_funds"` +} + +// ProposalType returns the type +func (p InstantiateContractProposal) ProposalType() string { + return string(ProposalTypeInstantiateContract) +} + +// ValidateBasic validates the proposal +func (p InstantiateContractProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if err := sdk.VerifyAddressFormat(p.RunAs); err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "run as") + } + + if p.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code id is required") + } + + if err := validateLabel(p.Label); err != nil { + return err + } + + if !p.InitFunds.IsValid() { + return sdkerrors.ErrInvalidCoins + } + + if len(p.Admin) != 0 { + if err := sdk.VerifyAddressFormat(p.Admin); err != nil { + return err + } + } + return nil +} + +// String implements the Stringer interface. +func (p InstantiateContractProposal) String() string { + return fmt.Sprintf(`Instantiate Code Proposal: + Title: %s + Description: %s + Run as: %s + Admin: %s + Code id: %d + Label: %s + InitMsg: %q + InitFunds: %s +`, p.Title, p.Description, p.RunAs, p.Admin, p.CodeID, p.Label, p.InitMsg, p.InitFunds) +} + +func (p InstantiateContractProposal) MarshalYAML() (interface{}, error) { + return struct { + WasmProposal `yaml:",inline"` + RunAs sdk.AccAddress `yaml:"run_as"` + Admin sdk.AccAddress `yaml:"admin"` + CodeID uint64 `yaml:"code_id"` + Label string `yaml:"label"` + InitMsg string `yaml:"init_msg"` + InitFunds sdk.Coins `yaml:"init_funds"` + }{ + WasmProposal: p.WasmProposal, + RunAs: p.RunAs, + Admin: p.Admin, + CodeID: p.CodeID, + Label: p.Label, + InitMsg: string(p.InitMsg), + InitFunds: p.InitFunds, + }, nil +} + +// MigrateContractProposal gov proposal content type to migrate a contract. +type MigrateContractProposal struct { + WasmProposal `yaml:",inline"` + Contract sdk.AccAddress `json:"contract"` + CodeID uint64 `json:"code_id"` + MigrateMsg json.RawMessage `json:"msg"` + // RunAs is the address that is passed to the contract's environment as sender + RunAs sdk.AccAddress `json:"run_as"` +} + +// ProposalType returns the type +func (p MigrateContractProposal) ProposalType() string { return string(ProposalTypeMigrateContract) } + +// ValidateBasic validates the proposal +func (p MigrateContractProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if p.CodeID == 0 { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code_id is required") + } + if err := sdk.VerifyAddressFormat(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if err := sdk.VerifyAddressFormat(p.RunAs); err != nil { + return sdkerrors.Wrap(err, "run as") + } + return nil +} + +// String implements the Stringer interface. +func (p MigrateContractProposal) String() string { + return fmt.Sprintf(`Migrate Contract Proposal: + Title: %s + Description: %s + Contract: %s + Code id: %d + Run as: %s + MigrateMsg %q +`, p.Title, p.Description, p.Contract, p.CodeID, p.RunAs, p.MigrateMsg) +} + +func (p MigrateContractProposal) MarshalYAML() (interface{}, error) { + return struct { + WasmProposal `yaml:",inline"` + Contract sdk.AccAddress `yaml:"contract"` + CodeID uint64 `yaml:"code_id"` + MigrateMsg string `yaml:"msg"` + RunAs sdk.AccAddress `yaml:"run_as"` + }{ + WasmProposal: p.WasmProposal, + Contract: p.Contract, + CodeID: p.CodeID, + MigrateMsg: string(p.MigrateMsg), + RunAs: p.RunAs, + }, nil +} + +// UpdateAdminProposal gov proposal content type to set an admin for a contract. +type UpdateAdminProposal struct { + WasmProposal `yaml:",inline"` + NewAdmin sdk.AccAddress `json:"new_admin" yaml:"new_admin"` + Contract sdk.AccAddress `json:"contract" yaml:"contract"` +} + +// ProposalType returns the type +func (p UpdateAdminProposal) ProposalType() string { return string(ProposalTypeUpdateAdmin) } + +// ValidateBasic validates the proposal +func (p UpdateAdminProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if err := sdk.VerifyAddressFormat(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + if err := sdk.VerifyAddressFormat(p.NewAdmin); err != nil { + return sdkerrors.Wrap(err, "new admin") + } + return nil +} + +// String implements the Stringer interface. +func (p UpdateAdminProposal) String() string { + return fmt.Sprintf(`Update Contract Admin Proposal: + Title: %s + Description: %s + Contract: %s + New Admin: %s +`, p.Title, p.Description, p.Contract, p.NewAdmin) +} + +// ClearAdminProposal gov proposal content type to clear the admin of a contract. +type ClearAdminProposal struct { + WasmProposal `yaml:",inline"` + + Contract sdk.AccAddress `json:"contract" yaml:"contract"` +} + +// ProposalType returns the type +func (p ClearAdminProposal) ProposalType() string { return string(ProposalTypeClearAdmin) } + +// ValidateBasic validates the proposal +func (p ClearAdminProposal) ValidateBasic() error { + if err := p.WasmProposal.ValidateBasic(); err != nil { + return err + } + if err := sdk.VerifyAddressFormat(p.Contract); err != nil { + return sdkerrors.Wrap(err, "contract") + } + return nil +} + +// String implements the Stringer interface. +func (p ClearAdminProposal) String() string { + return fmt.Sprintf(`Clear Contract Admin Proposal: + Title: %s + Description: %s + Contract: %s +`, p.Title, p.Description, p.Contract) +} diff --git a/x/wasm/internal/types/proposal_test.go b/x/wasm/internal/types/proposal_test.go new file mode 100644 index 0000000000..4f9aa41a3f --- /dev/null +++ b/x/wasm/internal/types/proposal_test.go @@ -0,0 +1,657 @@ +// nolint: dupl, scopelint +package types + +import ( + "bytes" + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/gov" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestValidateWasmProposal(t *testing.T) { + specs := map[string]struct { + src WasmProposal + expErr bool + }{ + "all good": {src: WasmProposal{ + Title: "Foo", + Description: "Bar", + }}, + "prevent empty title": { + src: WasmProposal{ + Description: "Bar", + }, + expErr: true, + }, + "prevent white space only title": { + src: WasmProposal{ + Title: " ", + Description: "Bar", + }, + expErr: true, + }, + "prevent leading white spaces in title": { + src: WasmProposal{ + Title: " Foo", + Description: "Bar", + }, + expErr: true, + }, + "prevent title exceeds max length ": { + src: WasmProposal{ + Title: strings.Repeat("a", govtypes.MaxTitleLength+1), + Description: "Bar", + }, + expErr: true, + }, + "prevent empty description": { + src: WasmProposal{ + Title: "Foo", + }, + expErr: true, + }, + "prevent leading white spaces in description": { + src: WasmProposal{ + Title: "Foo", + Description: " Bar", + }, + expErr: true, + }, + "prevent white space only description": { + src: WasmProposal{ + Title: "Foo", + Description: " ", + }, + expErr: true, + }, + "prevent descr exceeds max length ": { + src: WasmProposal{ + Title: "Foo", + Description: strings.Repeat("a", govtypes.MaxDescriptionLength+1), + }, + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateStoreCodeProposal(t *testing.T) { + var ( + anyAddress sdk.AccAddress = bytes.Repeat([]byte{0x0}, sdk.AddrLen) + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src StoreCodeProposal + expErr bool + }{ + "all good": { + src: StoreCodeProposalFixture(), + }, + "with instantiate permission": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + accessConfig := OnlyAddress.With(anyAddress) + p.InstantiatePermission = &accessConfig + }), + }, + + "without source": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Source = "" + }), + }, + "base data missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "run_as missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.RunAs = nil + }), + expErr: true, + }, + "run_as invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.RunAs = invalidAddress + }), + expErr: true, + }, + "wasm code missing": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = nil + }), + expErr: true, + }, + "wasm code invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = bytes.Repeat([]byte{0x0}, MaxWasmSize+1) + }), + expErr: true, + }, + "source invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Source = "not an url" + }), + expErr: true, + }, + "builder invalid": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.Builder = "not a builder" + }), + expErr: true, + }, + "with invalid instantiate permission": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.InstantiatePermission = &AccessConfig{} + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateInstantiateContractProposal(t *testing.T) { + var ( + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src InstantiateContractProposal + expErr bool + }{ + "all good": { + src: InstantiateContractProposalFixture(), + }, + "without admin": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Admin = nil + }), + }, + "without init msg": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitMsg = nil + }), + }, + "without init funds": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitFunds = nil + }), + }, + "base data missing": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "run_as missing": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.RunAs = nil + }), + expErr: true, + }, + "run_as invalid": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.RunAs = invalidAddress + }), + expErr: true, + }, + "admin invalid": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Admin = invalidAddress + }), + expErr: true, + }, + "code id empty": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.CodeID = 0 + }), + expErr: true, + }, + "label empty": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.Label = "" + }), + expErr: true, + }, + "init funds negative": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitFunds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(-1)}} + }), + expErr: true, + }, + "init funds with duplicates": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitFunds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "foo", Amount: sdk.NewInt(2)}} + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateMigrateContractProposal(t *testing.T) { + var ( + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src MigrateContractProposal + expErr bool + }{ + "all good": { + src: MigrateContractProposalFixture(), + }, + "without migrate msg": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.MigrateMsg = nil + }), + }, + "base data missing": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "contract missing": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Contract = nil + }), + expErr: true, + }, + "contract invalid": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + "code id empty": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.CodeID = 0 + }), + expErr: true, + }, + "run_as missing": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.RunAs = nil + }), + expErr: true, + }, + "run_as invalid": { + src: MigrateContractProposalFixture(func(p *MigrateContractProposal) { + p.RunAs = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateUpdateAdminProposal(t *testing.T) { + var ( + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src UpdateAdminProposal + expErr bool + }{ + "all good": { + src: UpdateAdminProposalFixture(), + }, + "base data missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "contract missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.Contract = nil + }), + expErr: true, + }, + "contract invalid": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + "admin missing": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.NewAdmin = nil + }), + expErr: true, + }, + "admin invalid": { + src: UpdateAdminProposalFixture(func(p *UpdateAdminProposal) { + p.NewAdmin = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateClearAdminProposal(t *testing.T) { + var ( + invalidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen-1) + ) + + specs := map[string]struct { + src ClearAdminProposal + expErr bool + }{ + "all good": { + src: ClearAdminProposalFixture(), + }, + "base data missing": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.WasmProposal = WasmProposal{} + }), + expErr: true, + }, + "contract missing": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.Contract = nil + }), + expErr: true, + }, + "contract invalid": { + src: ClearAdminProposalFixture(func(p *ClearAdminProposal) { + p.Contract = invalidAddress + }), + expErr: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + err := spec.src.ValidateBasic() + if spec.expErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestProposalStrings(t *testing.T) { + specs := map[string]struct { + src gov.Content + exp string + }{ + "store code": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = []byte{01, 02, 03, 04, 05, 06, 07, 0x08, 0x09, 0x0a} + }), + exp: `Store Code Proposal: + Title: Foo + Description: Bar + Run as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + WasmCode: 0102030405060708090A + Source: https://example.com/code + Builder: foo/bar:latest +`, + }, + "instantiate contract": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitFunds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "bar", Amount: sdk.NewInt(2)}} + }), + exp: `Instantiate Code Proposal: + Title: Foo + Description: Bar + Run as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Code id: 1 + Label: testing + InitMsg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\",\"beneficiary\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\"}" + InitFunds: 1foo,2bar +`, + }, + "instantiate contract without funds": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.InitFunds = nil }), + exp: `Instantiate Code Proposal: + Title: Foo + Description: Bar + Run as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Code id: 1 + Label: testing + InitMsg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\",\"beneficiary\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\"}" + InitFunds: +`, + }, + "instantiate contract without admin": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.Admin = nil }), + exp: `Instantiate Code Proposal: + Title: Foo + Description: Bar + Run as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + Admin: + Code id: 1 + Label: testing + InitMsg: "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\",\"beneficiary\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\"}" + InitFunds: +`, + }, + "migrate contract": { + src: MigrateContractProposalFixture(), + exp: `Migrate Contract Proposal: + Title: Foo + Description: Bar + Contract: cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5 + Code id: 1 + Run as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du + MigrateMsg "{\"verifier\":\"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du\"}" +`, + }, + "update admin": { + src: UpdateAdminProposalFixture(), + exp: `Update Contract Admin Proposal: + Title: Foo + Description: Bar + Contract: cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5 + New Admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +`, + }, + "clear admin": { + src: ClearAdminProposalFixture(), + exp: `Clear Contract Admin Proposal: + Title: Foo + Description: Bar + Contract: cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5 +`, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + assert.Equal(t, spec.exp, spec.src.String()) + }) + } +} + +func TestProposalYaml(t *testing.T) { + specs := map[string]struct { + src gov.Content + exp string + }{ + "store code": { + src: StoreCodeProposalFixture(func(p *StoreCodeProposal) { + p.WASMByteCode = []byte{01, 02, 03, 04, 05, 06, 07, 0x08, 0x09, 0x0a} + }), + exp: `title: Foo +description: Bar +run_as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +wasm_byte_code: AQIDBAUGBwgJCg== +source: https://example.com/code +builder: foo/bar:latest +instantiate_permission: null +`, + }, + "instantiate contract": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { + p.InitFunds = sdk.Coins{{Denom: "foo", Amount: sdk.NewInt(1)}, {Denom: "bar", Amount: sdk.NewInt(2)}} + }), + exp: `title: Foo +description: Bar +run_as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +code_id: 1 +label: testing +init_msg: '{"verifier":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du","beneficiary":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du"}' +init_funds: +- denom: foo + amount: "1" +- denom: bar + amount: "2" +`, + }, + "instantiate contract without funds": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.InitFunds = nil }), + exp: `title: Foo +description: Bar +run_as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +code_id: 1 +label: testing +init_msg: '{"verifier":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du","beneficiary":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du"}' +init_funds: [] +`, + }, + "instantiate contract without admin": { + src: InstantiateContractProposalFixture(func(p *InstantiateContractProposal) { p.Admin = nil }), + exp: `title: Foo +description: Bar +run_as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +admin: "" +code_id: 1 +label: testing +init_msg: '{"verifier":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du","beneficiary":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du"}' +init_funds: [] +`, + }, + "migrate contract": { + src: MigrateContractProposalFixture(), + exp: `title: Foo +description: Bar +contract: cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5 +code_id: 1 +msg: '{"verifier":"cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du"}' +run_as: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +`, + }, + "update admin": { + src: UpdateAdminProposalFixture(), + exp: `title: Foo +description: Bar +new_admin: cosmos1qyqszqgpqyqszqgpqyqszqgpqyqszqgpjnp7du +contract: cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5 +`, + }, + "clear admin": { + src: ClearAdminProposalFixture(), + exp: `title: Foo +description: Bar +contract: cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5 +`, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + v, err := yaml.Marshal(&spec.src) + require.NoError(t, err) + assert.Equal(t, spec.exp, string(v)) + }) + } +} + +func TestConvertToProposals(t *testing.T) { + cases := map[string]struct { + input string + isError bool + proposals []ProposalType + }{ + "one proper item": { + input: "UpdateAdmin", + proposals: []ProposalType{ProposalTypeUpdateAdmin}, + }, + "multiple proper items": { + input: "StoreCode,InstantiateContract,MigrateContract", + proposals: []ProposalType{ProposalTypeStoreCode, ProposalTypeInstantiateContract, ProposalTypeMigrateContract}, + }, + "empty trailing item": { + input: "StoreCode,", + isError: true, + }, + "invalid item": { + input: "StoreCode,InvalidProposalType", + isError: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + chunks := strings.Split(tc.input, ",") + proposals, err := ConvertToProposals(chunks) + if tc.isError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, proposals, tc.proposals) + } + }) + } +} diff --git a/x/wasm/internal/types/querier.go b/x/wasm/internal/types/querier.go new file mode 100644 index 0000000000..09167def7f --- /dev/null +++ b/x/wasm/internal/types/querier.go @@ -0,0 +1,151 @@ +package types + +import ( + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + tmbytes "github.com/tendermint/tendermint/libs/bytes" +) + +// Response model for CodeInfo query such as `QueryListCode` and `QueryGetCode` +type CodeInfoResponse interface { + GetID() uint64 + GetCreator() sdk.AccAddress + GetDataHash() tmbytes.HexBytes + GetSource() string + GetBuilder() string + GetData() []byte +} + +func NewCodeInfoResponse(id uint64, info CodeInfo, data []byte) CodeInfoResponse { + return codeInfo{ + ID: id, + Creator: info.Creator, + DataHash: info.CodeHash, + Source: info.Source, + Builder: info.Builder, + Data: data, + } +} + +type codeInfo struct { + ID uint64 `json:"id"` + Creator sdk.AccAddress `json:"creator"` + DataHash tmbytes.HexBytes `json:"data_hash"` + Source string `json:"source"` + Builder string `json:"builder"` + Data []byte `json:"data,omitempty" yaml:"data"` // Data is the entire wasm bytecode +} + +func (r codeInfo) GetID() uint64 { + return r.ID +} + +func (r codeInfo) GetCreator() sdk.AccAddress { + return r.Creator +} + +func (r codeInfo) GetDataHash() tmbytes.HexBytes { + return r.DataHash +} + +func (r codeInfo) GetSource() string { + return r.Source +} + +func (r codeInfo) GetBuilder() string { + return r.Builder +} + +func (r codeInfo) GetData() []byte { + return r.Data +} + +// Response model for ContractInfo query such as `QueryListContractByCode` and `QueryGetContract` +type ContractInfoResponse interface { + GetCodeID() uint64 + GetCreator() sdk.AccAddress + GetAdmin() sdk.AccAddress + GetLabel() string + GetAddress() sdk.AccAddress + LessThan(other ContractInfoResponse) bool +} + +func NewContractInfoResponse(info ContractInfo, addr sdk.AccAddress) ContractInfoResponse { + return contractInfo{ + CodeID: info.CodeID, + Creator: info.Creator, + Admin: info.Admin, + Label: info.Label, + Address: addr, + created: info.Created, + } +} + +// contractInfo adds the address (key) to the ContractInfo representation +type contractInfo struct { + CodeID uint64 `json:"code_id"` + Creator sdk.AccAddress `json:"creator"` + Admin sdk.AccAddress `json:"admin,omitempty"` + Label string `json:"label"` + Address sdk.AccAddress `json:"address"` + // never show this in query results, just use for sorting + created *AbsoluteTxPosition +} + +func (r contractInfo) GetCodeID() uint64 { + return r.CodeID +} + +func (r contractInfo) GetCreator() sdk.AccAddress { + return r.Creator +} + +func (r contractInfo) GetAdmin() sdk.AccAddress { + return r.Admin +} + +func (r contractInfo) GetLabel() string { + return r.Label +} + +func (r contractInfo) GetAddress() sdk.AccAddress { + return r.Address +} + +func (r contractInfo) LessThan(other ContractInfoResponse) bool { + return r.created.LessThan(other.(contractInfo).created) +} + +// Response model for ContractHistory query (`QueryContractHistory`) +type ContractHistoryResponse interface { + GetOperation() ContractCodeHistoryOperationType + GetCodeID() uint64 + GetMsg() json.RawMessage +} + +func NewContractHistoryResponse(entry ContractCodeHistoryEntry) ContractHistoryResponse { + return contractHistory{ + Operation: entry.Operation, + CodeID: entry.CodeID, + Msg: entry.Msg, + } +} + +type contractHistory struct { + Operation ContractCodeHistoryOperationType `json:"operation"` + CodeID uint64 `json:"code_id"` + Msg json.RawMessage `json:"msg,omitempty"` +} + +func (r contractHistory) GetOperation() ContractCodeHistoryOperationType { + return r.Operation +} + +func (r contractHistory) GetCodeID() uint64 { + return r.CodeID +} + +func (r contractHistory) GetMsg() json.RawMessage { + return r.Msg +} diff --git a/x/wasm/internal/types/querier_router.go b/x/wasm/internal/types/querier_router.go new file mode 100644 index 0000000000..ae98a8edea --- /dev/null +++ b/x/wasm/internal/types/querier_router.go @@ -0,0 +1,65 @@ +package types + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ QueryRouter = (*querierRouter)(nil) + +type EncodeQuerier func(ctx sdk.Context, jsonQuerier json.RawMessage) ([]byte, error) + +// QueryRouter provides queryables for each query path. +type QueryRouter interface { + AddRoute(r string, q EncodeQuerier) QueryRouter + HasRoute(r string) bool + GetRoute(path string) EncodeQuerier + Seal() +} + +type querierRouter struct { + routes map[string]EncodeQuerier + sealed bool +} + +func NewQuerierRouter() QueryRouter { + return &querierRouter{ + routes: make(map[string]EncodeQuerier), + } +} + +func (rtr *querierRouter) Seal() { + if rtr.sealed { + panic("querier router already sealed") + } + rtr.sealed = true +} + +func (rtr *querierRouter) AddRoute(path string, q EncodeQuerier) QueryRouter { + if rtr.sealed { + panic("router sealed; cannot add route handler") + } + if !sdk.IsAlphaNumeric(path) { + panic("querier route expressions can only contain alphanumeric characters") + } + if rtr.HasRoute(path) { + panic(fmt.Sprintf("querier route %s has already been initialized", path)) + } + + rtr.routes[path] = q + return rtr +} + +func (rtr *querierRouter) HasRoute(path string) bool { + return rtr.routes[path] != nil +} + +func (rtr *querierRouter) GetRoute(path string) EncodeQuerier { + if !rtr.HasRoute(path) { + panic(fmt.Sprintf("querier route \"%s\" does not exist", path)) + } + + return rtr.routes[path] +} diff --git a/x/wasm/internal/types/querier_router_test.go b/x/wasm/internal/types/querier_router_test.go new file mode 100644 index 0000000000..06bd253374 --- /dev/null +++ b/x/wasm/internal/types/querier_router_test.go @@ -0,0 +1,28 @@ +package types + +import ( + "encoding/json" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func testQuerierHandler(ctx sdk.Context, jsonQuerier json.RawMessage) ([]byte, error) { + return nil, nil +} + +func TestQuerierRouterSeal(t *testing.T) { + r := NewQuerierRouter() + r.Seal() + require.Panics(t, func() { r.AddRoute("test", nil) }) + require.Panics(t, func() { r.Seal() }) +} + +func TestQuerierRouter(t *testing.T) { + r := NewQuerierRouter() + r.AddRoute("test", testQuerierHandler) + require.True(t, r.HasRoute("test")) + require.Panics(t, func() { r.AddRoute("test", testQuerierHandler) }) + require.Panics(t, func() { r.AddRoute(" ", testQuerierHandler) }) +} diff --git a/x/wasm/internal/types/router.go b/x/wasm/internal/types/router.go new file mode 100644 index 0000000000..f8555750f9 --- /dev/null +++ b/x/wasm/internal/types/router.go @@ -0,0 +1,67 @@ +package types + +import ( + "encoding/json" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ Router = (*router)(nil) + +type EncodeHandler func(jsonMsg json.RawMessage) ([]sdk.Msg, error) + +type Router interface { + AddRoute(r string, h EncodeHandler) (rtr Router) + HasRoute(r string) bool + GetRoute(path string) (h EncodeHandler) + Seal() +} + +type router struct { + routes map[string]EncodeHandler + sealed bool +} + +func NewRouter() Router { + return &router{ + routes: make(map[string]EncodeHandler), + } +} + +func (rtr *router) Seal() { + if rtr.sealed { + panic("router already sealed") + } + rtr.sealed = true +} + +func (rtr *router) AddRoute(path string, h EncodeHandler) Router { + if rtr.sealed { + panic("router sealed; cannot add route handler") + } + + if !sdk.IsAlphaNumeric(path) { + panic("route expressions can only contain alphanumeric characters") + } + if rtr.HasRoute(path) { + panic(fmt.Sprintf("route %s has already been initialized", path)) + } + + rtr.routes[path] = h + return rtr +} + +// HasRoute returns true if the router has a path registered or false otherwise. +func (rtr *router) HasRoute(path string) bool { + return rtr.routes[path] != nil +} + +// GetRoute returns a Handler for a given path. +func (rtr *router) GetRoute(path string) EncodeHandler { + if !rtr.HasRoute(path) { + panic(fmt.Sprintf("route \"%s\" does not exist", path)) + } + + return rtr.routes[path] +} diff --git a/x/wasm/internal/types/router_test.go b/x/wasm/internal/types/router_test.go new file mode 100644 index 0000000000..e0872ff4c9 --- /dev/null +++ b/x/wasm/internal/types/router_test.go @@ -0,0 +1,26 @@ +package types + +import ( + "encoding/json" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func testHandler(jsonMsg json.RawMessage) ([]sdk.Msg, error) { return []sdk.Msg{}, nil } + +func TestRouterSeal(t *testing.T) { + r := NewRouter() + r.Seal() + require.Panics(t, func() { r.AddRoute("test", nil) }) + require.Panics(t, func() { r.Seal() }) +} + +func TestRouter(t *testing.T) { + r := NewRouter() + r.AddRoute("test", testHandler) + require.True(t, r.HasRoute("test")) + require.Panics(t, func() { r.AddRoute("test", testHandler) }) + require.Panics(t, func() { r.AddRoute(" ", testHandler) }) +} diff --git a/x/wasm/internal/types/test_fixtures.go b/x/wasm/internal/types/test_fixtures.go new file mode 100644 index 0000000000..60b1e683f6 --- /dev/null +++ b/x/wasm/internal/types/test_fixtures.go @@ -0,0 +1,243 @@ +package types + +import ( + "bytes" + "crypto/sha256" + "encoding/json" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/tendermint/libs/rand" +) + +func GenesisFixture(mutators ...func(*GenesisState)) GenesisState { + const ( + numCodes = 2 + numContracts = 2 + numSequences = 2 + ) + + fixture := GenesisState{ + Params: DefaultParams(), + Codes: make([]Code, numCodes), + Contracts: make([]Contract, numContracts), + Sequences: make([]Sequence, numSequences), + } + for i := 0; i < numCodes; i++ { + fixture.Codes[i] = CodeFixture() + } + for i := 0; i < numContracts; i++ { + fixture.Contracts[i] = ContractFixture() + } + for i := 0; i < numSequences; i++ { + fixture.Sequences[i] = Sequence{ + IDKey: rand.Bytes(5), + Value: uint64(i), + } + } + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func CodeFixture(mutators ...func(*Code)) Code { + wasmCode := rand.Bytes(100) + + fixture := Code{ + CodeID: 1, + CodeInfo: CodeInfoFixture(WithSHA256CodeHash(wasmCode)), + CodesBytes: wasmCode, + } + + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func CodeInfoFixture(mutators ...func(*CodeInfo)) CodeInfo { + wasmCode := bytes.Repeat([]byte{0x1}, 10) + codeHash := sha256.Sum256(wasmCode) + anyAddress := make([]byte, 20) + fixture := CodeInfo{ + CodeHash: codeHash[:], + Creator: anyAddress, + Source: "https://example.com", + Builder: "my/builder:tag", + InstantiateConfig: AllowEverybody, + } + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func ContractFixture(mutators ...func(*Contract)) Contract { + anyAddress := make([]byte, 20) + fixture := Contract{ + ContractAddress: anyAddress, + ContractInfo: ContractInfoFixture(OnlyGenesisFields), + ContractState: []Model{{Key: []byte("anyKey"), Value: []byte("anyValue")}}, + } + + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func OnlyGenesisFields(info *ContractInfo) { + info.Created = nil +} + +func ContractInfoFixture(mutators ...func(*ContractInfo)) ContractInfo { + anyAddress := make([]byte, 20) + fixture := ContractInfo{ + CodeID: 1, + Creator: anyAddress, + Label: "any", + Created: &AbsoluteTxPosition{BlockHeight: 1, TxIndex: 1}, + } + + for _, m := range mutators { + m(&fixture) + } + return fixture +} + +func WithSHA256CodeHash(wasmCode []byte) func(info *CodeInfo) { + return func(info *CodeInfo) { + codeHash := sha256.Sum256(wasmCode) + info.CodeHash = codeHash[:] + } +} + +func StoreCodeProposalFixture(mutators ...func(*StoreCodeProposal)) StoreCodeProposal { + var anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + p := StoreCodeProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + RunAs: anyValidAddress, + WASMByteCode: []byte{0x0}, + Source: "https://example.com/code", + Builder: "foo/bar:latest", + } + for _, m := range mutators { + m(&p) + } + return p +} + +func InstantiateContractProposalFixture(mutators ...func(p *InstantiateContractProposal)) InstantiateContractProposal { + var ( + anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + + initMsg = struct { + Verifier sdk.AccAddress `json:"verifier"` + Beneficiary sdk.AccAddress `json:"beneficiary"` + }{ + Verifier: anyValidAddress, + Beneficiary: anyValidAddress, + } + ) + + initMsgBz, err := json.Marshal(initMsg) + if err != nil { + panic(err) + } + p := InstantiateContractProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + RunAs: anyValidAddress, + Admin: anyValidAddress, + CodeID: 1, + Label: "testing", + InitMsg: initMsgBz, + InitFunds: nil, + } + + for _, m := range mutators { + m(&p) + } + return p +} + +func MigrateContractProposalFixture(mutators ...func(p *MigrateContractProposal)) MigrateContractProposal { + var ( + anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + + migMsg = struct { + Verifier sdk.AccAddress `json:"verifier"` + }{Verifier: anyValidAddress} + ) + + migMsgBz, err := json.Marshal(migMsg) + if err != nil { + panic(err) + } + contractAddr, err := sdk.AccAddressFromBech32("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5") + if err != nil { + panic(err) + } + + p := MigrateContractProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + CodeID: 1, + MigrateMsg: migMsgBz, + RunAs: anyValidAddress, + } + + for _, m := range mutators { + m(&p) + } + return p +} + +func UpdateAdminProposalFixture(mutators ...func(p *UpdateAdminProposal)) UpdateAdminProposal { + var anyValidAddress sdk.AccAddress = bytes.Repeat([]byte{0x1}, sdk.AddrLen) + + contractAddr, err := sdk.AccAddressFromBech32("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5") + if err != nil { + panic(err) + } + + p := UpdateAdminProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + NewAdmin: anyValidAddress, + Contract: contractAddr, + } + for _, m := range mutators { + m(&p) + } + return p +} + +func ClearAdminProposalFixture(mutators ...func(p *ClearAdminProposal)) ClearAdminProposal { + contractAddr, err := sdk.AccAddressFromBech32("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5") + if err != nil { + panic(err) + } + + p := ClearAdminProposal{ + WasmProposal: WasmProposal{ + Title: "Foo", + Description: "Bar", + }, + Contract: contractAddr, + } + for _, m := range mutators { + m(&p) + } + return p +} diff --git a/x/wasm/internal/types/types.go b/x/wasm/internal/types/types.go new file mode 100644 index 0000000000..245e3e50d3 --- /dev/null +++ b/x/wasm/internal/types/types.go @@ -0,0 +1,270 @@ +package types + +import ( + "encoding/json" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + tmBytes "github.com/tendermint/tendermint/libs/bytes" + + wasmTypes "github.com/CosmWasm/wasmvm/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const defaultMemoryCacheSize uint32 = 100 // in MiB +const defaultQueryGasLimit = uint64(3000000) +const defaultContractDebugMode = false + +// Model is a struct that holds a KV pair +type Model struct { + // hex-encode key to read it better (this is often ascii) + Key tmBytes.HexBytes `json:"key"` + // base64-encode raw value + Value []byte `json:"val"` +} + +func (m Model) ValidateBasic() error { + if len(m.Key) == 0 { + return sdkerrors.Wrap(ErrEmpty, "key") + } + return nil +} + +// CodeInfo is data for the uploaded contract WASM code +type CodeInfo struct { + CodeHash []byte `json:"code_hash"` + Creator sdk.AccAddress `json:"creator"` + Source string `json:"source"` + Builder string `json:"builder"` + InstantiateConfig AccessConfig `json:"instantiate_config"` +} + +func (c CodeInfo) ValidateBasic() error { + if len(c.CodeHash) == 0 { + return sdkerrors.Wrap(ErrEmpty, "code hash") + } + if err := sdk.VerifyAddressFormat(c.Creator); err != nil { + return sdkerrors.Wrap(err, "creator") + } + if err := validateSourceURL(c.Source); err != nil { + return sdkerrors.Wrap(err, "source") + } + if err := validateBuilder(c.Builder); err != nil { + return sdkerrors.Wrap(err, "builder") + } + if err := c.InstantiateConfig.ValidateBasic(); err != nil { + return sdkerrors.Wrap(err, "instantiate config") + } + return nil +} + +// NewCodeInfo fills a new Contract struct +func NewCodeInfo(codeHash []byte, creator sdk.AccAddress, source string, builder string, instantiatePermission AccessConfig) CodeInfo { + return CodeInfo{ + CodeHash: codeHash, + Creator: creator, + Source: source, + Builder: builder, + InstantiateConfig: instantiatePermission, + } +} + +type ContractCodeHistoryOperationType string + +const ( + InitContractCodeHistoryType ContractCodeHistoryOperationType = "Init" + MigrateContractCodeHistoryType ContractCodeHistoryOperationType = "Migrate" + GenesisContractCodeHistoryType ContractCodeHistoryOperationType = "Genesis" +) + +var AllCodeHistoryTypes = []ContractCodeHistoryOperationType{InitContractCodeHistoryType, MigrateContractCodeHistoryType} + +// ContractCodeHistoryEntry stores code updates to a contract. +type ContractCodeHistoryEntry struct { + Operation ContractCodeHistoryOperationType `json:"operation"` + CodeID uint64 `json:"code_id"` + Updated *AbsoluteTxPosition `json:"updated,omitempty"` + Msg json.RawMessage `json:"msg,omitempty"` +} + +// ContractInfo stores a WASM contract instance +type ContractInfo struct { + CodeID uint64 `json:"code_id"` + Creator sdk.AccAddress `json:"creator"` + Admin sdk.AccAddress `json:"admin,omitempty"` + Label string `json:"label"` + // never show this in query results, just use for sorting + // (Note: when using json tag "-" amino refused to serialize it...) + Created *AbsoluteTxPosition `json:"created,omitempty"` +} + +// NewContractInfo creates a new instance of a given WASM contract info +func NewContractInfo(codeID uint64, creator, admin sdk.AccAddress, label string, createdAt *AbsoluteTxPosition) ContractInfo { + return ContractInfo{ + CodeID: codeID, + Creator: creator, + Admin: admin, + Label: label, + Created: createdAt, + } +} +func (c *ContractInfo) ValidateBasic() error { + if c.CodeID == 0 { + return sdkerrors.Wrap(ErrEmpty, "code id") + } + if err := sdk.VerifyAddressFormat(c.Creator); err != nil { + return sdkerrors.Wrap(err, "creator") + } + if c.Admin != nil { + if err := sdk.VerifyAddressFormat(c.Admin); err != nil { + return sdkerrors.Wrap(err, "admin") + } + } + if err := validateLabel(c.Label); err != nil { + return sdkerrors.Wrap(err, "label") + } + return nil +} + +func (c ContractInfo) InitialHistory(initMsg []byte) ContractCodeHistoryEntry { + return ContractCodeHistoryEntry{ + Operation: InitContractCodeHistoryType, + CodeID: c.CodeID, + Updated: c.Created, + Msg: initMsg, + } +} + +func (c *ContractInfo) AddMigration(ctx sdk.Context, codeID uint64, msg []byte) ContractCodeHistoryEntry { + h := ContractCodeHistoryEntry{ + Operation: MigrateContractCodeHistoryType, + CodeID: codeID, + Updated: NewAbsoluteTxPosition(ctx), + Msg: msg, + } + c.CodeID = codeID + return h +} + +// ResetFromGenesis resets contracts timestamp and history. +func (c *ContractInfo) ResetFromGenesis(ctx sdk.Context) ContractCodeHistoryEntry { + c.Created = NewAbsoluteTxPosition(ctx) + return ContractCodeHistoryEntry{ + Operation: GenesisContractCodeHistoryType, + CodeID: c.CodeID, + Updated: c.Created, + } +} + +// AbsoluteTxPosition can be used to sort contracts +type AbsoluteTxPosition struct { + // BlockHeight is the block the contract was created at + BlockHeight int64 + // TxIndex is a monotonic counter within the block (actual transaction index, or gas consumed) + TxIndex uint64 +} + +// LessThan can be used to sort +func (a *AbsoluteTxPosition) LessThan(b *AbsoluteTxPosition) bool { + if a == nil { + return true + } + if b == nil { + return false + } + return a.BlockHeight < b.BlockHeight || (a.BlockHeight == b.BlockHeight && a.TxIndex < b.TxIndex) +} + +// NewAbsoluteTxPosition gets a timestamp from the context +func NewAbsoluteTxPosition(ctx sdk.Context) *AbsoluteTxPosition { + // we must safely handle nil gas meters + var index uint64 + meter := ctx.BlockGasMeter() + if meter != nil { + index = meter.GasConsumed() + } + return &AbsoluteTxPosition{ + BlockHeight: ctx.BlockHeight(), + TxIndex: index, + } +} + +// NewEnv initializes the environment for a contract instance +func NewEnv(ctx sdk.Context, contractAddr sdk.AccAddress) wasmTypes.Env { + // safety checks before casting below + if ctx.BlockHeight() < 0 { + panic("Block height must never be negative") + } + sec := ctx.BlockTime().Unix() + if sec < 0 { + panic("Block (unix) time must never be negative ") + } + nano := ctx.BlockTime().Nanosecond() + env := wasmTypes.Env{ + Block: wasmTypes.BlockInfo{ + Height: uint64(ctx.BlockHeight()), + Time: uint64(sec), + TimeNanos: uint64(nano), + ChainID: ctx.ChainID(), + }, + Contract: wasmTypes.ContractInfo{ + Address: contractAddr.String(), + }, + } + return env +} + +// NewInfo initializes the MessageInfo for a contract instance +func NewInfo(creator sdk.AccAddress, deposit sdk.Coins) wasmTypes.MessageInfo { + return wasmTypes.MessageInfo{ + Sender: creator.String(), + SentFunds: NewWasmCoins(deposit), + } +} + +// NewWasmCoins translates between Cosmos SDK coins and Wasm coins +func NewWasmCoins(cosmosCoins sdk.Coins) (wasmCoins []wasmTypes.Coin) { + for _, coin := range cosmosCoins { + wasmCoin := wasmTypes.Coin{ + Denom: coin.Denom, + Amount: coin.Amount.String(), + } + wasmCoins = append(wasmCoins, wasmCoin) + } + return wasmCoins +} + +const CustomEventType = "wasm" +const AttributeKeyContractAddr = "contract_address" + +// ParseEvents converts wasm LogAttributes into an sdk.Events (with 0 or 1 elements) +func ParseEvents(logs []wasmTypes.EventAttribute, contractAddr sdk.AccAddress) sdk.Events { + if len(logs) == 0 { + return nil + } + // we always tag with the contract address issuing this event + attrs := []sdk.Attribute{sdk.NewAttribute(AttributeKeyContractAddr, contractAddr.String())} + for _, l := range logs { + // and reserve the contract_address key for our use (not contract) + if l.Key != AttributeKeyContractAddr { + attr := sdk.NewAttribute(l.Key, l.Value) + attrs = append(attrs, attr) + } + } + return sdk.Events{sdk.NewEvent(CustomEventType, attrs...)} +} + +// WasmConfig is the extra config required for wasm +type WasmConfig struct { + SmartQueryGasLimit uint64 `mapstructure:"query_gas_limit"` + MemoryCacheSize uint32 `mapstructure:"memory_cache_size"` // in MiB + ContractDebugMode bool `mapstructure:"debug_mode"` +} + +// DefaultWasmConfig returns the default settings for WasmConfig +func DefaultWasmConfig() WasmConfig { + return WasmConfig{ + SmartQueryGasLimit: defaultQueryGasLimit, + MemoryCacheSize: defaultMemoryCacheSize, + ContractDebugMode: defaultContractDebugMode, + } +} diff --git a/x/wasm/internal/types/types_test.go b/x/wasm/internal/types/types_test.go new file mode 100644 index 0000000000..1b208a8991 --- /dev/null +++ b/x/wasm/internal/types/types_test.go @@ -0,0 +1,121 @@ +// nolint: scopelint +package types + +import ( + "strings" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" +) + +func TestContractInfoValidateBasic(t *testing.T) { + specs := map[string]struct { + srcMutator func(*ContractInfo) + expError bool + }{ + "all good": {srcMutator: func(_ *ContractInfo) {}}, + "code id empty": { + srcMutator: func(c *ContractInfo) { c.CodeID = 0 }, + expError: true, + }, + "creator empty": { + srcMutator: func(c *ContractInfo) { c.Creator = nil }, + expError: true, + }, + "creator not an address": { + srcMutator: func(c *ContractInfo) { c.Creator = make([]byte, sdk.AddrLen-1) }, + expError: true, + }, + "admin empty": { + srcMutator: func(c *ContractInfo) { c.Admin = nil }, + expError: false, + }, + "admin not an address": { + srcMutator: func(c *ContractInfo) { c.Admin = make([]byte, sdk.AddrLen-1) }, + expError: true, + }, + "label empty": { + srcMutator: func(c *ContractInfo) { c.Label = "" }, + expError: true, + }, + "label exceeds limit": { + srcMutator: func(c *ContractInfo) { c.Label = strings.Repeat("a", MaxLabelSize+1) }, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := ContractInfoFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} + +func TestCodeInfoValidateBasic(t *testing.T) { + specs := map[string]struct { + srcMutator func(*CodeInfo) + expError bool + }{ + "all good": {srcMutator: func(_ *CodeInfo) {}}, + "code hash empty": { + srcMutator: func(c *CodeInfo) { c.CodeHash = []byte{} }, + expError: true, + }, + "code hash nil": { + srcMutator: func(c *CodeInfo) { c.CodeHash = nil }, + expError: true, + }, + "creator empty": { + srcMutator: func(c *CodeInfo) { c.Creator = nil }, + expError: true, + }, + "creator not an address": { + srcMutator: func(c *CodeInfo) { c.Creator = make([]byte, sdk.AddrLen-1) }, + expError: true, + }, + "source empty": { + srcMutator: func(c *CodeInfo) { c.Source = "" }, + }, + "source not an url": { + srcMutator: func(c *CodeInfo) { c.Source = "invalid" }, + expError: true, + }, + "source not an absolute url": { + srcMutator: func(c *CodeInfo) { c.Source = "../bar.txt" }, + expError: true, + }, + "source not https schema url": { + srcMutator: func(c *CodeInfo) { c.Source = "http://example.com" }, + expError: true, + }, + "builder tag exceeds limit": { + srcMutator: func(c *CodeInfo) { c.Builder = strings.Repeat("a", MaxBuildTagSize+1) }, + expError: true, + }, + "builder tag does not match pattern": { + srcMutator: func(c *CodeInfo) { c.Builder = "invalid" }, + expError: true, + }, + "Instantiate config invalid": { + srcMutator: func(c *CodeInfo) { c.InstantiateConfig = AccessConfig{} }, + expError: true, + }, + } + for msg, spec := range specs { + t.Run(msg, func(t *testing.T) { + state := CodeInfoFixture(spec.srcMutator) + got := state.ValidateBasic() + if spec.expError { + require.Error(t, got) + return + } + require.NoError(t, got) + }) + } +} diff --git a/x/wasm/internal/types/validation.go b/x/wasm/internal/types/validation.go new file mode 100644 index 0000000000..23c59661b1 --- /dev/null +++ b/x/wasm/internal/types/validation.go @@ -0,0 +1,80 @@ +package types + +import ( + "net/url" + "regexp" + + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +const ( + MaxWasmSize = 500 * 1024 + + // MaxLabelSize is the longest label that can be used when Instantiating a contract + MaxLabelSize = 128 + + // BuildTagRegexp is a docker image regexp. + // We only support max 128 characters, with at least one organization name (subset of all legal names). + // + // Details from https://docs.docker.com/engine/reference/commandline/tag/#extended-description : + // + // An image name is made up of slash-separated name components (optionally prefixed by a registry hostname). + // Name components may contain lowercase characters, digits and separators. + // A separator is defined as a period, one or two underscores, or one or more dashes. A name component may not start or end with a separator. + // + // A tag name must be valid ASCII and may contain lowercase and uppercase letters, digits, underscores, periods and dashes. + // A tag name may not start with a period or a dash and may contain a maximum of 128 characters. + BuildTagRegexp = "^[a-z0-9][a-z0-9._-]*[a-z0-9](/[a-z0-9][a-z0-9._-]*[a-z0-9])+:[a-zA-Z0-9_][a-zA-Z0-9_.-]*$" + + MaxBuildTagSize = 128 +) + +func validateSourceURL(source string) error { + if source != "" { + u, err := url.Parse(source) + if err != nil { + return sdkerrors.Wrap(ErrInvalid, "not an url") + } + if !u.IsAbs() { + return sdkerrors.Wrap(ErrInvalid, "not an absolute url") + } + if u.Scheme != "https" { + return sdkerrors.Wrap(ErrInvalid, "must use https") + } + } + return nil +} + +func validateBuilder(buildTag string) error { + if len(buildTag) > MaxBuildTagSize { + return sdkerrors.Wrap(ErrLimit, "longer than 128 characters") + } + + if buildTag != "" { + ok, err := regexp.MatchString(BuildTagRegexp, buildTag) + if err != nil || !ok { + return ErrInvalid + } + } + return nil +} + +func validateWasmCode(s []byte) error { + if len(s) == 0 { + return sdkerrors.Wrap(ErrEmpty, "is required") + } + if len(s) > MaxWasmSize { + return sdkerrors.Wrapf(ErrLimit, "cannot be longer than %d bytes", MaxWasmSize) + } + return nil +} + +func validateLabel(label string) error { + if label == "" { + return sdkerrors.Wrap(ErrEmpty, "is required") + } + if len(label) > MaxLabelSize { + return sdkerrors.Wrap(ErrLimit, "cannot be longer than 128 characters") + } + return nil +} diff --git a/x/wasm/linkwasmd/Makefile b/x/wasm/linkwasmd/Makefile new file mode 100644 index 0000000000..748a235eb6 --- /dev/null +++ b/x/wasm/linkwasmd/Makefile @@ -0,0 +1,84 @@ +######################################## +### Process build tags + +ifeq ($(WITH_CLEVELDB),yes) + CGO_ENABLED=1 + build_tags += cleveldb +endif +build_tags += $(BUILD_TAGS) +build_tags := $(strip $(build_tags)) + +whitespace := +whitespace += $(whitespace) +comma := , +build_tags_comma_sep := $(subst $(whitespace),$(comma),$(build_tags)) +src := $(wildcard app/*.go) $(wildcard cmd/*.go) $(wildcard types/*.go) + +######################################## +### Process linker flags + +ldflags = -X github.com/cosmos/cosmos-sdk/version.Name=link \ + -X github.com/cosmos/cosmos-sdk/version.ServerName=linkd \ + -X github.com/cosmos/cosmos-sdk/version.ClientName=linkcli \ + -X github.com/cosmos/cosmos-sdk/version.Version=$(VERSION) \ + -X github.com/cosmos/cosmos-sdk/version.Commit=$(COMMIT) \ + -X "github.com/cosmos/cosmos-sdk/version.BuildTags=$(build_tags_comma_sep)" + +ifeq ($(WITH_CLEVELDB),yes) + ldflags += -X github.com/cosmos/cosmos-sdk/types.DBBackend=cleveldb +endif +ldflags += $(LDFLAGS) +ldflags := $(strip $(ldflags)) + +BUILD_FLAGS := -tags "$(build_tags)" -ldflags '$(ldflags)' +CLI_TEST_BUILD_FLAGS := -tags "cli_test $(build_tags)" + +######################################## +### Lint + +lint: $(src) + find . -name '*.go' -type f -not -path "*.git*" | xargs gofmt -d -s + go mod verify + +######################################## +### Build + +all: build install lint + +build: go.sum $(src) + CGO_ENABLED=$(CGO_ENABLED) go build -mod=readonly $(BUILD_FLAGS) -o build/linkwasmd ./cmd/linkwasmd + CGO_ENABLED=$(CGO_ENABLED) go build -mod=readonly $(BUILD_FLAGS) -o build/linkwasmcli ./cmd/linkwasmcli + +install: go.sum $(src) + CGO_ENABLED=$(CGO_ENABLED) go install $(BUILD_FLAGS) ./cmd/linkwasmd + CGO_ENABLED=$(CGO_ENABLED) go install $(BUILD_FLAGS) ./cmd/linkwasmcli + +clean: + rm -rf build/ + +######################################## +### Tools & dependencies + +go-mod-cache: go.sum + @echo "--> Download go modules to local cache" + @go mod download + +go.sum: go.mod + @echo "--> Ensure dependencies have not been modified" + @go mod verify + + +######################################## +### Testing + +test: test-all + +test-all: test-unit-all test-integration + +test-unit-all: test-unit + +test-unit: $(src) + @go test -mod=readonly -p 4 ./... + +test-integration: build + @go test -mod=readonly -p 4 `go list ./cli_test/...` $(CLI_TEST_BUILD_FLAGS) -v diff --git a/x/wasm/linkwasmd/README.md b/x/wasm/linkwasmd/README.md new file mode 100644 index 0000000000..13a0fac95b --- /dev/null +++ b/x/wasm/linkwasmd/README.md @@ -0,0 +1,3 @@ +# linkwasmd + +`linkwasmd` and `linkwasmcli` were forked from `line/link`'s `linkd` and `linkcli` and `wasm` sub-commands is added to it. diff --git a/x/wasm/linkwasmd/app/app.go b/x/wasm/linkwasmd/app/app.go new file mode 100644 index 0000000000..f29671e3a9 --- /dev/null +++ b/x/wasm/linkwasmd/app/app.go @@ -0,0 +1,360 @@ +package app + +import ( + "io" + "os" + "path/filepath" + + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/line/lbm-sdk/v2/x/account" + "github.com/line/lbm-sdk/v2/x/coin" + "github.com/line/lbm-sdk/v2/x/contract" + "github.com/line/lbm-sdk/v2/x/token" + "github.com/line/lbm-sdk/v2/x/wasm" + wasmclient "github.com/line/lbm-sdk/v2/x/wasm/client" + "github.com/spf13/viper" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/cli" + "github.com/tendermint/tendermint/libs/log" + tmos "github.com/tendermint/tendermint/libs/os" + dbm "github.com/tendermint/tm-db" + + bam "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/genutil" + "github.com/cosmos/cosmos-sdk/x/params" + paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/supply" + + "github.com/cosmos/cosmos-sdk/version" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/line/lbm-sdk/v2/x/collection" +) + +const appName = "LinkWasmApp" + +var ( + // DefaultCLIHome for linkcli + DefaultCLIHome = os.ExpandEnv("$HOME/.linkwasmcli") + + // DefaultNodeHome for linkd + DefaultNodeHome = os.ExpandEnv("$HOME/.linkwasmd") + + // ModuleBasics is in charge of setting up basic, + // non-dependant module elements, such as codec registration + // and genesis verification. + ModuleBasics = module.NewBasicManager( + genutil.AppModuleBasic{}, + auth.AppModuleBasic{}, + coin.AppModuleBasic{}, + staking.AppModuleBasic{}, + params.AppModuleBasic{}, + supply.AppModuleBasic{}, + gov.NewAppModuleBasic( + append( + wasmclient.ProposalHandlers, + paramsclient.ProposalHandler, + )..., + ), + token.AppModuleBasic{}, + collection.AppModuleBasic{}, + account.AppModuleBasic{}, + wasm.AppModuleBasic{}, + ) + + // module account permissions + maccPerms = map[string][]string{ + auth.FeeCollectorName: nil, + staking.BondedPoolName: {supply.Burner, supply.Staking}, + staking.NotBondedPoolName: {supply.Burner, supply.Staking}, + token.ModuleName: {supply.Minter, supply.Burner}, + collection.ModuleName: {supply.Minter, supply.Burner}, + gov.ModuleName: {supply.Burner}, + } +) + +// custom tx codec +func MakeCodec() *codec.Codec { + var cdc = codec.New() + + ModuleBasics.RegisterCodec(cdc) + vesting.RegisterCodec(cdc) + sdk.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + codec.RegisterEvidences(cdc) + + return cdc +} + +// Extended ABCI application +type LinkApp struct { + *bam.BaseApp + cdc *codec.Codec + + // keys to access the substores + keys map[string]*sdk.KVStoreKey + tkeys map[string]*sdk.TransientStoreKey + + // subspaces + subspaces map[string]params.Subspace + + // keepers + accountKeeper auth.AccountKeeper + bankKeeper bank.Keeper + coinKeeper coin.Keeper + supplyKeeper supply.Keeper + stakingKeeper staking.Keeper + paramsKeeper params.Keeper + govKeeper gov.Keeper + tokenKeeper token.Keeper + collectionKeeper collection.Keeper + wasmKeeper wasm.Keeper + + // the module manager + mm *module.Manager + + // simulation manager + sm *module.SimulationManager +} + +type WasmWrapper struct { + Wasm wasm.Config `mapstructure:"wasm"` +} + +// NewLinkApp returns a reference to an initialized LinkApp. +func NewLinkApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, skipUpgradeHeights map[int64]bool, + invCheckPeriod uint, baseAppOptions ...func(*bam.BaseApp)) *LinkApp { + cdc := MakeCodec() + + bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc), baseAppOptions...) + bApp.SetCommitMultiStoreTracer(traceStore) + bApp.SetAppVersion(version.Version) + + keys := sdk.NewKVStoreKeys( + bam.MainStoreKey, + auth.StoreKey, + staking.StoreKey, + supply.StoreKey, + params.StoreKey, + gov.StoreKey, + token.StoreKey, + collection.StoreKey, + coin.StoreKey, + contract.StoreKey, + wasm.StoreKey, + ) + tkeys := sdk.NewTransientStoreKeys(staking.TStoreKey, params.TStoreKey) + + app := &LinkApp{ + BaseApp: bApp, + cdc: cdc, + keys: keys, + tkeys: tkeys, + subspaces: make(map[string]params.Subspace), + } + + // init params keeper and subspaces + app.paramsKeeper = params.NewKeeper(app.cdc, keys[params.StoreKey], tkeys[params.TStoreKey]) + app.subspaces[auth.ModuleName] = app.paramsKeeper.Subspace(auth.DefaultParamspace) + app.subspaces[bank.ModuleName] = app.paramsKeeper.Subspace(bank.DefaultParamspace) + app.subspaces[staking.ModuleName] = app.paramsKeeper.Subspace(staking.DefaultParamspace) + app.subspaces[collection.ModuleName] = app.paramsKeeper.Subspace(collection.DefaultParamspace) + app.subspaces[gov.ModuleName] = app.paramsKeeper.Subspace(gov.DefaultParamspace).WithKeyTable(gov.ParamKeyTable()) + app.subspaces[wasm.ModuleName] = app.paramsKeeper.Subspace(wasm.DefaultParamspace) + + // add keepers + app.accountKeeper = auth.NewAccountKeeper(app.cdc, keys[auth.StoreKey], app.subspaces[auth.ModuleName], auth.ProtoBaseAccount) + app.bankKeeper = bank.NewBaseKeeper(app.accountKeeper, app.subspaces[bank.ModuleName], app.ModuleAccountAddrs()) + app.coinKeeper = coin.NewKeeper(app.bankKeeper, keys[coin.StoreKey]) + app.supplyKeeper = supply.NewKeeper(app.cdc, keys[supply.StoreKey], app.accountKeeper, app.bankKeeper, maccPerms) + app.stakingKeeper = staking.NewKeeper(app.cdc, keys[staking.StoreKey], app.supplyKeeper, app.subspaces[staking.ModuleName]) + + contractKeeper := contract.NewContractKeeper(cdc, keys[contract.StoreKey]) + app.tokenKeeper = token.NewKeeper(app.cdc, app.accountKeeper, contractKeeper, keys[token.StoreKey]) + app.collectionKeeper = collection.NewKeeper( + app.cdc, + app.accountKeeper, + contractKeeper, + app.subspaces[collection.ModuleName], + keys[collection.StoreKey], + ) + + // just re-use the full router - do we want to limit this more? + var wasmRouter = bApp.Router() + + // encodeRouter + tokenEncodeHandler := token.NewMsgEncodeHandler(app.tokenKeeper) + collectionEncoder := collection.NewMsgEncodeHandler(app.collectionKeeper) + var encodeRouter = wasm.NewRouter() + encodeRouter.AddRoute(token.EncodeRouterKey, tokenEncodeHandler) + encodeRouter.AddRoute(collection.EncodeRouterKey, collectionEncoder) + + // queryRouter + tokenQuerier := token.NewQuerier(app.tokenKeeper) + tokenQueryEncoder := token.NewQueryEncoder(tokenQuerier) + collectionQuerier := collection.NewQuerier(app.collectionKeeper) + collectionQueryEncoder := collection.NewQueryEncoder(collectionQuerier) + var querierRouter = wasm.NewQuerierRouter() + querierRouter.AddRoute(token.EncodeRouterKey, tokenQueryEncoder) + querierRouter.AddRoute(collection.EncodeRouterKey, collectionQueryEncoder) + + // better way to get this dir??? + homeDir := viper.GetString(cli.HomeFlag) + wasmDir := filepath.Join(homeDir, "wasm") + + wasmWrap := WasmWrapper{Wasm: wasm.DefaultWasmConfig()} + err := viper.Unmarshal(&wasmWrap) + if err != nil { + panic("error while reading wasm config: " + err.Error()) + } + wasmConfig := wasmWrap.Wasm + supportedFeatures := "staking,link" + + app.wasmKeeper = wasm.NewKeeper( + app.cdc, + keys[wasm.StoreKey], + app.subspaces[wasm.ModuleName], + app.accountKeeper, + app.coinKeeper, + app.stakingKeeper, + distr.Keeper{}, + wasmRouter, + encodeRouter, + querierRouter, + wasmDir, + wasmConfig, + supportedFeatures, + nil, + nil, + ) + + // register the proposal types + govRouter := gov.NewRouter() + govRouter.AddRoute(gov.RouterKey, gov.ProposalHandler). + AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). + AddRoute(wasm.RouterKey, wasm.NewWasmProposalHandler(app.wasmKeeper, wasm.EnableAllProposals)) + app.govKeeper = gov.NewKeeper( + app.cdc, keys[gov.StoreKey], app.subspaces[gov.ModuleName], app.supplyKeeper, + &app.stakingKeeper, govRouter, + ) + + // NOTE: Any module instantiated in the module manager that is later modified + // must be passed by reference here. + app.mm = module.NewManager( + genutil.NewAppModule(app.accountKeeper, app.stakingKeeper, app.BaseApp.DeliverTx), + auth.NewAppModule(app.accountKeeper), + coin.NewAppModule(app.coinKeeper), + supply.NewAppModule(app.supplyKeeper, app.accountKeeper), + staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), + gov.NewAppModule(app.govKeeper, app.accountKeeper, app.supplyKeeper), + token.NewAppModule(app.tokenKeeper), + collection.NewAppModule(app.collectionKeeper), + account.NewAppModule(app.accountKeeper), + wasm.NewAppModule(app.wasmKeeper), + ) + app.mm.SetOrderEndBlockers(gov.ModuleName, staking.ModuleName) + + // NOTE: The genutils module must occur after staking so that pools are + // properly initialized with tokens from genesis accounts. + app.mm.SetOrderInitGenesis( + staking.ModuleName, + auth.ModuleName, + gov.ModuleName, + supply.ModuleName, + coin.ModuleName, + genutil.ModuleName, + token.ModuleName, + collection.ModuleName, + account.ModuleName, + wasm.ModuleName, + ) + + app.mm.RegisterRoutes(app.Router(), app.QueryRouter()) + + // create the simulation manager and define the order of the modules for deterministic simulations + // + // NOTE: This is not required for apps that don't use the simulator for fuzz testing + // transactions. + app.sm = module.NewSimulationManager( + auth.NewAppModule(app.accountKeeper), + supply.NewAppModule(app.supplyKeeper, app.accountKeeper), + staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper), + collection.NewAppModule(app.collectionKeeper), + gov.NewAppModule(app.govKeeper, app.accountKeeper, app.supplyKeeper), + // TODO: Implement AppModuleSimulation interface in each module. + // bank.NewAppModule(app.coinKeeper), + // token.NewAppModule(app.tokenKeeper), + // account.NewAppModule(app.accountKeeper), + ) + + app.sm.RegisterStoreDecoders() + + // initialize stores + app.MountKVStores(keys) + app.MountTransientStores(tkeys) + + // initialize BaseApp + app.SetInitChainer(app.InitChainer) + app.SetBeginBlocker(app.BeginBlocker) + app.SetAnteHandler(auth.NewAnteHandler(app.accountKeeper, app.supplyKeeper, auth.DefaultSigVerificationGasConsumer)) + app.SetEndBlocker(app.EndBlocker) + + if loadLatest { + err := app.LoadLatestVersion(app.keys[bam.MainStoreKey]) + if err != nil { + tmos.Exit(err.Error()) + } + } + + return app +} + +// application updates every begin block +func (app *LinkApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + return app.mm.BeginBlock(ctx, req) +} + +// application updates every end block +func (app *LinkApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { + return app.mm.EndBlock(ctx, req) +} + +// application update at chain initialization +func (app *LinkApp) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + var genesisState simapp.GenesisState + app.cdc.MustUnmarshalJSON(req.AppStateBytes, &genesisState) + + return app.mm.InitGenesis(ctx, genesisState) +} + +// load a particular height +func (app *LinkApp) LoadHeight(height int64) error { + return app.LoadVersion(height, app.keys[bam.MainStoreKey]) +} + +// ModuleAccountAddrs returns all the app's module account addresses. +func (app *LinkApp) ModuleAccountAddrs() map[string]bool { + modAccAddrs := make(map[string]bool) + for acc := range maccPerms { + modAccAddrs[supply.NewModuleAddress(acc).String()] = true + } + + return modAccAddrs +} + +// Codec returns the application's sealed codec. +func (app *LinkApp) Codec() *codec.Codec { + return app.cdc +} + +// SimulationManager implements the SimulationApp interface +func (app *LinkApp) SimulationManager() *module.SimulationManager { + return app.sm +} diff --git a/x/wasm/linkwasmd/app/app_test.go b/x/wasm/linkwasmd/app/app_test.go new file mode 100644 index 0000000000..9a6cce1e7a --- /dev/null +++ b/x/wasm/linkwasmd/app/app_test.go @@ -0,0 +1,53 @@ +package app + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/log" + db "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/codec" + abci "github.com/tendermint/tendermint/abci/types" +) + +func TestLinkdExport(t *testing.T) { + db := db.NewMemDB() + gapp := NewLinkApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, 0) + err := setGenesis(gapp) + require.NoError(t, err) + + // Making a new app object with the db, so that initchain hasn't been called + newGapp := NewLinkApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, 0) + _, _, err = newGapp.ExportAppStateAndValidators(false, []string{}) + require.NoError(t, err, "ExportAppStateAndValidators should not have an error") +} + +// ensure that black listed addresses are properly set in bank keeper +func TestBlacklistedAddrs(t *testing.T) { + db := db.NewMemDB() + app := NewLinkApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, 0) + + for acc := range maccPerms { + require.True(t, app.bankKeeper.BlacklistedAddr(app.supplyKeeper.GetModuleAddress(acc))) + } +} + +func setGenesis(gapp *LinkApp) error { + genesisState := NewDefaultGenesisState() + stateBytes, err := codec.MarshalJSONIndent(gapp.cdc, genesisState) + if err != nil { + return err + } + + // Initialize the chain + gapp.InitChain( + abci.RequestInitChain{ + Validators: []abci.ValidatorUpdate{}, + AppStateBytes: stateBytes, + }, + ) + gapp.Commit() + return nil +} diff --git a/x/wasm/linkwasmd/app/export.go b/x/wasm/linkwasmd/app/export.go new file mode 100644 index 0000000000..83e1b0c2e7 --- /dev/null +++ b/x/wasm/linkwasmd/app/export.go @@ -0,0 +1,107 @@ +package app + +import ( + "encoding/json" + "log" + + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" +) + +// export the state of link for a genesis file +func (app *LinkApp) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteList []string, +) (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { + // as if they could withdraw from the start of the next block + ctx := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) + + if forZeroHeight { + app.prepForZeroHeightGenesis(ctx, jailWhiteList) + } + + genState := app.mm.ExportGenesis(ctx) + appState, err = codec.MarshalJSONIndent(app.cdc, genState) + if err != nil { + return nil, nil, err + } + validators = staking.WriteValidators(ctx, app.stakingKeeper) + return appState, validators, nil +} + +// prepare for fresh start at zero height +// NOTE zero height genesis is a temporary feature which will be deprecated +// in flavor of export at a block height +func (app *LinkApp) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string) { + applyWhiteList := false + + // Check if there is a whitelist + if len(jailWhiteList) > 0 { + applyWhiteList = true + } + + whiteListMap := make(map[string]bool) + + for _, addr := range jailWhiteList { + _, err := sdk.ValAddressFromBech32(addr) + if err != nil { + log.Fatal(err) + } + whiteListMap[addr] = true + } + + // set context height to zero + height := ctx.BlockHeight() + ctx = ctx.WithBlockHeight(0) + + // reset context height + ctx = ctx.WithBlockHeight(height) + + /* Handle staking state. */ + + // iterate through redelegations, reset creation height + app.stakingKeeper.IterateRedelegations(ctx, func(_ int64, red staking.Redelegation) (stop bool) { + for i := range red.Entries { + red.Entries[i].CreationHeight = 0 + } + app.stakingKeeper.SetRedelegation(ctx, red) + return false + }) + + // iterate through unbonding delegations, reset creation height + app.stakingKeeper.IterateUnbondingDelegations(ctx, func(_ int64, ubd staking.UnbondingDelegation) (stop bool) { + for i := range ubd.Entries { + ubd.Entries[i].CreationHeight = 0 + } + app.stakingKeeper.SetUnbondingDelegation(ctx, ubd) + return false + }) + + // Iterate through validators by power descending, reset bond heights, and + // update bond intra-tx counters. + store := ctx.KVStore(app.keys[staking.StoreKey]) + iter := sdk.KVStoreReversePrefixIterator(store, staking.ValidatorsKey) + counter := int16(0) + + for ; iter.Valid(); iter.Next() { + addr := sdk.ValAddress(iter.Key()[1:]) + validator, found := app.stakingKeeper.GetValidator(ctx, addr) + if !found { + panic("expected validator, not found") + } + + validator.UnbondingHeight = 0 + if applyWhiteList && !whiteListMap[addr.String()] { + validator.Jailed = true + } + + app.stakingKeeper.SetValidator(ctx, validator) + counter++ + } + + iter.Close() + + _ = app.stakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx) +} diff --git a/x/wasm/linkwasmd/app/genesis.go b/x/wasm/linkwasmd/app/genesis.go new file mode 100644 index 0000000000..97258ac992 --- /dev/null +++ b/x/wasm/linkwasmd/app/genesis.go @@ -0,0 +1,11 @@ +package app + +import "encoding/json" + +// GenesisState defines a type alias for the Gaia genesis application state. +type GenesisState map[string]json.RawMessage + +// NewDefaultGenesisState generates the default state for the application. +func NewDefaultGenesisState() GenesisState { + return ModuleBasics.DefaultGenesis() +} diff --git a/x/wasm/linkwasmd/app/params.go b/x/wasm/linkwasmd/app/params.go new file mode 100644 index 0000000000..96b7729760 --- /dev/null +++ b/x/wasm/linkwasmd/app/params.go @@ -0,0 +1,22 @@ +package app + +// Simulation parameter constants +const ( + StakePerAccount = "stake_per_account" + InitiallyBondedValidators = "initially_bonded_validators" + OpWeightMsgSend = "op_weight_msg_send" + OpWeightSingleInputMsgMultiSend = "op_weight_single_input_msg_multisend" + OpWeightMsgSetWithdrawAddress = "op_weight_msg_set_withdraw_address" + OpWeightMsgWithdrawDelegationReward = "op_weight_msg_withdraw_delegation_reward" + OpWeightMsgWithdrawValidatorCommission = "op_weight_msg_withdraw_validator_commission" + OpWeightSubmitVotingSlashingTextProposal = "op_weight_submit_voting_slashing_text_proposal" + OpWeightSubmitVotingSlashingCommunitySpendProposal = "op_weight_submit_voting_slashing_community_spend_proposal" + OpWeightSubmitVotingSlashingParamChangeProposal = "op_weight_submit_voting_slashing_param_change_proposal" + OpWeightMsgDeposit = "op_weight_msg_deposit" + OpWeightMsgCreateValidator = "op_weight_msg_create_validator" + OpWeightMsgEditValidator = "op_weight_msg_edit_validator" + OpWeightMsgDelegate = "op_weight_msg_delegate" + OpWeightMsgUndelegate = "op_weight_msg_undelegate" + OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate" + OpWeightMsgUnjail = "op_weight_msg_unjail" +) diff --git a/x/wasm/linkwasmd/app/sim_test.go b/x/wasm/linkwasmd/app/sim_test.go new file mode 100644 index 0000000000..b09469347b --- /dev/null +++ b/x/wasm/linkwasmd/app/sim_test.go @@ -0,0 +1,299 @@ +package app + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "math" + "math/big" + "os" + "testing" + + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/simapp/helpers" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/mint" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/cosmos/cosmos-sdk/x/slashing" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/supply" +) + +func init() { + simapp.GetSimulatorFlags() +} + +type StoreKeysPrefixes struct { + A sdk.StoreKey + B sdk.StoreKey + Prefixes [][]byte +} + +// fauxMerkleModeOpt returns a BaseApp option to use a dbStoreAdapter instead of +// an IAVLStore for faster simulation speed. +func fauxMerkleModeOpt(bapp *baseapp.BaseApp) { + bapp.SetFauxMerkleMode() +} + +// interBlockCacheOpt returns a BaseApp option function that sets the persistent +// inter-block write-through cache. +func interBlockCacheOpt() func(*baseapp.BaseApp) { + return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager()) +} + +func TestFullAppSimulation(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping application simulation") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewLinkApp(logger, db, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(app, app.Codec(), config), + app.ModuleAccountAddrs(), config, + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simapp.PrintStats(db) + } +} + +func TestAppImportExport(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping application import/export simulation") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewLinkApp(logger, db, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + // Run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(app, app.Codec(), config), + app.ModuleAccountAddrs(), config, + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simapp.PrintStats(db) + } + + fmt.Printf("exporting genesis...\n") + + appState, _, err := app.ExportAppStateAndValidators(false, []string{}) + require.NoError(t, err) + + fmt.Printf("importing genesis...\n") + + _, newDB, newDir, _, _, err := simapp.SetupSimulation("leveldb-app-sim-2", "Simulation-2") + require.NoError(t, err, "simulation setup failed") + + defer func() { + newDB.Close() + require.NoError(t, os.RemoveAll(newDir)) + }() + + newApp := NewLinkApp(log.NewNopLogger(), newDB, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, newApp.Name()) + + var genesisState GenesisState + err = app.Codec().UnmarshalJSON(appState, &genesisState) + require.NoError(t, err) + + ctxA := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) + ctxB := newApp.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) + newApp.mm.InitGenesis(ctxB, genesisState) + + fmt.Printf("comparing stores...\n") + + storeKeysPrefixes := []StoreKeysPrefixes{ + {app.keys[baseapp.MainStoreKey], newApp.keys[baseapp.MainStoreKey], [][]byte{}}, + {app.keys[auth.StoreKey], newApp.keys[auth.StoreKey], [][]byte{}}, + {app.keys[staking.StoreKey], newApp.keys[staking.StoreKey], + [][]byte{ + staking.UnbondingQueueKey, staking.RedelegationQueueKey, staking.ValidatorQueueKey, + }}, // ordering may change but it doesn't matter + {app.keys[slashing.StoreKey], newApp.keys[slashing.StoreKey], [][]byte{}}, + {app.keys[mint.StoreKey], newApp.keys[mint.StoreKey], [][]byte{}}, + {app.keys[distr.StoreKey], newApp.keys[distr.StoreKey], [][]byte{}}, + {app.keys[supply.StoreKey], newApp.keys[supply.StoreKey], [][]byte{}}, + {app.keys[params.StoreKey], newApp.keys[params.StoreKey], [][]byte{}}, + {app.keys[gov.StoreKey], newApp.keys[gov.StoreKey], [][]byte{}}, + } + + for _, skp := range storeKeysPrefixes { + storeA := ctxA.KVStore(skp.A) + storeB := ctxB.KVStore(skp.B) + + failedKVAs, failedKVBs := sdk.DiffKVStores(storeA, storeB, skp.Prefixes) + require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare") + + fmt.Printf("compared %d key/value pairs between %s and %s\n", len(failedKVAs), skp.A, skp.B) + require.Equal(t, len(failedKVAs), 0, simapp.GetSimulationLog(skp.A.Name(), app.SimulationManager().StoreDecoders, app.Codec(), failedKVAs, failedKVBs)) + } +} + +func TestAppSimulationAfterImport(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping application simulation after import") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewLinkApp(logger, db, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + // Run randomized simulation + stopEarly, simParams, simErr := simulation.SimulateFromSeed( + t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(app, app.Codec(), config), + app.ModuleAccountAddrs(), config, + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simapp.PrintStats(db) + } + + if stopEarly { + fmt.Println("can't export or import a zero-validator genesis, exiting test...") + return + } + + fmt.Printf("exporting genesis...\n") + + appState, _, err := app.ExportAppStateAndValidators(true, []string{}) + require.NoError(t, err) + + fmt.Printf("importing genesis...\n") + + _, newDB, newDir, _, _, err := simapp.SetupSimulation("leveldb-app-sim-2", "Simulation-2") + require.NoError(t, err, "simulation setup failed") + + defer func() { + newDB.Close() + require.NoError(t, os.RemoveAll(newDir)) + }() + + newApp := NewLinkApp(log.NewNopLogger(), newDB, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, newApp.Name()) + + newApp.InitChain(abci.RequestInitChain{ + AppStateBytes: appState, + }) + + _, _, err = simulation.SimulateFromSeed( + t, os.Stdout, newApp.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(newApp, newApp.Codec(), config), + newApp.ModuleAccountAddrs(), config, + ) + require.NoError(t, err) +} + +func TestAppStateDeterminism(t *testing.T) { + if !simapp.FlagEnabledValue { + t.Skip("skipping application simulation") + } + + config := simapp.NewConfigFromFlags() + config.InitialBlockHeight = 1 + config.ExportParamsPath = "" + config.OnOperation = false + config.AllInvariants = false + config.ChainID = helpers.SimAppChainID + + numSeeds := 2 + numTimesToRunPerSeed := 2 + appHashList := make([]json.RawMessage, numTimesToRunPerSeed) + + for i := 0; i < numSeeds; i++ { + n, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + require.NoError(t, err) + config.Seed = n.Int64() + + for j := 0; j < numTimesToRunPerSeed; j++ { + var logger log.Logger + if simapp.FlagVerboseValue { + logger = log.TestingLogger() + } else { + logger = log.NewNopLogger() + } + + db := dbm.NewMemDB() + + app := NewLinkApp(logger, db, nil, true, map[int64]bool{}, simapp.FlagPeriodValue, interBlockCacheOpt()) + + fmt.Printf( + "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n", + config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + + _, _, err := simulation.SimulateFromSeed( + t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(app, app.Codec(), config), + app.ModuleAccountAddrs(), config, + ) + require.NoError(t, err) + + if config.Commit { + simapp.PrintStats(db) + } + + appHash := app.LastCommitID().Hash + appHashList[j] = appHash + + if j != 0 { + require.Equal( + t, appHashList[0], appHashList[j], + "non-determinism in seed %d: %d/%d, attempt: %d/%d\n", config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + } + } + } +} diff --git a/x/wasm/linkwasmd/app/utils.go b/x/wasm/linkwasmd/app/utils.go new file mode 100644 index 0000000000..8bcd604eb3 --- /dev/null +++ b/x/wasm/linkwasmd/app/utils.go @@ -0,0 +1,46 @@ +package app + +import ( + "io" + + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" +) + +// nolint +var ( + genesisFile string + paramsFile string + exportParamsPath string + exportParamsHeight int + exportStatePath string + exportStatsPath string + seed int64 + initialBlockHeight int + numBlocks int + blockSize int + enabled bool + verbose bool + lean bool + commit bool + period int + onOperation bool // TODO Remove in favor of binary search for invariant violation + allInvariants bool + genesisTime int64 +) + +// DONTCOVER + +// NewLinkAppUNSAFE is used for debugging purposes only. +// +// NOTE: to not use this function with non-test code +func NewLinkAppUNSAFE(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, + invCheckPeriod uint, baseAppOptions ...func(*baseapp.BaseApp), +) (gapp *LinkApp, keyMain, keyStaking *sdk.KVStoreKey, stakingKeeper staking.Keeper) { + gapp = NewLinkApp(logger, db, traceStore, loadLatest, map[int64]bool{}, invCheckPeriod, baseAppOptions...) + return gapp, gapp.keys[baseapp.MainStoreKey], gapp.keys[staking.StoreKey], gapp.stakingKeeper +} diff --git a/x/wasm/linkwasmd/cli_test/README.md b/x/wasm/linkwasmd/cli_test/README.md new file mode 100644 index 0000000000..3c87497954 --- /dev/null +++ b/x/wasm/linkwasmd/cli_test/README.md @@ -0,0 +1,61 @@ +# Link CLI Integration tests + +The link cli integration tests live in this folder. You can run the full suite by running: + +```bash +go test -mod=readonly -v -p 4 `go list ./cli_test/...` -tags=cli_test +``` + +> NOTE: While the full suite runs in parallel, some of the tests can take up to a minute to complete + +### Test Structure + +This integration suite [uses a thin wrapper](https://godoc.org/github.com/cosmos/cosmos-sdk/tests) over the [`os/exec`](https://golang.org/pkg/os/exec/) package. This allows the integration test to run against built binaries (both `linkd` and `linkcli` are used) while being written in golang. This allows tests to take advantage of the various golang code we have for operations like marshal/unmarshal, crypto, etc... + +> NOTE: The tests will use whatever `linkd` or `linkcli` binaries are available in your `$PATH`. You can check which binary will be run by the suite by running `which linkd` or `which linkcli`. If you have your `$GOPATH` properly setup they should be in `$GOPATH/bin/link*`. This will ensure that your test uses the latest binary you have built + +Tests generally follow this structure: + +```go +func TestMyNewCommand(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + defer f.Cleanup() + + // start linkd server + proc := f.GDStart() + defer func() { require.NoError(t, proc.Stop(false)) }() + + // Your test code goes here... +} +``` + +This boilerplate above: + +- Ensures the tests run in parallel. Because the tests are calling out to `os/exec` for many operations these tests can take a long time to run. +- Creates `.linkd` and `.linkcli` folders in a new temp folder. +- Uses `linkcli` to create 2 accounts for use in testing: `foo` and `bar` +- Creates a genesis file with coins (`1000footoken,1000feetoken,150stake`) controlled by the `foo` key +- Generates an initial bonding transaction (`gentx`) to make the `foo` key a validator at genesis +- Starts `linkd` and stops it once the test exits +- Cleans up test state on a successful run + +### Notes when adding/running tests + +- Because the tests run against a built binary, you should make sure you build every time the code changes and you want to test again, otherwise you will be testing against an older version. If you are adding new tests this can easily lead to confusing test results. +- The [`test_helpers.go`](./test_helpers.go) file is organized according to the format of `linkcli` and `linkd` commands. There are comments with section headers describing the different areas. Helper functions to call CLI functionality are generally named after the command (e.g. `linkcli query staking validator` would be `QueryStakingValidator`). Try to keep functions grouped by their position in the command tree. +- Test state that is needed by `tx` and `query` commands (`home`, `chain_id`, etc...) is stored on the `Fixtures` object. This makes constructing your new tests almost trivial. +- Sometimes if you exit a test early there can be still running `linkd` and `linkcli` processes that will interrupt subsequent runs. Still running `linkcli` processes will block access to the keybase while still running `linkd` processes will block ports and prevent new tests from spinning up. You can ensure new tests spin up clean by running `pkill -9 linkd && pkill -9 linkcli` before each test run. +- Most `query` and `tx` commands take a variadic `flags` argument. This pattern allows for the creation of a general function which is easily modified by adding flags. See the `TxSend` function and its use for a good example. +- `Tx*` functions follow a general pattern and return `(success bool, stdout string, stderr string)`. This allows for easy testing of multiple different flag configurations. See `TestLinkCLICreateValidator` or `TestLinkCLISubmitProposal` for a good example of the pattern. + +### Notes multi-nodes tests + +- To enable multi-nodes integration test, [Docker](https://www.docker.com) is required. + +- Test state for a network is stored on the `FixtureGroup` object. And the FixtureGroup consists of multiple `Fixtures` which is explained above + +- One test function has one docker network with predefined ip range subnet(ex: `192.168.0.0/24`). If you want to add a test function then, please make sure the subnet does not overlap others subnet + +- Sometimes if you exit a test early there can be still running docker container. But don't worry it will be stoped and replaced by new container which is generated for the test case when the test function executed. + diff --git a/x/wasm/linkwasmd/cli_test/cli_test.go b/x/wasm/linkwasmd/cli_test/cli_test.go new file mode 100644 index 0000000000..3dde693d8d --- /dev/null +++ b/x/wasm/linkwasmd/cli_test/cli_test.go @@ -0,0 +1,1800 @@ +// +build cli_test + +package clitest + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + "strconv" + "strings" + "testing" + + collectionModule "github.com/line/lbm-sdk/v2/x/collection" + tokenModule "github.com/line/lbm-sdk/v2/x/token" + "github.com/line/lbm-sdk/v2/x/wasm/linkwasmd/app" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/tests" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestLinkCLIWasmEscrow(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + defer f.Cleanup() + + // start linkd server + proc := f.LDStart() + defer func() { require.NoError(t, proc.Stop(false)) }() + + fooAddr := f.KeyAddress(keyFoo) + barAddr := f.KeyAddress(keyBar) + + flagFromFoo := fmt.Sprintf("--from=%s", fooAddr) + flagFromBar := fmt.Sprintf("--from=%s", barAddr) + flagGas := "--gas=auto --gas-adjustment=1.2" + workDir, _ := os.Getwd() + tmpDir := path.Join(workDir, "tmp-dir-for-test-escrow") + dirContract := path.Join(workDir, "contracts", "escrow") + hashFile := path.Join(dirContract, "hash.txt") + wasmEscrow := path.Join(dirContract, "contract.wasm") + codeId := uint64(1) + amountSend := uint64(10) + denomSend := fooDenom + approvalMsgJson := fmt.Sprintf("{\"approve\":{\"quantity\":[{\"amount\":\"%d\",\"denom\":\"%s\"}]}}", amountSend, denomSend) + var contractAddress sdk.AccAddress + + // make tmpDir + os.Mkdir(tmpDir, os.ModePerm) + + // get init amount + initAmountOfFoo := f.QueryAccount(fooAddr).Coins.AmountOf(denomSend).Uint64() + initAmountOfBar := uint64(0) + + // validate that there are no code in the chain + { + listCode := f.QueryListCodeWasm() + require.Empty(t, listCode) + } + + // store the contract escrow + { + f.LogResult(f.TxStoreWasm(wasmEscrow, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate the code is stored + { + listCode := f.QueryListCodeWasm() + require.Len(t, listCode, 1) + + //validate the hash is the same + expectedRow, _ := ioutil.ReadFile(hashFile) + expected, err := hex.DecodeString(string(expectedRow[:64])) + require.NoError(t, err) + actual := listCode[0].GetDataHash().Bytes() + require.Equal(t, expected, actual) + } + + // validate getCode get the exact same wasm + { + outputPath := path.Join(tmpDir, "escrow-tmp.wasm") + f.QueryCodeWasm(codeId, outputPath) + fLocal, _ := os.Open(wasmEscrow) + fChain, _ := os.Open(outputPath) + + // 2000000 is enough length + dataLocal := make([]byte, 2000000) + dataChain := make([]byte, 2000000) + fLocal.Read(dataLocal) + fChain.Read(dataChain) + require.Equal(t, dataLocal, dataChain) + } + + // validate that there are no contract using the code (id=1) + { + listContract := f.QueryListContractByCodeWasm(codeId) + require.Empty(t, listContract) + } + + // instantiate a contract with the code escrow + { + msgJson := fmt.Sprintf("{\"arbiter\":\"%s\",\"recipient\":\"%s\"}", fooAddr, barAddr) + flagLabel := "--label=escrow-test" + flagAmount := fmt.Sprintf("--amount=%d%s", amountSend, denomSend) + f.LogResult(f.TxInstantiateWasm(codeId, msgJson, flagLabel, flagAmount, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate foo's amount decreased + { + amount := f.QueryAccount(fooAddr).Coins.AmountOf(denomSend).Uint64() + require.Equal(t, initAmountOfFoo-amountSend, amount) + } + + // validate there is only one contract using codeId=1 and get contractAddress + { + listContract := f.QueryListContractByCodeWasm(codeId) + require.Len(t, listContract, 1) + contractAddress = listContract[0].GetAddress() + } + + // check arbiter with query + { + res := f.QueryContractStateSmartWasm(contractAddress, "{\"arbiter\":{}}") + require.Equal(t, fmt.Sprintf("{\"arbiter\":\"%s\"}", fooAddr), res) + } + + // validate executing approve is failed by invalid account + { + succeeded, _, _ := f.TxExecuteWasm(contractAddress, approvalMsgJson, flagFromBar, flagGas, "-y") + require.False(t, succeeded) + } + + // execute approve + { + f.LogResult(f.TxExecuteWasm(contractAddress, approvalMsgJson, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate the coin Foot is transfered + { + amount := f.QueryAccount(barAddr).Coins.AmountOf(denomSend).Uint64() + require.Equal(t, initAmountOfBar+amountSend, amount) + } + + // validate approve over amount does not succeed + { + succeeded, _, _ := f.TxExecuteWasm(contractAddress, approvalMsgJson, flagFromFoo, flagGas, "-y") + require.False(t, succeeded) + } + + // remove tmp dir + os.RemoveAll(tmpDir) +} + +func TestLinkCLIWasmTokenTester(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + defer f.Cleanup() + + // start linkd server + proc := f.LDStart() + defer func() { require.NoError(t, proc.Stop(false)) }() + + fooAddr := f.KeyAddress(keyFoo) + + flagFromFoo := fmt.Sprintf("--from=%s", fooAddr) + flagGas := "--gas=auto --gas-adjustment=1.2" + workDir, _ := os.Getwd() + wasmTokenTester := path.Join(workDir, "contracts", "token-tester", "contract.wasm") + codeId := uint64(1) + var contractAddress sdk.AccAddress + tokenContractId := "9be17165" + tokenName := "TestToken1" + tokenSymbol := "TT1" + tokenMeta := "meta" + tokenImageURI := "http://example.com/image" + tokenDecimals := sdk.NewInt(8) + + initAmount := 1 + mintAmount := 99 + transferAmount := 10 + burnAmount := 5 + mintFromFooAmount := 1 + burnFromFooAmount := 3 + + modifyKey := "meta" + modifyValue := "update_token_meta" + + // store the contract token-tester + { + f.LogResult(f.TxStoreWasm(wasmTokenTester, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // instantiate + { + msgJson := "{}" + flagLabel := "--label=token-tester" + f.LogResult(f.TxInstantiateWasm(codeId, msgJson, flagLabel, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate there is only one contract using codeId=1 and get contractAddress + { + listContract := f.QueryListContractByCodeWasm(codeId) + require.Len(t, listContract, 1) + contractAddress = listContract[0].GetAddress() + } + + // issue token + { + msg := map[string]map[string]interface{}{ + "issue": { + "owner": contractAddress, + "to": contractAddress, + "name": tokenName, + "symbol": tokenSymbol, + "meta": tokenMeta, + "img_uri": tokenImageURI, + "amount": strconv.Itoa(initAmount), + "mintable": true, + "decimals": tokenDecimals, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate that token is issued + { + token := f.QueryToken(tokenContractId) + require.Equal(t, tokenContractId, token.GetContractID()) + require.Equal(t, tokenName, token.GetName()) + require.Equal(t, tokenSymbol, token.GetSymbol()) + require.Equal(t, tokenMeta, token.GetMeta()) + require.Equal(t, tokenImageURI, token.GetImageURI()) + require.Equal(t, tokenDecimals, token.GetDecimals()) + require.True(t, token.GetMintable()) + + perms := f.QueryAccountPermission(contractAddress, tokenContractId) + require.Len(t, perms, 3) + } + + // mint token + { + msg := map[string]map[string]interface{}{ + "mint": { + "from": contractAddress, + "contract_id": tokenContractId, + "to": contractAddress, + "amount": strconv.Itoa(mintAmount), + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate total supply, mint, burn + { + totalSupply := f.QuerySupplyToken(tokenContractId) + require.Equal(t, sdk.NewInt(int64(initAmount+mintAmount)), totalSupply) + totalMint := f.QueryMintToken(tokenContractId) + require.Equal(t, sdk.NewInt(int64(initAmount+mintAmount)), totalMint) + totalBurn := f.QueryBurnToken(tokenContractId) + require.Equal(t, sdk.NewInt(0), totalBurn) + } + + // transfer token + { + msg := map[string]map[string]interface{}{ + "transfer": { + "from": contractAddress, + "contract_id": tokenContractId, + "to": fooAddr, + "amount": strconv.Itoa(transferAmount), + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate balance + { + balanceOfContract := f.QueryBalanceToken(tokenContractId, contractAddress) + require.Equal(t, sdk.NewInt(int64(initAmount+mintAmount-transferAmount)), balanceOfContract) + balanceOfFoo := f.QueryBalanceToken(tokenContractId, fooAddr) + require.Equal(t, sdk.NewInt(int64(transferAmount)), balanceOfFoo) + } + + // burn token + { + msg := map[string]map[string]interface{}{ + "burn": { + "from": contractAddress, + "contract_id": tokenContractId, + "amount": strconv.Itoa(burnAmount), + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate total supply, mint, burn + { + totalSupply := f.QuerySupplyToken(tokenContractId) + require.Equal(t, sdk.NewInt(int64(initAmount+mintAmount-burnAmount)), totalSupply) + totalMint := f.QueryMintToken(tokenContractId) + require.Equal(t, sdk.NewInt(int64(initAmount+mintAmount)), totalMint) + totalBurn := f.QueryBurnToken(tokenContractId) + require.Equal(t, sdk.NewInt(int64(burnAmount)), totalBurn) + } + + // validate that fooAddr cannot mint token + { + perms := f.QueryAccountPermission(fooAddr, tokenContractId) + require.Len(t, perms, 0) + _, res, _ := f.TxTokenMint(fooAddr.String(), tokenContractId, fooAddr.String(), strconv.Itoa(mintFromFooAmount), "-y") + require.True(t, strings.Contains(res, "Permission: mint: failed")) + } + + // grant permission for fooAddr + { + msg := map[string]map[string]interface{}{ + "grant_perm": { + "from": contractAddress, + "contract_id": tokenContractId, + "to": fooAddr, + "permission": "mint", + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate permission for fooAddr + { + perms := f.QueryAccountPermission(fooAddr, tokenContractId) + require.Len(t, perms, 1) + require.Equal(t, tokenModule.Permission("mint"), perms[0]) + } + + // mint token from fooAddr + { + f.LogResult(f.TxTokenMint(fooAddr.String(), tokenContractId, fooAddr.String(), strconv.Itoa(mintFromFooAmount), "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate balance for fooAddr + { + balance := f.QueryBalanceToken(tokenContractId, fooAddr) + require.Equal(t, sdk.NewInt(int64(transferAmount+mintFromFooAmount)), balance) + } + + // revoke permission from contractAddress + { + msg := map[string]map[string]interface{}{ + "revoke_perm": { + "from": contractAddress, + "contract_id": tokenContractId, + "permission": "mint", + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate permission from contractAddress + { + perms := f.QueryAccountPermission(contractAddress, tokenContractId) + require.Len(t, perms, 2) + require.True(t, perms.HasPermission(tokenModule.NewBurnPermission())) + require.True(t, perms.HasPermission(tokenModule.NewModifyPermission())) + } + + // validate that contractAddress cannot mint token + { + msg := map[string]map[string]interface{}{ + "mint": { + "from": contractAddress, + "contract_id": tokenContractId, + "to": contractAddress, + "amount": strconv.Itoa(mintAmount), + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + _, _, errStr := f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y") + require.True(t, strings.Contains(errStr, "Permission: mint: failed")) + } + + // modify token + { + msg := map[string]map[string]interface{}{ + "modify": { + "owner": contractAddress, + "contract_id": tokenContractId, + "key": modifyKey, + "value": modifyValue, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate that updated token + { + token := f.QueryToken(tokenContractId) + require.Equal(t, tokenContractId, token.GetContractID()) + require.Equal(t, tokenName, token.GetName()) + require.Equal(t, tokenSymbol, token.GetSymbol()) + require.Equal(t, modifyValue, token.GetMeta()) + require.Equal(t, tokenImageURI, token.GetImageURI()) + require.Equal(t, tokenDecimals, token.GetDecimals()) + require.True(t, token.GetMintable()) + } + + // validate that fooAddr cannot burn token + { + perms := f.QueryAccountPermission(fooAddr, tokenContractId) + require.Len(t, perms, 1) + _, res, _ := f.TxTokenBurnFrom(fooAddr.String(), tokenContractId, contractAddress, int64(burnFromFooAmount), "-y") + require.True(t, strings.Contains(res, "proxy is not approved on the token")) + } + + // approve and grant burn perm + { + msg := map[string]map[string]interface{}{ + "approve": { + "approver": contractAddress, + "contract_id": tokenContractId, + "proxy": fooAddr, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + msg = map[string]map[string]interface{}{ + "grant_perm": { + "from": contractAddress, + "contract_id": tokenContractId, + "to": fooAddr, + "permission": "burn", + }, + } + msgJson, _ = json.Marshal(msg) + msgString = string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate approved and perm + { + res := f.QueryApprovedToken(tokenContractId, fooAddr, contractAddress) + require.True(t, res) + + perms := f.QueryAccountPermission(fooAddr, tokenContractId) + require.Len(t, perms, 2) + require.True(t, perms.HasPermission(tokenModule.NewMintPermission())) + require.True(t, perms.HasPermission(tokenModule.NewBurnPermission())) + } + + // burn from fooAddr + { + f.LogResult(f.TxTokenBurnFrom(fooAddr.String(), tokenContractId, contractAddress, int64(burnFromFooAmount), "-y")) + balanceOfContract := f.QueryBalanceToken(tokenContractId, contractAddress) + require.Equal(t, sdk.NewInt(int64(initAmount+mintAmount-transferAmount-burnAmount-burnFromFooAmount)), balanceOfContract) + + } + + // test for query + { + cdc := app.MakeCodec() + + // query tokens + query := map[string]map[string]interface{}{ + "get_token": { + "contract_id": tokenContractId, + }, + } + queryJson, _ := json.Marshal(query) + queryString := string(queryJson) + res := f.QueryContractStateSmartWasm(contractAddress, queryString) + var token tokenModule.Token + tokenModule.ModuleCdc.UnmarshalJSON([]byte(res), &token) + + require.Equal(t, tokenContractId, token.GetContractID()) + require.Equal(t, tokenName, token.GetName()) + require.Equal(t, tokenSymbol, token.GetSymbol()) + require.Equal(t, modifyValue, token.GetMeta()) + require.Equal(t, tokenImageURI, token.GetImageURI()) + require.Equal(t, tokenDecimals, token.GetDecimals()) + require.True(t, token.GetMintable()) + + // query balances + query = map[string]map[string]interface{}{ + "get_balance": { + "contract_id": tokenContractId, + "address": contractAddress, + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var balance sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &balance) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(int64(initAmount+mintAmount-transferAmount-burnAmount-burnFromFooAmount)), balance) + + // query total + query = map[string]map[string]interface{}{ + "get_total": { + "contract_id": tokenContractId, + "target": "mint", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var totalMint sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &totalMint) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(int64(initAmount+mintAmount+mintFromFooAmount)), totalMint) + + query = map[string]map[string]interface{}{ + "get_total": { + "contract_id": tokenContractId, + "target": "burn", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var totalBurn sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &totalBurn) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(int64(burnAmount+burnFromFooAmount)), totalBurn) + + query = map[string]map[string]interface{}{ + "get_total": { + "contract_id": tokenContractId, + "target": "supply", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var totalSupply sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &totalSupply) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(int64(initAmount+mintAmount+mintFromFooAmount-burnAmount-burnFromFooAmount)), totalSupply) + + // query perm + query = map[string]map[string]interface{}{ + "get_perm": { + "contract_id": tokenContractId, + "address": contractAddress, + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var perms tokenModule.Permissions + err = cdc.UnmarshalJSON([]byte(res), &perms) + require.NoError(f.T, err) + require.Equal(t, 2, len(perms)) + } +} + +func TestLinkCLIWasmTokenTesterProxy(t *testing.T) { + + t.Parallel() + f := InitFixtures(t) + defer f.Cleanup() + + // start linkd server + proc := f.LDStart() + defer func() { require.NoError(t, proc.Stop(false)) }() + + fooAddr := f.KeyAddress(keyFoo) + + flagFromFoo := fmt.Sprintf("--from=%s", fooAddr) + flagGas := "--gas=auto --gas-adjustment=1.2" + workDir, _ := os.Getwd() + wasmTokenTester := path.Join(workDir, "contracts", "token-tester", "contract.wasm") + codeId := uint64(1) + var contractAddress sdk.AccAddress + tokenContractId := "9be17165" + tokenName := "TestToken2" + tokenSymbol := "TT2" + tokenMeta := "meta" + + // store the contract token-tester + { + f.LogResult(f.TxStoreWasm(wasmTokenTester, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // instantiate + { + msgJson := "{}" + flagLabel := "--label=token-tester" + f.LogResult(f.TxInstantiateWasm(codeId, msgJson, flagLabel, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate there is only one contract using codeId=1 and get contractAddress + { + listContract := f.QueryListContractByCodeWasm(codeId) + require.Len(t, listContract, 1) + contractAddress = listContract[0].GetAddress() + } + + // set test token and approve for contractAddress + { + f.LogResult(f.TxTokenIssue(fooAddr.String(), fooAddr, tokenName, tokenMeta, tokenSymbol, 10000, 6, true, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + f.LogResult(f.TxTokenApprove(fooAddr.String(), tokenContractId, contractAddress, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + f.LogResult(f.TxTokenGrantPerm(fooAddr.String(), contractAddress.String(), tokenContractId, "burn", "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + res := f.QueryApprovedToken(tokenContractId, contractAddress, fooAddr) + require.True(t, res) + + } + + // burn token from proxy + { + msgJson := fmt.Sprintf("{\"burn_from\":{\"proxy\":\"%s\",\"from\":\"%s\",\"contract_id\":\"%s\",\"amount\":\"%s\"}}", contractAddress, fooAddr, tokenContractId, "2") + f.LogResult(f.TxExecuteWasm(contractAddress, msgJson, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate balance for fooAddr + { + balanceOfContract := f.QueryBalanceToken(tokenContractId, fooAddr) + require.Equal(t, sdk.NewInt(9998), balanceOfContract) + } + + // transfer token from proxy + { + msgJson := fmt.Sprintf("{\"transfer_from\":{\"proxy\":\"%s\",\"from\":\"%s\",\"contract_id\":\"%s\",\"to\":\"%s\",\"amount\":\"%s\"}}", contractAddress, fooAddr, tokenContractId, contractAddress, "3") + f.LogResult(f.TxExecuteWasm(contractAddress, msgJson, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate balance + { + balanceOfFoo := f.QueryBalanceToken(tokenContractId, fooAddr) + require.Equal(t, sdk.NewInt(9995), balanceOfFoo) + balanceOfContract := f.QueryBalanceToken(tokenContractId, contractAddress) + require.Equal(t, sdk.NewInt(3), balanceOfContract) + } + + // test for query + { + cdc := app.MakeCodec() + + // query isApproved + query := map[string]map[string]interface{}{ + "get_is_approved": { + "proxy": contractAddress, + "contract_id": tokenContractId, + "approver": fooAddr, + }, + } + queryJson, _ := json.Marshal(query) + queryString := string(queryJson) + res := f.QueryContractStateSmartWasm(contractAddress, queryString) + var isApproved bool + err := cdc.UnmarshalJSON([]byte(res), &isApproved) + require.NoError(f.T, err) + require.True(t, isApproved) + + // query approvers + query = map[string]map[string]interface{}{ + "get_approvers": { + "proxy": contractAddress, + "contract_id": tokenContractId, + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var approvers []sdk.AccAddress + err = cdc.UnmarshalJSON([]byte(res), &approvers) + require.NoError(f.T, err) + require.Equal(t, fooAddr, approvers[0]) + } +} + +func TestLinkCLIWasmCollectionTester(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + defer f.Cleanup() + + // start linkd server + proc := f.LDStart() + defer func() { require.NoError(t, proc.Stop(false)) }() + + fooAddr := f.KeyAddress(keyFoo) + + flagFromFoo := fmt.Sprintf("--from=%s", fooAddr) + flagGas := "--gas=auto --gas-adjustment=1.2" + workDir, _ := os.Getwd() + wasmCollectionTester := path.Join(workDir, "contracts", "collection-tester", "contract.wasm") + codeId := uint64(1) + var contractAddress sdk.AccAddress + collectionContractId := "9be17165" + collectionName := "TestCollection1" + collectionMeta := "meta" + collectionBaseImageURI := "http://example.com/image" + + cdc := app.MakeCodec() + + nftName1 := "TestNFT1" + nftMeta1 := "nftMeta1" + nftName2 := "TestNFT2" + nftMeta2 := "nftMeta2" + nftName3 := "TestNFT3" + nftMeta3 := "nftMeta3" + + tokenTypeID1 := "10000001" + tokenTypeID2 := "10000002" + tokenTypeID3 := "10000003" + + index1 := "00000001" + index2 := "00000002" + nft0ID := tokenTypeID1 + index1 + nft1ID := tokenTypeID1 + index2 + nft2ID := tokenTypeID2 + index1 + nft3ID := tokenTypeID3 + index1 + mintNftName := "nft-0" + mintNftMeta := "" + + ftName := "TestFT1" + ftMeta := "ftMeta" + tokenID := "0000000100000000" + + initFtAmount := 1 + mintFtAmount := 99 + burnFtAmount := 3 + transferFtAmount := 10 + mintFt := strconv.Itoa(mintFtAmount) + ":" + tokenID + burnFt := strconv.Itoa(burnFtAmount) + ":" + tokenID + transferFt := strconv.Itoa(transferFtAmount) + ":" + tokenID + + // store the contract collection-tester + { + f.LogResult(f.TxStoreWasm(wasmCollectionTester, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // instantiate + { + msgString := "{}" + flagLabel := "--label=collection-tester" + f.LogResult(f.TxInstantiateWasm(codeId, msgString, flagLabel, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate there is only one contract using codeId=1 and get contractAddress + { + listContract := f.QueryListContractByCodeWasm(codeId) + require.Len(t, listContract, 1) + contractAddress = listContract[0].GetAddress() + } + + // create collection + { + msg := map[string]map[string]interface{}{ + "create": { + "owner": contractAddress, + "name": collectionName, + "meta": collectionMeta, + "base_img_uri": collectionBaseImageURI, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate that collection is issued + { + collection := f.QueryCollection(collectionContractId) + require.Equal(t, collectionContractId, collection.GetContractID()) + require.Equal(t, collectionName, collection.GetName()) + require.Equal(t, collectionMeta, collection.GetMeta()) + require.Equal(t, collectionBaseImageURI, collection.GetBaseImgURI()) + } + + // issue nft + { + msg := map[string]map[string]interface{}{ + "issue_nft": { + "owner": contractAddress, + "contract_id": collectionContractId, + "name": nftName1, + "meta": nftMeta1, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate issued nft + { + tokenType := f.QueryTokenTypeCollection(collectionContractId, tokenTypeID1) + require.Equal(t, nftName1, tokenType.GetName()) + require.Equal(t, nftMeta1, tokenType.GetMeta()) + require.Equal(t, collectionContractId, tokenType.GetContractID()) + require.Equal(t, tokenTypeID1, tokenType.GetTokenType()) + } + + // issue ft + { + msg := map[string]map[string]interface{}{ + "issue_ft": { + "owner": contractAddress, + "contract_id": collectionContractId, + "to": contractAddress, + "name": ftName, + "meta": ftMeta, + "amount": strconv.Itoa(initFtAmount), + "mintable": true, + "decimals": sdk.NewInt(8), + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate issued ft + { + token := f.QueryTokenCollection(collectionContractId, tokenID).(collectionModule.FT) + require.Equal(t, collectionContractId, token.GetContractID()) + require.Equal(t, ftName, token.GetName()) + require.Equal(t, ftMeta, token.GetMeta()) + require.Equal(t, tokenID, token.GetTokenID()) + require.Equal(t, sdk.NewInt(8), token.GetDecimals()) + require.True(t, token.GetMintable()) + + balanceOfContract := f.QueryBalanceCollection(collectionContractId, tokenID, contractAddress) + require.Equal(t, sdk.NewInt(int64(initFtAmount)), balanceOfContract) + + totalMint := f.QueryTotalMintTokenCollection(collectionContractId, tokenID) + require.Equal(t, sdk.NewInt(int64(initFtAmount)), totalMint) + + totalSupply := f.QueryTotalSupplyTokenCollection(collectionContractId, tokenID) + require.Equal(t, sdk.NewInt(int64(initFtAmount)), totalSupply) + + totalBurn := f.QueryTotalBurnTokenCollection(collectionContractId, tokenID) + require.Equal(t, sdk.NewInt(0), totalBurn) + } + + // mint nft + { + msg := map[string]map[string]interface{}{ + "mint_nft": { + "from": contractAddress, + "contract_id": collectionContractId, + "to": contractAddress, + "token_types": []string{tokenTypeID1}, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate minted nft + { + nft := f.QueryTokenCollection(collectionContractId, nft0ID).(collectionModule.NFT) + require.Equal(t, collectionContractId, nft.GetContractID()) + require.Equal(t, nft0ID, nft.GetTokenID()) + require.Equal(t, contractAddress, nft.GetOwner()) + require.Equal(t, mintNftName, nft.GetName()) + require.Equal(t, mintNftMeta, nft.GetMeta()) + } + + // mint ft + { + msg := map[string]map[string]interface{}{ + "mint_ft": { + "from": contractAddress, + "contract_id": collectionContractId, + "to": contractAddress, + "tokens": []string{mintFt}, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate minted ft + { + balanceOfContract := f.QueryBalanceCollection(collectionContractId, tokenID, contractAddress) + require.Equal(t, sdk.NewInt(int64(initFtAmount+mintFtAmount)), balanceOfContract) + } + + // modify token info + { + msg := map[string]map[string]interface{}{ + "modify": { + "owner": contractAddress, + "contract_id": collectionContractId, + "token_type": tokenTypeID1, + "token_index": index1, + "key": "meta", + "value": "modified_meta", + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate modified nft + { + nft := f.QueryTokenCollection(collectionContractId, nft0ID).(collectionModule.NFT) + require.Equal(t, collectionContractId, nft.GetContractID()) + require.Equal(t, nft0ID, nft.GetTokenID()) + require.Equal(t, contractAddress, nft.GetOwner()) + require.Equal(t, mintNftName, nft.GetName()) + require.Equal(t, "modified_meta", nft.GetMeta()) + } + + // burn nft + { + msg := map[string]map[string]interface{}{ + "burn_nft": { + "from": contractAddress, + "contract_id": collectionContractId, + "token_id": nft0ID, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate burn nft + { + f.QueryTokenCollectionExpectEmpty(collectionContractId, nft0ID) + } + + // burn ft + { + msg := map[string]map[string]interface{}{ + "burn_ft": { + "from": contractAddress, + "contract_id": collectionContractId, + "amounts": []string{burnFt}, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate burn ft + { + balanceOfContract := f.QueryBalanceCollection(collectionContractId, tokenID, contractAddress) + require.Equal(t, sdk.NewInt(int64(initFtAmount+mintFtAmount-burnFtAmount)), balanceOfContract) + + totalMint := f.QueryTotalMintTokenCollection(collectionContractId, tokenID) + require.Equal(t, sdk.NewInt(int64(initFtAmount+mintFtAmount)), totalMint) + + totalSupply := f.QueryTotalSupplyTokenCollection(collectionContractId, tokenID) + require.Equal(t, sdk.NewInt(int64(initFtAmount+mintFtAmount-burnFtAmount)), totalSupply) + + totalBurn := f.QueryTotalBurnTokenCollection(collectionContractId, tokenID) + require.Equal(t, sdk.NewInt(int64(burnFtAmount)), totalBurn) + } + + // transfer nft + { + // prepare nft + msg := map[string]map[string]interface{}{ + "mint_nft": { + "from": contractAddress, + "contract_id": collectionContractId, + "to": contractAddress, + "token_types": []string{tokenTypeID1}, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + // transfer nft to fooAddr + msg = map[string]map[string]interface{}{ + "transfer_nft": { + "from": contractAddress, + "contract_id": collectionContractId, + "to": fooAddr, + "token_ids": []string{nft1ID}, + }, + } + msgJson, _ = json.Marshal(msg) + msgString = string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate transfered nft + { + nft := f.QueryTokenCollection(collectionContractId, nft1ID).(collectionModule.NFT) + require.Equal(t, collectionContractId, nft.GetContractID()) + require.Equal(t, nft1ID, nft.GetTokenID()) + require.Equal(t, fooAddr, nft.GetOwner()) + require.Equal(t, mintNftName, nft.GetName()) + require.Equal(t, mintNftMeta, nft.GetMeta()) + } + + // transfer ft + { + msg := map[string]map[string]interface{}{ + "transfer_ft": { + "from": contractAddress, + "contract_id": collectionContractId, + "to": fooAddr, + "tokens": []string{transferFt}, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate transfered ft + { + balanceOfContract := f.QueryBalanceCollection(collectionContractId, tokenID, contractAddress) + require.Equal(t, sdk.NewInt(int64(initFtAmount+mintFtAmount-burnFtAmount-transferFtAmount)), balanceOfContract) + balanceOfFoo := f.QueryBalanceCollection(collectionContractId, tokenID, fooAddr) + require.Equal(t, sdk.NewInt(int64(transferFtAmount)), balanceOfFoo) + } + + // validate that contractAddress has all the permissions + { + perms := f.QueryAccountPermissionCollection(contractAddress, collectionContractId) + require.Equal(t, 4, len(perms)) + require.True(t, perms.HasPermission(collectionModule.NewMintPermission())) + require.True(t, perms.HasPermission(collectionModule.NewBurnPermission())) + require.True(t, perms.HasPermission(collectionModule.NewIssuePermission())) + require.True(t, perms.HasPermission(collectionModule.NewModifyPermission())) + } + + // validate that fooAddr cannot mint token + { + perms := f.QueryAccountPermissionCollection(fooAddr, collectionContractId) + require.Len(t, perms, 0) + mintParam := strings.Join([]string{tokenTypeID1, "description", "meta"}, ":") + _, res, _ := f.TxTokenMintNFTCollection(fooAddr.String(), collectionContractId, fooAddr.String(), mintParam, "-y") + require.True(t, strings.Contains(res, "Permission: mint: failed")) + } + + // grant permission + { + msg := map[string]map[string]interface{}{ + "grant_perm": { + "from": contractAddress, + "contract_id": collectionContractId, + "to": fooAddr, + "permission": "mint", + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate perm + { + perms := f.QueryAccountPermissionCollection(fooAddr, collectionContractId) + require.Equal(t, 1, len(perms)) + require.True(t, perms.HasPermission(collectionModule.NewMintPermission())) + + f.TxTokenMintFTCollection(fooAddr.String(), collectionContractId, fooAddr.String(), mintFt, "-y") + balanceOfFoo := f.QueryBalanceCollection(collectionContractId, tokenID, fooAddr) + require.Equal(t, sdk.NewInt(int64(transferFtAmount+mintFtAmount)), balanceOfFoo) + } + + // revoke perm + { + msg := map[string]map[string]interface{}{ + "revoke_perm": { + "from": contractAddress, + "contract_id": collectionContractId, + "permission": "mint", + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate permission from contractAddress + { + perms := f.QueryAccountPermissionCollection(contractAddress, collectionContractId) + require.Len(t, perms, 3) + require.True(t, perms.HasPermission(collectionModule.NewBurnPermission())) + require.True(t, perms.HasPermission(collectionModule.NewIssuePermission())) + require.True(t, perms.HasPermission(collectionModule.NewModifyPermission())) + } + + // attach token + { + // prepare token + msg := map[string]map[string]interface{}{ + "issue_nft": { + "owner": contractAddress, + "contract_id": collectionContractId, + "name": nftName2, + "meta": nftMeta2, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + msg = map[string]map[string]interface{}{ + "issue_nft": { + "owner": contractAddress, + "contract_id": collectionContractId, + "name": nftName3, + "meta": nftMeta3, + }, + } + msgJson, _ = json.Marshal(msg) + msgString = string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + msg = map[string]map[string]interface{}{ + "mint_nft": { + "from": contractAddress, + "contract_id": collectionContractId, + "to": contractAddress, + "token_types": []string{tokenTypeID2, tokenTypeID3}, + }, + } + msgJson, _ = json.Marshal(msg) + msgString = string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + // attach + msg = map[string]map[string]interface{}{ + "attach": { + "from": contractAddress, + "contract_id": collectionContractId, + "to_token_id": nft3ID, + "token_id": nft2ID, + }, + } + msgJson, _ = json.Marshal(msg) + msgString = string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate attached token + { + parent := f.QueryParentTokenCollection(collectionContractId, nft2ID) + require.Equal(t, nft3ID, parent.GetTokenID()) + + children := f.QueryChildrenTokenCollection(collectionContractId, nft3ID) + require.Equal(t, 1, len(children)) + require.Equal(t, nft2ID, children[0].GetTokenID()) + + // validate for query encoder + query := map[string]map[string]interface{}{ + "get_root_or_parent_or_children": { + "contract_id": collectionContractId, + "token_id": nft2ID, + "target": "parent", + }, + } + queryJson, _ := json.Marshal(query) + queryString := string(queryJson) + res := f.QueryContractStateSmartWasm(contractAddress, queryString) + + var parentToken collectionModule.Token + err := cdc.UnmarshalJSON([]byte(res), &parentToken) + require.NoError(f.T, err) + require.Equal(t, nft3ID, parentToken.(collectionModule.NFT).GetTokenID()) + + query = map[string]map[string]interface{}{ + "get_root_or_parent_or_children": { + "contract_id": collectionContractId, + "token_id": nft2ID, + "target": "root", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + + var rootToken collectionModule.Token + err = cdc.UnmarshalJSON([]byte(res), &rootToken) + require.NoError(f.T, err) + require.Equal(t, nft3ID, rootToken.(collectionModule.NFT).GetTokenID()) + + query = map[string]map[string]interface{}{ + "get_root_or_parent_or_children": { + "contract_id": collectionContractId, + "token_id": nft3ID, + "target": "children", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + + var childrenTokens []collectionModule.Token + err = cdc.UnmarshalJSON([]byte(res), &childrenTokens) + require.NoError(f.T, err) + require.Equal(t, 1, len(childrenTokens)) + require.Equal(t, nft2ID, childrenTokens[0].(collectionModule.NFT).GetTokenID()) + } + + // detach token + { + msg := map[string]map[string]interface{}{ + "detach": { + "contract_id": collectionContractId, + "from": contractAddress, + "token_id": nft2ID, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate attached token + { + parentToken := f.QueryParentTokenCollection(collectionContractId, nft2ID) + require.Nil(t, parentToken) + + childrenTokens := f.QueryChildrenTokenCollection(collectionContractId, nft3ID) + require.Equal(t, 0, len(childrenTokens)) + + rootToken := f.QueryRootTokenCollection(collectionContractId, nft2ID) + require.Equal(t, nft2ID, rootToken.(collectionModule.NFT).GetTokenID()) + } + + // test for query + { + cdc := app.MakeCodec() + + // query collection + query := map[string]map[string]interface{}{ + "get_collection": { + "contract_id": collectionContractId, + }, + } + queryJson, _ := json.Marshal(query) + queryString := string(queryJson) + res := f.QueryContractStateSmartWasm(contractAddress, queryString) + var collection collectionModule.Collection + err := cdc.UnmarshalJSON([]byte(res), &collection) + require.NoError(f.T, err) + require.Equal(t, collectionContractId, collection.GetContractID()) + + // query balance + query = map[string]map[string]interface{}{ + "get_balance": { + "contract_id": collectionContractId, + "token_id": tokenID, + "addr": contractAddress, + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var balanceOfContract sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &balanceOfContract) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(int64(initFtAmount+mintFtAmount-burnFtAmount-transferFtAmount)), balanceOfContract) + + // query token type + query = map[string]map[string]interface{}{ + "get_token_type": { + "contract_id": collectionContractId, + "token_id": tokenTypeID1, + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + + var tokenType collectionModule.TokenType + err = cdc.UnmarshalJSON([]byte(res), &tokenType) + require.NoError(f.T, err) + require.Equal(t, nftName1, tokenType.GetName()) + require.Equal(t, nftMeta1, tokenType.GetMeta()) + require.Equal(t, collectionContractId, tokenType.GetContractID()) + require.Equal(t, tokenTypeID1, tokenType.GetTokenType()) + + // query token types + query = map[string]map[string]interface{}{ + "get_token_types": { + "contract_id": collectionContractId, + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + + var tokenTypes []collectionModule.TokenType + err = cdc.UnmarshalJSON([]byte(res), &tokenTypes) + require.NoError(f.T, err) + require.Equal(t, 3, len(tokenTypes)) + + // query token + query = map[string]map[string]interface{}{ + "get_token": { + "contract_id": collectionContractId, + "token_id": tokenID, + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var token collectionModule.FT + err = cdc.UnmarshalJSON([]byte(res), &token) + require.NoError(f.T, err) + + require.Equal(t, collectionContractId, token.GetContractID()) + require.Equal(t, ftName, token.GetName()) + require.Equal(t, ftMeta, token.GetMeta()) + require.Equal(t, tokenID, token.GetTokenID()) + require.Equal(t, sdk.NewInt(8), token.GetDecimals()) + require.True(t, token.GetMintable()) + + // query tokens + query = map[string]map[string]interface{}{ + "get_tokens": { + "contract_id": collectionContractId, + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var tokens []collectionModule.Token + err = cdc.UnmarshalJSON([]byte(res), &tokens) + require.NoError(f.T, err) + require.Equal(t, 4, len(tokens)) + + // query nft total + query = map[string]map[string]interface{}{ + "get_nft_count": { + "contract_id": collectionContractId, + "token_id": tokenTypeID3, + "target": "count", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var nftCount sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &nftCount) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(1), nftCount) + + query = map[string]map[string]interface{}{ + "get_nft_count": { + "contract_id": collectionContractId, + "token_id": tokenTypeID1, + "target": "mint", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var nftMint sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &nftMint) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(2), nftMint) + + query = map[string]map[string]interface{}{ + "get_nft_count": { + "contract_id": collectionContractId, + "token_id": tokenTypeID1, + "target": "burn", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var nftBurn sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &nftBurn) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(1), nftBurn) + + // query total + query = map[string]map[string]interface{}{ + "get_total": { + "contract_id": collectionContractId, + "token_id": tokenID, + "target": "supply", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var totalSupply sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &totalSupply) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(int64(initFtAmount+2*mintFtAmount-burnFtAmount)), totalSupply) + + query = map[string]map[string]interface{}{ + "get_total": { + "contract_id": collectionContractId, + "token_id": tokenID, + "target": "mint", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var totalMint sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &totalMint) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(int64(initFtAmount+2*mintFtAmount)), totalMint) + + query = map[string]map[string]interface{}{ + "get_total": { + "contract_id": collectionContractId, + "token_id": tokenID, + "target": "burn", + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var totalBurn sdk.Int + err = cdc.UnmarshalJSON([]byte(res), &totalBurn) + require.NoError(f.T, err) + require.Equal(t, sdk.NewInt(int64(burnFtAmount)), totalBurn) + + } +} + +func TestLinkCLIWasmCollectionTesterProxy(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + defer f.Cleanup() + + // start linkd server + proc := f.LDStart() + defer func() { require.NoError(t, proc.Stop(false)) }() + + fooAddr := f.KeyAddress(keyFoo) + + flagFromFoo := fmt.Sprintf("--from=%s", fooAddr) + flagGas := "--gas=auto --gas-adjustment=1.2" + workDir, _ := os.Getwd() + wasmCollectionTester := path.Join(workDir, "contracts", "collection-tester", "contract.wasm") + codeId := uint64(1) + var contractAddress sdk.AccAddress + collectionContractId := "9be17165" + collectionName := "TestCollection1" + collectionMeta := "meta" + collectionBaseImageURI := "http://example.com/image" + + nftName1 := "TestNFT1" + nftMeta1 := "nftMeta1" + nftName2 := "TestNFT2" + nftMeta2 := "nftMeta2" + nftName3 := "TestNFT3" + nftMeta3 := "nftMeta3" + + tokenTypeID1 := "10000001" + tokenTypeID2 := "10000002" + tokenTypeID3 := "10000003" + + index1 := "00000001" + index2 := "00000002" + nft0ID := tokenTypeID1 + index1 + nft1ID := tokenTypeID1 + index2 + nft2ID := tokenTypeID2 + index1 + nft3ID := tokenTypeID3 + index1 + + ftName := "TestFT1" + ftMeta := "ftMeta" + tokenID := "0000000100000000" + + burnFtAmount := 3 + transferFtAmount := 10 + burnFt := strconv.Itoa(burnFtAmount) + ":" + tokenID + transferFt := strconv.Itoa(transferFtAmount) + ":" + tokenID + + // store the contract collection-tester + { + f.LogResult(f.TxStoreWasm(wasmCollectionTester, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // instantiate + { + msgJson := "{}" + flagLabel := "--label=collection-tester" + f.LogResult(f.TxInstantiateWasm(codeId, msgJson, flagLabel, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate there is only one contract using codeId=1 and get contractAddress + { + listContract := f.QueryListContractByCodeWasm(codeId) + require.Len(t, listContract, 1) + contractAddress = listContract[0].GetAddress() + } + + // set test token and approve for contractAddress + { + // create collection + f.LogResult(f.TxTokenCreateCollection(fooAddr.String(), collectionName, collectionMeta, collectionBaseImageURI, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + // issue ft + f.LogResult(f.TxTokenIssueFTCollection(fooAddr.String(), collectionContractId, fooAddr, ftName, ftMeta, 10000, 6, true, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + // issue nft + f.LogResult(f.TxTokenIssueNFTCollection(fooAddr.String(), collectionContractId, nftName1, nftMeta1, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + // mint nft + mintParam := strings.Join([]string{tokenTypeID1, "description", "meta"}, ":") + f.LogResult(f.TxTokenMintNFTCollection(fooAddr.String(), collectionContractId, fooAddr.String(), mintParam, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // approve and grant burn perm + { + nft := f.QueryTokenCollection(collectionContractId, nft0ID).(collectionModule.NFT) + require.Equal(t, collectionContractId, nft.GetContractID()) + require.Equal(t, nft0ID, nft.GetTokenID()) + require.Equal(t, fooAddr, nft.GetOwner()) + + f.LogResult(f.TxCollectionApprove(fooAddr.String(), collectionContractId, contractAddress, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + f.LogResult(f.TxCollectionGrantPerm(fooAddr.String(), contractAddress, collectionContractId, "burn", "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + res := f.QueryApprovedTokenCollection(collectionContractId, contractAddress, fooAddr) + require.True(t, res) + } + + // query isApproved + { + cdc := app.MakeCodec() + + query := map[string]map[string]interface{}{ + "get_approved": { + "proxy": contractAddress, + "contract_id": collectionContractId, + "approver": fooAddr, + }, + } + queryJson, _ := json.Marshal(query) + queryString := string(queryJson) + res := f.QueryContractStateSmartWasm(contractAddress, queryString) + var isApproved bool + err := cdc.UnmarshalJSON([]byte(res), &isApproved) + require.NoError(f.T, err) + require.True(t, isApproved) + + // query approvers + query = map[string]map[string]interface{}{ + "get_approvers": { + "proxy": contractAddress, + "contract_id": collectionContractId, + }, + } + queryJson, _ = json.Marshal(query) + queryString = string(queryJson) + res = f.QueryContractStateSmartWasm(contractAddress, queryString) + var approvers []sdk.AccAddress + err = cdc.UnmarshalJSON([]byte(res), &approvers) + require.NoError(f.T, err) + require.Equal(t, fooAddr, approvers[0]) + } + + // burn ft from proxy + { + msg := map[string]map[string]interface{}{ + "burn_ft_from": { + "proxy": contractAddress, + "from": fooAddr, + "contract_id": collectionContractId, + "amounts": []string{burnFt}, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate balance for fooAddr + { + balanceOfFoo := f.QueryBalanceCollection(collectionContractId, tokenID, fooAddr) + require.Equal(t, sdk.NewInt(9997), balanceOfFoo) + } + + // burn nft from proxy + { + msg := map[string]map[string]interface{}{ + "burn_nft_from": { + "proxy": contractAddress, + "from": fooAddr, + "contract_id": collectionContractId, + "token_ids": []string{nft0ID}, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate burn nft + { + f.QueryTokenCollectionExpectEmpty(collectionContractId, nft0ID) + } + + // transfer ft from + { + msg := map[string]map[string]interface{}{ + "transfer_ft_from": { + "proxy": contractAddress, + "contract_id": collectionContractId, + "from": fooAddr, + "to": contractAddress, + "tokens": []string{transferFt}, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate balance for contractAddress + { + balanceOfContract := f.QueryBalanceCollection(collectionContractId, tokenID, contractAddress) + require.Equal(t, sdk.NewInt(int64(transferFtAmount)), balanceOfContract) + } + + // transfer nft from + { + // prepare nft + mintParam := strings.Join([]string{tokenTypeID1, "description", "meta"}, ":") + f.LogResult(f.TxTokenMintNFTCollection(fooAddr.String(), collectionContractId, fooAddr.String(), mintParam, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + msg := map[string]map[string]interface{}{ + "transfer_nft_from": { + "proxy": contractAddress, + "contract_id": collectionContractId, + "from": fooAddr, + "to": contractAddress, + "token_ids": []string{nft1ID}, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate transfered nft + { + nft := f.QueryTokenCollection(collectionContractId, nft1ID).(collectionModule.NFT) + require.Equal(t, collectionContractId, nft.GetContractID()) + require.Equal(t, nft1ID, nft.GetTokenID()) + require.Equal(t, contractAddress, nft.GetOwner()) + } + + // token attach, detach from + { + // issue nft + f.LogResult(f.TxTokenIssueNFTCollection(fooAddr.String(), collectionContractId, nftName2, nftMeta2, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + f.LogResult(f.TxTokenIssueNFTCollection(fooAddr.String(), collectionContractId, nftName3, nftMeta3, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + // mint nft + mintParam := strings.Join([]string{tokenTypeID2, "description", "meta"}, ":") + f.LogResult(f.TxTokenMintNFTCollection(fooAddr.String(), collectionContractId, fooAddr.String(), mintParam, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + mintParam = strings.Join([]string{tokenTypeID3, "description", "meta"}, ":") + f.LogResult(f.TxTokenMintNFTCollection(fooAddr.String(), collectionContractId, fooAddr.String(), mintParam, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + + // attach from + msg := map[string]map[string]interface{}{ + "attach_from": { + "proxy": contractAddress, + "contract_id": collectionContractId, + "from": fooAddr, + "to_token_id": nft3ID, + "token_id": nft2ID, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate attach from + { + parent := f.QueryParentTokenCollection(collectionContractId, nft2ID) + require.Equal(t, nft3ID, parent.GetTokenID()) + + children := f.QueryChildrenTokenCollection(collectionContractId, nft3ID) + require.Equal(t, 1, len(children)) + require.Equal(t, nft2ID, children[0].GetTokenID()) + + root := f.QueryRootTokenCollection(collectionContractId, nft2ID) + require.Equal(t, nft3ID, root.GetTokenID()) + } + + // detach from + { + msg := map[string]map[string]interface{}{ + "detach_from": { + "proxy": contractAddress, + "contract_id": collectionContractId, + "from": fooAddr, + "token_id": nft2ID, + }, + } + msgJson, _ := json.Marshal(msg) + msgString := string(msgJson) + f.LogResult(f.TxExecuteWasm(contractAddress, msgString, flagFromFoo, flagGas, "-y")) + tests.WaitForNextNBlocksTM(1, f.Port) + } + + // validate detach from + { + parentToken := f.QueryParentTokenCollection(collectionContractId, nft2ID) + require.Nil(t, parentToken) + + childrenTokens := f.QueryChildrenTokenCollection(collectionContractId, nft3ID) + require.Equal(t, 0, len(childrenTokens)) + + rootToken := f.QueryRootTokenCollection(collectionContractId, nft2ID) + require.Equal(t, nft2ID, rootToken.(collectionModule.NFT).GetTokenID()) + } +} diff --git a/x/wasm/linkwasmd/cli_test/contracts/collection-tester/contract.wasm b/x/wasm/linkwasmd/cli_test/contracts/collection-tester/contract.wasm new file mode 100644 index 0000000000..a6fc6cbe03 Binary files /dev/null and b/x/wasm/linkwasmd/cli_test/contracts/collection-tester/contract.wasm differ diff --git a/x/wasm/linkwasmd/cli_test/contracts/collection-tester/hash.txt b/x/wasm/linkwasmd/cli_test/contracts/collection-tester/hash.txt new file mode 100644 index 0000000000..2a3537495e --- /dev/null +++ b/x/wasm/linkwasmd/cli_test/contracts/collection-tester/hash.txt @@ -0,0 +1,2 @@ +bd2c6262cafd49d2f331a0583ba9de2960ac94ed8e6fd923c863f21ea0eaf6d7 - + diff --git a/x/wasm/linkwasmd/cli_test/contracts/escrow/README.md b/x/wasm/linkwasmd/cli_test/contracts/escrow/README.md new file mode 100644 index 0000000000..8c8133d3ca --- /dev/null +++ b/x/wasm/linkwasmd/cli_test/contracts/escrow/README.md @@ -0,0 +1,9 @@ +# Escrow v0.6.0 +This is the wasm compiled from https://github.com/CosmWasm/cosmwasm-examples/tree/escrow-0.6.0/escrow with + +``` +docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/rust-optimizer:0.9.0 +``` diff --git a/x/wasm/linkwasmd/cli_test/contracts/escrow/contract.wasm b/x/wasm/linkwasmd/cli_test/contracts/escrow/contract.wasm new file mode 100644 index 0000000000..452fc80932 Binary files /dev/null and b/x/wasm/linkwasmd/cli_test/contracts/escrow/contract.wasm differ diff --git a/x/wasm/linkwasmd/cli_test/contracts/escrow/hash.txt b/x/wasm/linkwasmd/cli_test/contracts/escrow/hash.txt new file mode 100644 index 0000000000..f8fd315327 --- /dev/null +++ b/x/wasm/linkwasmd/cli_test/contracts/escrow/hash.txt @@ -0,0 +1 @@ +9652bd9e784c47dd24a75afd1a6a464e7953a307689c5eccd7284294a2094d5b - diff --git a/x/wasm/linkwasmd/cli_test/contracts/token-tester/REAMDE.md b/x/wasm/linkwasmd/cli_test/contracts/token-tester/REAMDE.md new file mode 100644 index 0000000000..42104a0e2f --- /dev/null +++ b/x/wasm/linkwasmd/cli_test/contracts/token-tester/REAMDE.md @@ -0,0 +1,2 @@ +# token-tester +This is the wasm compiled from https://github.com/line/cosmwasm/tree/master/contracts/token-tester . diff --git a/x/wasm/linkwasmd/cli_test/contracts/token-tester/contract.wasm b/x/wasm/linkwasmd/cli_test/contracts/token-tester/contract.wasm new file mode 100644 index 0000000000..5e2f551c7e Binary files /dev/null and b/x/wasm/linkwasmd/cli_test/contracts/token-tester/contract.wasm differ diff --git a/x/wasm/linkwasmd/cli_test/contracts/token-tester/hash.txt b/x/wasm/linkwasmd/cli_test/contracts/token-tester/hash.txt new file mode 100644 index 0000000000..a02bce2d80 --- /dev/null +++ b/x/wasm/linkwasmd/cli_test/contracts/token-tester/hash.txt @@ -0,0 +1 @@ +3bd085d8f27ab285320ee59f424492c1c80ddf2b40a227ade2fbd6775bed9d4e - diff --git a/x/wasm/linkwasmd/cli_test/doc.go b/x/wasm/linkwasmd/cli_test/doc.go new file mode 100644 index 0000000000..bcf9c5e4d0 --- /dev/null +++ b/x/wasm/linkwasmd/cli_test/doc.go @@ -0,0 +1,3 @@ +package clitest + +// package clitest runs integration tests which make use of CLI commands. diff --git a/x/wasm/linkwasmd/cli_test/test_helpers.go b/x/wasm/linkwasmd/cli_test/test_helpers.go new file mode 100644 index 0000000000..74bbc87617 --- /dev/null +++ b/x/wasm/linkwasmd/cli_test/test_helpers.go @@ -0,0 +1,1879 @@ +package clitest + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/spf13/viper" + + collectionModule "github.com/line/lbm-sdk/v2/x/collection" + tokenModule "github.com/line/lbm-sdk/v2/x/token" + "github.com/line/lbm-sdk/v2/x/wasm/linkwasmd/types" + + clientkeys "github.com/cosmos/cosmos-sdk/client/keys" + tmhttp "github.com/tendermint/tendermint/rpc/client/http" + + "github.com/stretchr/testify/require" + + cfg "github.com/tendermint/tendermint/config" + tmctypes "github.com/tendermint/tendermint/rpc/core/types" + tmtypes "github.com/tendermint/tendermint/types" + + wasmtypes "github.com/line/lbm-sdk/v2/x/wasm/internal/types" + "github.com/line/lbm-sdk/v2/x/wasm/linkwasmd/app" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/tests" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/slashing" + "github.com/cosmos/cosmos-sdk/x/staking" +) + +const ( + denom = "stake" + keyFoo = "foo" + keyBar = "bar" + fooDenom = "foot" + feeDenom = "feet" + fee2Denom = "fee2t" + keyBaz = "baz" + keyVesting = "vesting" + keyFooBarBaz = "foobarbaz" + + DenomStake = "stake2" + DenomLink = "link" + UserTina = "tina" + UserKevin = "kevin" + UserRinah = "rinah" + UserBrian = "brian" + UserEvelyn = "evelyn" + UserSam = "sam" +) + +const ( + namePrefix = "node" + networkNamePrefix = "line-linkdnode-testnet-" +) + +var curPort int32 = 26655 + +var ( + TotalCoins = sdk.NewCoins( + sdk.NewCoin(DenomLink, sdk.TokensFromConsensusPower(6000)), + sdk.NewCoin(DenomStake, sdk.TokensFromConsensusPower(600000000)), + sdk.NewCoin(fee2Denom, sdk.TokensFromConsensusPower(2000000)), + sdk.NewCoin(feeDenom, sdk.TokensFromConsensusPower(2000000)), + sdk.NewCoin(fooDenom, sdk.TokensFromConsensusPower(2000)), + sdk.NewCoin(denom, sdk.TokensFromConsensusPower(300)), // We don't use inflation + // sdk.NewCoin(denom, sdk.TokensFromConsensusPower(300).Add(sdk.NewInt(12))), // add coins from inflation + ) + + startCoins = sdk.NewCoins( + sdk.NewCoin(fee2Denom, sdk.TokensFromConsensusPower(1000000)), + sdk.NewCoin(feeDenom, sdk.TokensFromConsensusPower(1000000)), + sdk.NewCoin(fooDenom, sdk.TokensFromConsensusPower(1000)), + sdk.NewCoin(denom, sdk.TokensFromConsensusPower(150)), + ) + + vestingCoins = sdk.NewCoins( + sdk.NewCoin(feeDenom, sdk.TokensFromConsensusPower(500000)), + ) + + // coins we set during ./.initialize.sh + defaultCoins = sdk.NewCoins( + sdk.NewCoin(DenomLink, sdk.TokensFromConsensusPower(1000)), + sdk.NewCoin(DenomStake, sdk.TokensFromConsensusPower(100000000)), + ) +) + +func init() { + testnet := false + config := sdk.GetConfig() + config.SetBech32PrefixForAccount(types.Bech32PrefixAcc(testnet), types.Bech32PrefixAccPub(testnet)) + config.SetBech32PrefixForValidator(types.Bech32PrefixValAddr(testnet), types.Bech32PrefixValPub(testnet)) + config.SetBech32PrefixForConsensusNode(types.Bech32PrefixConsAddr(testnet), types.Bech32PrefixConsPub(testnet)) + config.SetCoinType(types.CoinType) + config.SetFullFundraiserPath(types.FullFundraiserPath) + config.Seal() +} + +// ___________________________________________________________________________________ +// Fixtures + +// Fixtures is used to setup the testing environment +type Fixtures struct { + BuildDir string + RootDir string + LinkdBinary string + LinkcliBinary string + ChainID string + RPCAddr string + Port string + LinkdHome string + LinkcliHome string + P2PAddr string + P2PPort string + Moniker string + BridgeIP string + T *testing.T +} + +// NewFixtures creates a new instance of Fixtures with many vars set +func NewFixtures(t *testing.T) *Fixtures { + tmpDir := path.Join(os.ExpandEnv("$HOME"), ".linkwasmtest") + err := os.MkdirAll(tmpDir, os.ModePerm) + require.NoError(t, err) + tmpDir, err = ioutil.TempDir(tmpDir, "linkwasm_integration_"+strings.Split(t.Name(), "/")[0]+"_") + require.NoError(t, err) + + servAddr, servPort := newTCPAddr(t) + p2pAddr, p2pPort := newTCPAddr(t) + + buildDir := os.Getenv("BUILDDIR") + if buildDir == "" { + buildDir, err = filepath.Abs("../build/") + require.NoError(t, err) + } + + return &Fixtures{ + T: t, + BuildDir: buildDir, + RootDir: tmpDir, + LinkdBinary: filepath.Join(buildDir, "linkwasmd"), + LinkcliBinary: filepath.Join(buildDir, "linkwasmcli"), + LinkdHome: filepath.Join(tmpDir, ".linkwasmd"), + LinkcliHome: filepath.Join(tmpDir, ".linkwasmcli"), + RPCAddr: servAddr, + P2PAddr: p2pAddr, + Port: servPort, + P2PPort: p2pPort, + Moniker: "", // initialized by LDInit + BridgeIP: "", + } +} + +func newTCPAddr(t *testing.T) (addr, port string) { + portI := atomic.AddInt32(&curPort, 1) + require.Less(t, portI, int32(32768), "A new port should be less than ip_local_port_range.min") + + port = fmt.Sprintf("%d", portI) + addr = fmt.Sprintf("tcp://0.0.0.0:%s", port) + return +} + +func (f *Fixtures) LogResult(isSuccess bool, stdOut, stdErr string) { + if !isSuccess { + f.T.Error(stdErr) + } else { + f.T.Log(stdOut) + } +} + +func (f Fixtures) Clone() *Fixtures { + newF := NewFixtures(f.T) + newF.ChainID = f.ChainID + + tests.ExecuteT(newF.T, fmt.Sprintf("cp -r %s/ %s/", f.RootDir, newF.RootDir), "") + + return newF +} + +// GenesisFile returns the path of the genesis file +func (f Fixtures) GenesisFile() string { + return filepath.Join(f.LinkdHome, "config", "genesis.json") +} + +func (f Fixtures) PrivValidatorKeyFile() string { + return filepath.Join(f.LinkdHome, "config", "priv_validator_key.json") +} + +// GenesisFile returns the application's genesis state +func (f Fixtures) GenesisState() simapp.GenesisState { + cdc := codec.New() + genDoc, err := tmtypes.GenesisDocFromFile(f.GenesisFile()) + require.NoError(f.T, err) + + var appState simapp.GenesisState + require.NoError(f.T, cdc.UnmarshalJSON(genDoc.AppState, &appState)) + return appState +} + +// InitFixtures is called at the beginning of a test and initializes a chain +// with 1 validator. +func InitFixtures(t *testing.T) (f *Fixtures) { + f = NewFixtures(t) + + // reset test state + f.UnsafeResetAll() + + // ensure keystore has foo and bar keys + f.KeysDelete(keyFoo) + f.KeysDelete(keyBar) + f.KeysDelete(keyBaz) + f.KeysDelete(keyFooBarBaz) + f.KeysAdd(keyFoo) + f.KeysAdd(keyBar) + f.KeysAdd(keyBaz) + f.KeysAdd(keyVesting) + f.KeysAdd(keyFooBarBaz, "--multisig-threshold=2", fmt.Sprintf( + "--multisig=%s,%s,%s", keyFoo, keyBar, keyBaz)) + + // ensure keystore to have user keys + f.KeysDelete(UserTina) + f.KeysDelete(UserKevin) + f.KeysDelete(UserRinah) + f.KeysDelete(UserBrian) + f.KeysDelete(UserEvelyn) + f.KeysDelete(UserSam) + f.KeysAdd(UserTina) + f.KeysAdd(UserKevin) + f.KeysAdd(UserRinah) + f.KeysAdd(UserBrian) + f.KeysAdd(UserEvelyn) + f.KeysAdd(UserSam) + + // ensure that CLI output is in JSON format + f.CLIConfig("output", "json") + + // NOTE: LDInit sets the ChainID + f.LDInit(keyFoo) + + f.CLIConfig("chain-id", f.ChainID) + f.CLIConfig("broadcast-mode", "block") + f.CLIConfig("trust-node", "true") + f.CLIConfig("keyring-backend", "test") + + // start an account with tokens + f.AddGenesisAccount(f.KeyAddress(keyFoo), startCoins) + // f.AddGenesisAccount(f.KeyAddress(keyBar), startCoins) + f.AddGenesisAccount( + f.KeyAddress(keyVesting), startCoins, + fmt.Sprintf("--vesting-amount=%s", vestingCoins), + fmt.Sprintf("--vesting-start-time=%d", time.Now().UTC().UnixNano()), + fmt.Sprintf("--vesting-end-time=%d", time.Now().Add(60*time.Second).UTC().UnixNano()), + ) + + // add genesis accounts for testing + f.AddGenesisAccount(f.KeyAddress(UserTina), defaultCoins) + f.AddGenesisAccount(f.KeyAddress(UserKevin), defaultCoins) + f.AddGenesisAccount(f.KeyAddress(UserRinah), defaultCoins) + f.AddGenesisAccount(f.KeyAddress(UserBrian), defaultCoins) + f.AddGenesisAccount(f.KeyAddress(UserEvelyn), defaultCoins) + f.AddGenesisAccount(f.KeyAddress(UserSam), defaultCoins) + + f.GenTx(keyFoo) + f.CollectGenTxs() + + return f +} + +// Cleanup is meant to be run at the end of a test to clean up an remaining test state +func (f *Fixtures) Cleanup(dirs ...string) { + clean := append(dirs, f.RootDir) + for _, d := range clean { + _ = os.RemoveAll(d) + } +} + +// Flags returns the flags necessary for making most CLI calls +func (f *Fixtures) Flags() string { + return fmt.Sprintf("--home=%s --node=%s", f.LinkcliHome, f.RPCAddr) +} + +// ___________________________________________________________________________________ +// linkd + +// UnsafeResetAll is linkd unsafe-reset-all +func (f *Fixtures) UnsafeResetAll(flags ...string) { + cmd := fmt.Sprintf("%s --home=%s unsafe-reset-all", f.LinkdBinary, f.LinkdHome) + executeWrite(f.T, addFlags(cmd, flags)) + err := os.RemoveAll(filepath.Join(f.LinkdHome, "config", "gentx")) + require.NoError(f.T, err) +} + +// LDInit is linkd init +// NOTE: LDInit sets the ChainID for the Fixtures instance +func (f *Fixtures) LDInit(moniker string, flags ...string) { + f.Moniker = moniker + cmd := fmt.Sprintf("%s init -o --home=%s %s", f.LinkdBinary, f.LinkdHome, moniker) + _, stderr := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + + var chainID string + var initRes map[string]json.RawMessage + + err := json.Unmarshal([]byte(stderr), &initRes) + require.NoError(f.T, err) + + err = json.Unmarshal(initRes["chain_id"], &chainID) + require.NoError(f.T, err) + + f.ChainID = chainID +} + +// AddGenesisAccount is linkd add-genesis-account +func (f *Fixtures) AddGenesisAccount(address sdk.AccAddress, coins sdk.Coins, flags ...string) { + cmd := fmt.Sprintf("%s add-genesis-account %s %s --home=%s --keyring-backend=test", f.LinkdBinary, address, coins, f.LinkdHome) + executeWriteCheckErr(f.T, addFlags(cmd, flags)) +} + +// GenTx is linkd gentx +func (f *Fixtures) GenTx(name string, flags ...string) { + cmd := fmt.Sprintf("%s gentx --name=%s --home=%s --home-client=%s --keyring-backend=test", f.LinkdBinary, name, f.LinkdHome, f.LinkcliHome) + executeWriteCheckErr(f.T, addFlags(cmd, flags)) +} + +// CollectGenTxs is linkd collect-gentxs +func (f *Fixtures) CollectGenTxs(flags ...string) { + cmd := fmt.Sprintf("%s collect-gentxs --home=%s", f.LinkdBinary, f.LinkdHome) + executeWriteCheckErr(f.T, addFlags(cmd, flags)) +} + +// LDStart runs linkd start with the appropriate flags and returns a process +func (f *Fixtures) LDStart(flags ...string) *tests.Process { + cmd := fmt.Sprintf("%s start --home=%s --rpc.laddr=%v --p2p.laddr=%v", f.LinkdBinary, f.LinkdHome, f.RPCAddr, f.P2PAddr) + proc := tests.GoExecuteT(f.T, addFlags(cmd, flags)) + defer func() { + if v := recover(); v != nil { + stdout, stderr, err := proc.ReadAll() + require.NoError(f.T, err) + // Log for start command + f.T.Log(cmd, string(stdout)) + f.T.Log(cmd, string(stderr)) + f.T.Fatal(v) + } + }() + WaitForTMStart(f.Port) + tests.WaitForNextNBlocksTM(1, f.Port) + return proc +} + +// LDTendermint returns the results of linkd tendermint [query] +func (f *Fixtures) LDTendermint(query string) string { + cmd := fmt.Sprintf("%s tendermint %s --home=%s", f.LinkdBinary, query, f.LinkdHome) + success, stdout, stderr := executeWriteRetStdStreams(f.T, cmd) + require.Empty(f.T, stderr) + require.True(f.T, success) + return strings.TrimSpace(stdout) +} + +// ValidateGenesis runs linkd validate-genesis +func (f *Fixtures) ValidateGenesis() { + cmd := fmt.Sprintf("%s validate-genesis --home=%s", f.LinkdBinary, f.LinkdHome) + executeWriteCheckErr(f.T, cmd) +} + +// ___________________________________________________________________________________ +// linkcli rest-server +func (f *Fixtures) RestServerStart(port int, flags ...string) (*tests.Process, error) { + cmd := fmt.Sprintf("%s rest-server --home=%s --laddr=%s", f.LinkcliBinary, f.LinkcliHome, fmt.Sprintf("tcp://0.0.0.0:%d", port)) + proc := tests.GoExecuteTWithStdout(f.T, addFlags(cmd, flags)) + defer func() { + if v := recover(); v != nil { + stdout, stderr, err := proc.ReadAll() + if err != nil { + fmt.Println(err) + f.T.Fail() + } + f.T.Log(stdout) + f.T.Log(stderr) + } + }() + tests.WaitForNextNBlocksTM(1, f.Port) + return proc, nil +} + +// ___________________________________________________________________________________ +// linkcli keys + +// KeysDelete is linkcli keys delete +func (f *Fixtures) KeysDelete(name string, flags ...string) { + cmd := fmt.Sprintf("%s keys delete --keyring-backend=test --home=%s %s", f.LinkcliBinary, f.LinkcliHome, name) + executeWrite(f.T, addFlags(cmd, append(append(flags, "-y"), "-f"))) +} + +// KeysAdd is linkcli keys add +func (f *Fixtures) KeysAdd(name string, flags ...string) { + cmd := fmt.Sprintf("%s keys add --keyring-backend=test --home=%s %s", f.LinkcliBinary, f.LinkcliHome, name) + executeWriteCheckErr(f.T, addFlags(cmd, flags)) +} + +// KeysAddRecover prepares linkcli keys add --recover +func (f *Fixtures) KeysAddRecover(name, mnemonic string, flags ...string) (exitSuccess bool, stdout, stderr string) { + cmd := fmt.Sprintf("%s keys add --keyring-backend=test --home=%s --recover %s", f.LinkcliBinary, f.LinkcliHome, name) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), mnemonic) +} + +// KeysAddRecoverHDPath prepares linkcli keys add --recover --account --index +func (f *Fixtures) KeysAddRecoverHDPath(name, mnemonic string, account uint32, index uint32, flags ...string) { + cmd := fmt.Sprintf("%s keys add --keyring-backend=test --home=%s --recover %s --account %d --index %d", f.LinkcliBinary, f.LinkcliHome, name, account, index) + executeWriteCheckErr(f.T, addFlags(cmd, flags), mnemonic) +} + +// KeysShow is linkcli keys show +func (f *Fixtures) KeysShow(name string, flags ...string) keys.KeyOutput { + cmd := fmt.Sprintf("%s keys show --keyring-backend=test --home=%s %s", f.LinkcliBinary, f.LinkcliHome, name) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var ko keys.KeyOutput + err := clientkeys.UnmarshalJSON([]byte(out), &ko) + require.NoError(f.T, err) + return ko +} + +// KeyAddress returns the SDK account address from the key +func (f *Fixtures) KeyAddress(name string) sdk.AccAddress { + ko := f.KeysShow(name) + accAddr, err := sdk.AccAddressFromBech32(ko.Address) + require.NoError(f.T, err) + return accAddr +} + +// ___________________________________________________________________________________ +// linkcli config + +// CLIConfig is linkcli config +func (f *Fixtures) CLIConfig(key, value string, flags ...string) { + cmd := fmt.Sprintf("%s config --home=%s %s %s", f.LinkcliBinary, f.LinkcliHome, key, value) + executeWriteCheckErr(f.T, addFlags(cmd, flags)) +} + +// ___________________________________________________________________________________ +// linkcli tx send/sign/broadcast + +// TxSend is linkcli tx send +func (f *Fixtures) TxSend(from string, to sdk.AccAddress, amount sdk.Coin, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx send --keyring-backend=test %s %s %s %v", f.LinkcliBinary, from, to, amount, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// TxSign is linkcli tx sign +func (f *Fixtures) TxSign(signer, fileName string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx sign %v --keyring-backend=test --from=%s %v", f.LinkcliBinary, f.Flags(), signer, fileName) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// TxBroadcast is linkcli tx broadcast +func (f *Fixtures) TxBroadcast(fileName string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx broadcast %v %v", f.LinkcliBinary, f.Flags(), fileName) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// TxEncode is linkcli tx encode +func (f *Fixtures) TxEncode(fileName string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx encode %v %v", f.LinkcliBinary, f.Flags(), fileName) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// TxMultisign is linkcli tx multisign +func (f *Fixtures) TxMultisign(fileName, name string, signaturesFiles []string, + flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx multisign --keyring-backend=test %v %s %s %s", f.LinkcliBinary, f.Flags(), + fileName, name, strings.Join(signaturesFiles, " "), + ) + return executeWriteRetStdStreams(f.T, cmd) +} + +// ___________________________________________________________________________________ +// linkcli tx staking + +// TxStakingCreateValidator is linkcli tx staking create-validator +func (f *Fixtures) TxStakingCreateValidator(from, consPubKey string, amount sdk.Coin, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx staking create-validator %v --keyring-backend=test --from=%s --pubkey=%s", f.LinkcliBinary, f.Flags(), from, consPubKey) + cmd += fmt.Sprintf(" --amount=%v --moniker=%v --commission-rate=%v", amount, from, "0.05") + cmd += fmt.Sprintf(" --commission-max-rate=%v --commission-max-change-rate=%v", "0.20", "0.10") + cmd += fmt.Sprintf(" --min-self-delegation=%v", "1") + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// TxStakingUnbond is linkcli tx staking unbond +func (f *Fixtures) TxStakingUnbond(from, shares string, validator sdk.ValAddress, flags ...string) bool { + cmd := fmt.Sprintf("%s tx staking unbond --keyring-backend=test %s %v --from=%s %v", f.LinkcliBinary, validator, shares, from, f.Flags()) + return executeWrite(f.T, addFlags(cmd, flags)) +} + +// ___________________________________________________________________________________ +// linkcli tx gov + +// TxGovSubmitProposal is linkcli tx gov submit-proposal +func (f *Fixtures) TxGovSubmitProposal(from, typ, title, description string, deposit sdk.Coin, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx gov submit-proposal %v --keyring-backend=test --from=%s --type=%s", f.LinkcliBinary, f.Flags(), from, typ) + cmd += fmt.Sprintf(" --title=%s --description=%s --deposit=%s", title, description, deposit) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// TxGovDeposit is linkcli tx gov deposit +func (f *Fixtures) TxGovDeposit(proposalID int, from string, amount sdk.Coin, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx gov deposit %d %s --keyring-backend=test --from=%s %v", f.LinkcliBinary, proposalID, amount, from, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// TxGovVote is linkcli tx gov vote +func (f *Fixtures) TxGovVote(proposalID int, option gov.VoteOption, from string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx gov vote %d %s --keyring-backend=test --from=%s %v", f.LinkcliBinary, proposalID, option, from, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// TxGovSubmitParamChangeProposal executes a CLI parameter change proposal +// submission. +func (f *Fixtures) TxGovSubmitParamChangeProposal( + from, proposalPath string, deposit sdk.Coin, flags ...string, +) (bool, string, string) { + cmd := fmt.Sprintf( + "%s tx gov submit-proposal param-change %s --keyring-backend=test --from=%s %v", + f.LinkcliBinary, proposalPath, from, f.Flags(), + ) + + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// TxGovSubmitCommunityPoolSpendProposal executes a CLI community pool spend proposal +// submission. +func (f *Fixtures) TxGovSubmitCommunityPoolSpendProposal( + from, proposalPath string, deposit sdk.Coin, flags ...string, +) (bool, string, string) { + cmd := fmt.Sprintf( + "%s tx gov submit-proposal community-pool-spend %s --keyring-backend=test --from=%s %v", + f.LinkcliBinary, proposalPath, from, f.Flags(), + ) + + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// ___________________________________________________________________________________ +// linkcli tx token + +func (f *Fixtures) TxTokenIssue(from string, to sdk.AccAddress, name, meta string, symbol string, amount int64, decimals int64, mintable bool, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token issue %s %s %s %s --total-supply=%d --decimals=%d --mintable=%t --meta=%s %v", f.LinkcliBinary, from, to.String(), name, symbol, amount, decimals, mintable, meta, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} +func (f *Fixtures) TxTokenMint(from string, contractID string, to string, amount string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token mint %s %s %s %s %v", f.LinkcliBinary, from, contractID, to, amount, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenBurn(from, contractID, amount string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token burn %s %s %s %v", f.LinkcliBinary, from, contractID, amount, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenBurnFrom(proxy, contractID string, from sdk.AccAddress, amount int64, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token burn-from %s %s %s %d %v", f.LinkcliBinary, proxy, contractID, from, amount, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenGrantPerm(from string, to string, contractID, action string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token grant %s %s %s %s %v", f.LinkcliBinary, from, contractID, to, action, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenRevokePerm(from string, contractID, action string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token revoke %s %s %s %v", f.LinkcliBinary, from, contractID, action, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenModify(owner, contractID, field, newValue string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token modify %s %s %s %s %v", f.LinkcliBinary, owner, contractID, field, newValue, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenTransfer(from string, to sdk.AccAddress, symbol string, amount int64, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token transfer %s %s %s %d %v", f.LinkcliBinary, from, to, symbol, amount, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenTransferFrom(proxy string, contractID string, from sdk.AccAddress, to sdk.AccAddress, amount int64, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token transfer-from %s %s %s %s %d %v", f.LinkcliBinary, proxy, contractID, from, to, amount, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenApprove(approver string, contractID string, proxyAddress sdk.AccAddress, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx token approve %s %s %s %v", f.LinkcliBinary, approver, contractID, proxyAddress, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// ___________________________________________________________________________________ +// linkcli tx collection + +func (f *Fixtures) TxTokenCreateCollection(from string, name, meta, baseImgURI string, flags ...string) (bool, string, + string) { + cmd := fmt.Sprintf("%s tx collection create %s %s %s %s %v", f.LinkcliBinary, from, name, meta, baseImgURI, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} +func (f *Fixtures) TxTokenIssueFTCollection(from string, contractID string, to sdk.AccAddress, name, meta string, amount int64, decimals int64, mintable bool, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection issue-ft %s %s %s %s %s --total-supply=%d --decimals=%d --mintable=%t %v", f.LinkcliBinary, from, contractID, to.String(), name, meta, amount, decimals, mintable, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenMintFTCollection(from string, contractID string, to string, amount string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection mint-ft %s %s %s %s %v", f.LinkcliBinary, from, contractID, to, amount, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenBurnFTCollection(from string, contractID, tokenID string, amount int64, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection burn-ft %s %s %s %d %v", f.LinkcliBinary, from, contractID, tokenID, amount, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenTransferFTCollection(from string, contractID string, to string, amount string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection transfer-ft %s %s %s %s %v", f.LinkcliBinary, from, contractID, to, amount, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass) +} + +func (f *Fixtures) TxTokenIssueNFTCollection(from string, contractID, name, meta string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection issue-nft %s %s %s %s %v", f.LinkcliBinary, from, contractID, name, meta, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenMintNFTCollection(from string, contractID string, to string, mintNFTParam string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection mint-nft %s %s %s %s %v", f.LinkcliBinary, from, contractID, to, mintNFTParam, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenBurnNFTCollection(from string, contractID, tokenID string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection burn-nft %s %s %s %v", f.LinkcliBinary, from, contractID, tokenID, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxTokenTransferNFTCollection(from string, contractID string, to string, tokenID string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection transfer-nft %s %s %s %s %v", f.LinkcliBinary, from, contractID, to, tokenID, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), clientkeys.DefaultKeyPass) +} + +func (f *Fixtures) TxCollectionModify(owner, contractID, tokenType, tokenIndex, field, newValue string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection modify %s %s %s %s --token-type %s --token-index %s %v", + f.LinkcliBinary, owner, contractID, field, newValue, tokenType, tokenIndex, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxCollectionGrantPerm(from string, to sdk.AccAddress, contractID, action string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection grant %s %s %s %s %v", f.LinkcliBinary, from, contractID, to, action, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxCollectionRevokePerm(from string, contractID, action string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection revoke %s %s %s %v", f.LinkcliBinary, from, contractID, action, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxCollectionApprove(approver string, contractID string, proxyAd sdk.AccAddress, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx collection approve %s %s %s %v", f.LinkcliBinary, approver, contractID, proxyAd, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxEmpty(from string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx empty %s %v", f.LinkcliBinary, from, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +// ___________________________________________________________________________________ +// linkcli query account + +// QueryAccount is linkcli query account +func (f *Fixtures) QueryAccount(address sdk.AccAddress, flags ...string) auth.BaseAccount { + cmd := fmt.Sprintf("%s query account %s %v", f.LinkcliBinary, address, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var initRes map[string]json.RawMessage + err := json.Unmarshal([]byte(out), &initRes) + require.NoError(f.T, err, "out %v, err %v", out, err) + value := initRes["value"] + var acc auth.BaseAccount + cdc := codec.New() + codec.RegisterCrypto(cdc) + err = cdc.UnmarshalJSON(value, &acc) + require.NoError(f.T, err, "value %v, err %v", string(value), err) + return acc +} + +// ___________________________________________________________________________________ +// linkcli query tx + +// QueryTx is linkcli query tx +func (f *Fixtures) QueryTx(hash string) *sdk.TxResponse { + cmd := fmt.Sprintf("%s query tx %s %v", f.LinkcliBinary, hash, f.Flags()) + out, _ := tests.ExecuteT(f.T, cmd, "") + var result sdk.TxResponse + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &result) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return &result +} + +// QueryTxInvalid query tx with wrong hash and compare expected error +func (f *Fixtures) QueryTxInvalid(expectedErr error, hash string) { + cmd := fmt.Sprintf("%s query tx %s %v", f.LinkcliBinary, hash, f.Flags()) + _, err := tests.ExecuteT(f.T, cmd, "") + require.EqualError(f.T, expectedErr, err) +} + +// ___________________________________________________________________________________ +// linkcli query txs + +// QueryTxs is linkcli query txs +func (f *Fixtures) QueryTxs(page, limit int, flags ...string) *sdk.SearchTxsResult { + cmd := fmt.Sprintf("%s query txs --page=%d --limit=%d %v", f.LinkcliBinary, page, limit, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var result sdk.SearchTxsResult + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &result) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return &result +} + +// QueryTxsInvalid query txs with wrong parameters and compare expected error +func (f *Fixtures) QueryTxsInvalid(expectedErr error, page, limit int, flags ...string) { + cmd := fmt.Sprintf("%s query txs --page=%d --limit=%d %v", f.LinkcliBinary, page, limit, f.Flags()) + _, err := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + require.EqualError(f.T, expectedErr, err) +} + +// ___________________________________________________________________________________ +// linkcli query block + +func (f *Fixtures) QueryLatestBlock(flags ...string) *tmctypes.ResultBlock { + cmd := fmt.Sprintf("%s query block %v", f.LinkcliBinary, f.Flags()) + out, _ := tests.ExecuteT(f.T, cmd, "") + var result tmctypes.ResultBlock + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &result) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return &result +} + +func (f *Fixtures) QueryBlockWithHeight(height int, flags ...string) *tmctypes.ResultBlock { + cmd := fmt.Sprintf("%s query block %d %v", f.LinkcliBinary, height, f.Flags()) + out, _ := tests.ExecuteT(f.T, cmd, "") + var result tmctypes.ResultBlock + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &result) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return &result +} + +// ___________________________________________________________________________________ +// linkcli query staking + +// QueryStakingValidator is linkcli query staking validator +func (f *Fixtures) QueryStakingValidator(valAddr sdk.ValAddress, flags ...string) staking.Validator { + cmd := fmt.Sprintf("%s query staking validator %s %v", f.LinkcliBinary, valAddr, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var validator staking.Validator + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &validator) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return validator +} + +// QueryStakingUnbondingDelegationsFrom is linkcli query staking unbonding-delegations-from +func (f *Fixtures) QueryStakingUnbondingDelegationsFrom(valAddr sdk.ValAddress, flags ...string) []staking.UnbondingDelegation { + cmd := fmt.Sprintf("%s query staking unbonding-delegations-from %s %v", f.LinkcliBinary, valAddr, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var ubds []staking.UnbondingDelegation + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &ubds) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return ubds +} + +// QueryStakingDelegationsTo is linkcli query staking delegations-to +func (f *Fixtures) QueryStakingDelegationsTo(valAddr sdk.ValAddress, flags ...string) []staking.Delegation { + cmd := fmt.Sprintf("%s query staking delegations-to %s %v", f.LinkcliBinary, valAddr, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var delegations []staking.Delegation + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &delegations) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return delegations +} + +// QueryStakingPool is linkcli query staking pool +func (f *Fixtures) QueryStakingPool(flags ...string) staking.Pool { + cmd := fmt.Sprintf("%s query staking pool %v", f.LinkcliBinary, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var pool staking.Pool + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &pool) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return pool +} + +// QueryStakingParameters is linkcli query staking parameters +func (f *Fixtures) QueryStakingParameters(flags ...string) staking.Params { + cmd := fmt.Sprintf("%s query staking params %v", f.LinkcliBinary, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var params staking.Params + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), ¶ms) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return params +} + +// ___________________________________________________________________________________ +// linkcli query gov + +// QueryGovParamDeposit is linkcli query gov param deposit +func (f *Fixtures) QueryGovParamDeposit() gov.DepositParams { + cmd := fmt.Sprintf("%s query gov param deposit %s", f.LinkcliBinary, f.Flags()) + out, _ := tests.ExecuteT(f.T, cmd, "") + var depositParam gov.DepositParams + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &depositParam) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return depositParam +} + +// QueryGovParamVoting is linkcli query gov param voting +func (f *Fixtures) QueryGovParamVoting() gov.VotingParams { + cmd := fmt.Sprintf("%s query gov param voting %s", f.LinkcliBinary, f.Flags()) + out, _ := tests.ExecuteT(f.T, cmd, "") + var votingParam gov.VotingParams + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &votingParam) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return votingParam +} + +// QueryGovParamTallying is linkcli query gov param tallying +func (f *Fixtures) QueryGovParamTallying() gov.TallyParams { + cmd := fmt.Sprintf("%s query gov param tallying %s", f.LinkcliBinary, f.Flags()) + out, _ := tests.ExecuteT(f.T, cmd, "") + var tallyingParam gov.TallyParams + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &tallyingParam) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return tallyingParam +} + +// QueryGovProposals is linkcli query gov proposals +func (f *Fixtures) QueryGovProposals(flags ...string) gov.Proposals { + cmd := fmt.Sprintf("%s query gov proposals %v", f.LinkcliBinary, f.Flags()) + stdout, stderr := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + if strings.Contains(stderr, "No matching proposals found") { + return gov.Proposals{} + } + require.Empty(f.T, stderr) + var out gov.Proposals + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(stdout), &out) + require.NoError(f.T, err) + return out +} + +// QueryGovProposal is linkcli query gov proposal +func (f *Fixtures) QueryGovProposal(proposalID int, flags ...string) gov.Proposal { + cmd := fmt.Sprintf("%s query gov proposal %d %v", f.LinkcliBinary, proposalID, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var proposal gov.Proposal + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &proposal) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return proposal +} + +// QueryGovVote is linkcli query gov vote +func (f *Fixtures) QueryGovVote(proposalID int, voter sdk.AccAddress, flags ...string) gov.Vote { + cmd := fmt.Sprintf("%s query gov vote %d %s %v", f.LinkcliBinary, proposalID, voter, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var vote gov.Vote + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &vote) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return vote +} + +// QueryGovVotes is linkcli query gov votes +func (f *Fixtures) QueryGovVotes(proposalID int, flags ...string) []gov.Vote { + cmd := fmt.Sprintf("%s query gov votes %d %v", f.LinkcliBinary, proposalID, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var votes []gov.Vote + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &votes) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return votes +} + +// QueryGovDeposit is linkcli query gov deposit +func (f *Fixtures) QueryGovDeposit(proposalID int, depositor sdk.AccAddress, flags ...string) gov.Deposit { + cmd := fmt.Sprintf("%s query gov deposit %d %s %v", f.LinkcliBinary, proposalID, depositor, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var deposit gov.Deposit + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &deposit) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return deposit +} + +// QueryGovDeposits is linkcli query gov deposits +func (f *Fixtures) QueryGovDeposits(propsalID int, flags ...string) []gov.Deposit { + cmd := fmt.Sprintf("%s query gov deposits %d %v", f.LinkcliBinary, propsalID, f.Flags()) + out, _ := tests.ExecuteT(f.T, addFlags(cmd, flags), "") + var deposits []gov.Deposit + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(out), &deposits) + require.NoError(f.T, err, "out %v\n, err %v", out, err) + return deposits +} + +// ___________________________________________________________________________________ +// query slashing + +// QuerySigningInfo returns the signing info for a validator +func (f *Fixtures) QuerySigningInfo(val string) slashing.ValidatorSigningInfo { + cmd := fmt.Sprintf("%s query slashing signing-info %s %s", f.LinkcliBinary, val, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var sinfo slashing.ValidatorSigningInfo + err := cdc.UnmarshalJSON([]byte(res), &sinfo) + require.NoError(f.T, err) + return sinfo +} + +// QuerySlashingParams is linkcli query slashing params +func (f *Fixtures) QuerySlashingParams() slashing.Params { + cmd := fmt.Sprintf("%s query slashing params %s", f.LinkcliBinary, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var params slashing.Params + err := cdc.UnmarshalJSON([]byte(res), ¶ms) + require.NoError(f.T, err) + return params +} + +// ___________________________________________________________________________________ +// query distribution + +// QueryRewards returns the rewards of a delegator +func (f *Fixtures) QueryRewards(delAddr sdk.AccAddress, flags ...string) distribution.QueryDelegatorTotalRewardsResponse { + cmd := fmt.Sprintf("%s query distribution rewards %s %s", f.LinkcliBinary, delAddr, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var rewards distribution.QueryDelegatorTotalRewardsResponse + err := cdc.UnmarshalJSON([]byte(res), &rewards) + require.NoError(f.T, err) + return rewards +} + +// ___________________________________________________________________________________ +// query supply + +// QueryTotalSupply returns the total supply of coins +func (f *Fixtures) QueryTotalSupply(flags ...string) (totalSupply sdk.Coins) { + cmd := fmt.Sprintf("%s query supply total %s", f.LinkcliBinary, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + err := cdc.UnmarshalJSON([]byte(res), &totalSupply) + require.NoError(f.T, err) + return totalSupply +} + +// QueryTotalSupplyOf returns the total supply of a given coin denom +func (f *Fixtures) QueryTotalSupplyOf(denom string, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query supply total %s %s", f.LinkcliBinary, denom, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supplyOf sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supplyOf) + require.NoError(f.T, err) + return supplyOf +} + +// ___________________________________________________________________________________ +// query token + +func (f *Fixtures) QueryToken(contractID string, flags ...string) tokenModule.Token { + cmd := fmt.Sprintf("%s query token token %s %s", f.LinkcliBinary, contractID, f.Flags()) + cmd = addFlags(cmd, flags) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var token tokenModule.Token + err := cdc.UnmarshalJSON([]byte(res), &token) + require.NoError(f.T, err) + return token +} + +func (f *Fixtures) QueryTokenExpectEmpty(contractID string, flags ...string) { + cmd := fmt.Sprintf("%s query token token %s %s", f.LinkcliBinary, contractID, f.Flags()) + cmd = addFlags(cmd, flags) + _, errStr := tests.ExecuteT(f.T, cmd, "") + require.NotEmpty(f.T, errStr) +} + +func (f *Fixtures) QueryBalanceToken(contractID string, addr sdk.AccAddress, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query token balance %s %s %s", f.LinkcliBinary, contractID, addr.String(), f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supply sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supply) + require.NoError(f.T, err) + + return supply +} + +func (f *Fixtures) QuerySupplyToken(contractID string, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query token total supply %s %s", f.LinkcliBinary, contractID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supply sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supply) + require.NoError(f.T, err) + + return supply +} + +func (f *Fixtures) QueryMintToken(contractID string, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query token total mint %s %s", f.LinkcliBinary, contractID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supply sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supply) + require.NoError(f.T, err) + + return supply +} + +func (f *Fixtures) QueryBurnToken(contractID string, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query token total burn %s %s", f.LinkcliBinary, contractID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supply sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supply) + require.NoError(f.T, err) + + return supply +} + +func (f *Fixtures) QueryAccountPermission(addr sdk.AccAddress, contractID string, flags ...string) tokenModule.Permissions { + cmd := fmt.Sprintf("%s query token perm %s %s %s", f.LinkcliBinary, addr, contractID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var pms tokenModule.Permissions + err := cdc.UnmarshalJSON([]byte(res), &pms) + require.NoError(f.T, err) + return pms +} + +func (f *Fixtures) QueryApprovedToken(contractID string, proxy sdk.AccAddress, approver sdk.AccAddress, flags ...string) bool { + cmd := fmt.Sprintf("%s query token approved %s %s %s %s", f.LinkcliBinary, contractID, proxy, approver, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var isApproved bool + err := cdc.UnmarshalJSON([]byte(res), &isApproved) + require.NoError(f.T, err) + return isApproved +} + +// ___________________________________________________________________________________ +// query collection +func (f *Fixtures) QueryBalancesCollection(contractID string, addr sdk.AccAddress) collectionModule.Coins { + cmd := fmt.Sprintf("%s query collection balances %s %s %s", f.LinkcliBinary, contractID, addr.String(), f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var coins collectionModule.Coins + err := cdc.UnmarshalJSON([]byte(res), &coins) + require.NoError(f.T, err) + + return coins +} + +func (f *Fixtures) QueryBalanceCollection(contractID, tokenID string, addr sdk.AccAddress, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query collection balance %s %s %s %s", f.LinkcliBinary, contractID, tokenID, addr.String(), f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supply sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supply) + require.NoError(f.T, err) + + return supply +} + +func (f *Fixtures) QueryTokenCollection(contractID, tokenID string, flags ...string) collectionModule.Token { + cmd := fmt.Sprintf("%s query collection token %s %s %s", f.LinkcliBinary, contractID, tokenID, f.Flags()) + cmd = addFlags(cmd, flags) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var token collectionModule.Token + err := cdc.UnmarshalJSON([]byte(res), &token) + require.NoError(f.T, err) + return token +} + +func (f *Fixtures) QueryTokenCollectionExpectEmpty(contractID, tokenID string, flags ...string) { + cmd := fmt.Sprintf("%s query collection token %s %s %s", f.LinkcliBinary, contractID, tokenID, f.Flags()) + cmd = addFlags(cmd, flags) + _, errStr := tests.ExecuteT(f.T, cmd, "") + require.NotEmpty(f.T, errStr) +} + +func (f *Fixtures) QueryTokensCollection(contractID string) collectionModule.Tokens { + cmd := fmt.Sprintf("%s query collection tokens %s %s", f.LinkcliBinary, contractID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var tokens collectionModule.Tokens + err := cdc.UnmarshalJSON([]byte(res), &tokens) + require.NoError(f.T, err) + return tokens +} + +func (f *Fixtures) QueryTokensByTokenTypeCollection(contractID string, tokenType string) collectionModule.Tokens { + cmd := fmt.Sprintf("%s query collection tokens %s --token-type %s %s", f.LinkcliBinary, contractID, tokenType, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var tokens collectionModule.Tokens + err := cdc.UnmarshalJSON([]byte(res), &tokens) + require.NoError(f.T, err) + return tokens +} + +func (f *Fixtures) QueryTokenTypeCollection(contractID, tokenTypeID string, flags ...string) collectionModule.TokenType { + cmd := fmt.Sprintf("%s query collection tokentype %s %s %s", f.LinkcliBinary, contractID, tokenTypeID, f.Flags()) + cmd = addFlags(cmd, flags) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var tokenType collectionModule.TokenType + err := cdc.UnmarshalJSON([]byte(res), &tokenType) + require.NoError(f.T, err) + return tokenType +} + +func (f *Fixtures) QueryCollection(contractID string, flags ...string) collectionModule.Collection { + cmd := fmt.Sprintf("%s query collection collection %s %s", f.LinkcliBinary, contractID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var collection collectionModule.Collection + err := cdc.UnmarshalJSON([]byte(res), &collection) + require.NoError(f.T, err) + + return collection +} + +func (f *Fixtures) QueryTotalSupplyTokenCollection(contractID, tokenID string, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query collection total supply %s %s %s", f.LinkcliBinary, contractID, tokenID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supply sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supply) + require.NoError(f.T, err) + + return supply +} +func (f *Fixtures) QueryTotalMintTokenCollection(contractID, tokenID string, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query collection total mint %s %s %s", f.LinkcliBinary, contractID, tokenID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supply sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supply) + require.NoError(f.T, err) + + return supply +} +func (f *Fixtures) QueryTotalBurnTokenCollection(contractID, tokenID string, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query collection total burn %s %s %s", f.LinkcliBinary, contractID, tokenID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supply sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supply) + require.NoError(f.T, err) + + return supply +} +func (f *Fixtures) QueryCountTokenCollection(contractID, tokenID string, flags ...string) sdk.Int { + cmd := fmt.Sprintf("%s query collection count total %s %s %s", f.LinkcliBinary, contractID, tokenID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var supply sdk.Int + err := cdc.UnmarshalJSON([]byte(res), &supply) + require.NoError(f.T, err) + + return supply +} + +func (f *Fixtures) QueryAccountPermissionCollection(addr sdk.AccAddress, contractID string, flags ...string) collectionModule.Permissions { + cmd := fmt.Sprintf("%s query collection perm %s %s %s", f.LinkcliBinary, addr, contractID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var pms collectionModule.Permissions + err := cdc.UnmarshalJSON([]byte(res), &pms) + require.NoError(f.T, err) + return pms +} + +func (f *Fixtures) QueryRootTokenCollection(contractID string, tokenID string, flags ...string) collectionModule.Token { + cmd := fmt.Sprintf("%s query collection root %s %s %s", f.LinkcliBinary, contractID, tokenID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var root collectionModule.Token + err := cdc.UnmarshalJSON([]byte(res), &root) + require.NoError(f.T, err) + return root +} + +func (f *Fixtures) QueryParentTokenCollection(contractID string, tokenID string, flags ...string) collectionModule.Token { + cmd := fmt.Sprintf("%s query collection parent %s %s %s", f.LinkcliBinary, contractID, tokenID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var parent collectionModule.Token + err := cdc.UnmarshalJSON([]byte(res), &parent) + require.NoError(f.T, err) + return parent +} + +func (f *Fixtures) QueryChildrenTokenCollection(contractID string, tokenID string, flags ...string) []collectionModule.Token { + cmd := fmt.Sprintf("%s query collection children %s %s %s", f.LinkcliBinary, contractID, tokenID, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var children []collectionModule.Token + err := cdc.UnmarshalJSON([]byte(res), &children) + require.NoError(f.T, err) + return children +} + +func (f *Fixtures) QueryApproversTokenCollection(contractID string, proxy sdk.AccAddress) []sdk.AccAddress { + cmd := fmt.Sprintf("%s query collection approvers %s %s %s", f.LinkcliBinary, contractID, proxy, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var approvers []sdk.AccAddress + err := cdc.UnmarshalJSON([]byte(res), &approvers) + require.NoError(f.T, err) + return approvers +} + +func (f *Fixtures) QueryApprovedTokenCollection(contractID string, proxy sdk.AccAddress, approver sdk.AccAddress) bool { + cmd := fmt.Sprintf("%s query collection approved %s %s %s %s", f.LinkcliBinary, contractID, proxy, approver, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var isApproved bool + err := cdc.UnmarshalJSON([]byte(res), &isApproved) + require.NoError(f.T, err) + return isApproved +} + +// ___________________________________________________________________________________ +// wasm +func (f *Fixtures) TxStoreWasm(wasmFilePath string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx wasm store %s %v", f.LinkcliBinary, wasmFilePath, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxInstantiateWasm(codeId uint64, msgJson string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx wasm instantiate %d %s %v", f.LinkcliBinary, codeId, msgJson, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxExecuteWasm(contractAddress sdk.AccAddress, msgJson string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx wasm execute %s %s %s", f.LinkcliBinary, contractAddress, msgJson, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxMigrateWasm(contractAddress sdk.AccAddress, codeId uint64, msgJson string, flags ...string) (bool, string, string) { + require.Fail(f.T, "TODO: Test It!") + cmd := fmt.Sprintf("%s tx wasm migrate %s %d %s %s", f.LinkcliBinary, contractAddress, codeId, msgJson, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxSetContractAdminWasm(contractAddress sdk.AccAddress, newAdmin sdk.AccAddress, flags ...string) (bool, string, string) { + require.Fail(f.T, "TODO: Test It!") + cmd := fmt.Sprintf("%s tx wasm set-contract-admin %s %s %s", f.LinkcliBinary, contractAddress, newAdmin, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) TxClearContractAdminWasm(contractAddress sdk.AccAddress, flags ...string) (bool, string, string) { + require.Fail(f.T, "TODO: Test It!") + cmd := fmt.Sprintf("%s tx wasm clear-contract-admin %s %s", f.LinkcliBinary, contractAddress, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags)) +} + +func (f *Fixtures) QueryListCodeWasm() []wasmtypes.CodeInfoResponse { + cmd := fmt.Sprintf("%s query wasm list-code %s", f.LinkcliBinary, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var listCode []wasmtypes.CodeInfoResponse + err := cdc.UnmarshalJSON([]byte(res), &listCode) + require.NoError(f.T, err) + return listCode +} + +func (f *Fixtures) QueryListContractByCodeWasm(codeId uint64) []wasmtypes.ContractInfoResponse { + cmd := fmt.Sprintf("%s query wasm list-contract-by-code %d %s", f.LinkcliBinary, codeId, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var listContract []wasmtypes.ContractInfoResponse + err := cdc.UnmarshalJSON([]byte(res), &listContract) + require.NoError(f.T, err) + return listContract +} + +func (f *Fixtures) QueryCodeWasm(codeId uint64, outputPath string) { + cmd := fmt.Sprintf("%s query wasm code %d %s %s", f.LinkcliBinary, codeId, outputPath, f.Flags()) + _, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) +} + +func (f *Fixtures) QueryContractWasm(contractAddress sdk.AccAddress) wasmtypes.ContractInfoResponse { + cmd := fmt.Sprintf("%s query wasm contract %s %s", f.LinkcliBinary, contractAddress, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var contractInfo wasmtypes.ContractInfoResponse + err := cdc.UnmarshalJSON([]byte(res), &contractInfo) + require.NoError(f.T, err) + return contractInfo +} + +func (f *Fixtures) QueryContractHistoryWasm(contractAddress sdk.AccAddress) wasmtypes.ContractCodeHistoryEntry { + require.Fail(f.T, "TODO: Test It!") + cmd := fmt.Sprintf("%s query wasm contract-history %s %s", f.LinkcliBinary, contractAddress, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var contractHistory wasmtypes.ContractCodeHistoryEntry + err := cdc.UnmarshalJSON([]byte(res), &contractHistory) + require.NoError(f.T, err) + return contractHistory +} + +func (f *Fixtures) QueryContractStateAllWasm(contractAddress sdk.AccAddress) []wasmtypes.Model { + require.Fail(f.T, "TODO: Test It!") + cmd := fmt.Sprintf("%s query wasm contract-state all %s %s", f.LinkcliBinary, contractAddress, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var state []wasmtypes.Model + err := cdc.UnmarshalJSON([]byte(res), &state) + require.NoError(f.T, err) + return state +} + +func (f *Fixtures) QueryContractStateRawWasm(contractAddress sdk.AccAddress, key string) []wasmtypes.Model { + require.Fail(f.T, "TODO: Test It!") + cmd := fmt.Sprintf("%s query wasm contract-state raw %s %s %s", f.LinkcliBinary, contractAddress, key, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + cdc := app.MakeCodec() + var state []wasmtypes.Model + err := cdc.UnmarshalJSON([]byte(res), &state) + require.NoError(f.T, err) + return state +} + +func (f *Fixtures) QueryContractStateSmartWasm(contractAddress sdk.AccAddress, reqJson string) string { + cmd := fmt.Sprintf("%s query wasm contract-state smart %s %s %s", f.LinkcliBinary, contractAddress, reqJson, f.Flags()) + res, errStr := tests.ExecuteT(f.T, cmd, "") + require.Empty(f.T, errStr) + return res +} + +// ___________________________________________________________________________________ +// tendermint rpc +func (f *Fixtures) NetInfo(flags ...string) *tmctypes.ResultNetInfo { + tmc, err := tmhttp.New(fmt.Sprintf("tcp://0.0.0.0:%s", f.Port), "/websocket") + if err != nil { + panic(fmt.Sprintf("failed to create Tendermint HTTP client: %s", err)) + } + + err = tmc.Start() + require.NoError(f.T, err) + defer func() { + err := tmc.Stop() + require.NoError(f.T, err) + }() + + netInfo, err := tmc.NetInfo() + require.NoError(f.T, err) + return netInfo +} + +func (f *Fixtures) Status(flags ...string) *tmctypes.ResultStatus { + tmc, err := tmhttp.New(fmt.Sprintf("tcp://0.0.0.0:%s", f.Port), "/websocket") + if err != nil { + panic(fmt.Sprintf("failed to create Tendermint HTTP client: %s", err)) + } + + err = tmc.Start() + require.NoError(f.T, err) + defer func() { + err := tmc.Stop() + require.NoError(f.T, err) + }() + + netInfo, err := tmc.Status() + require.NoError(f.T, err) + return netInfo +} + +// ___________________________________________________________________________________ +// executors + +func executeWriteCheckErr(t *testing.T, cmdStr string, writes ...string) { + require.True(t, executeWrite(t, cmdStr, writes...)) +} + +func executeWrite(t *testing.T, cmdStr string, writes ...string) (exitSuccess bool) { + exitSuccess, _, _ = executeWriteRetStdStreams(t, cmdStr, writes...) + return +} + +func executeWriteRetStdStreams(t *testing.T, cmdStr string, writes ...string) (bool, string, string) { + proc := tests.GoExecuteT(t, cmdStr) + + // Enables use of interactive commands + for _, write := range writes { + _, err := proc.StdinPipe.Write([]byte(write + "\n")) + require.NoError(t, err) + } + + // Read both stdout and stderr from the process + stdout, stderr, err := proc.ReadAll() + if err != nil { + fmt.Println("Err on proc.ReadAll()", err, cmdStr) + } + + // Log output. + if len(stdout) > 0 { + t.Log("Stdout:", string(stdout)) + } + if len(stderr) > 0 { + t.Log("Stderr:", string(stderr)) + } + + // Wait for process to exit + proc.Wait() + + // Return succes, stdout, stderr + return proc.ExitState.Success(), string(stdout), string(stderr) +} + +// ___________________________________________________________________________________ +// utils + +func addFlags(cmd string, flags []string) string { + for _, f := range flags { + cmd += " " + f + } + return strings.TrimSpace(cmd) +} + +// Write the given string to a new temporary file +func WriteToNewTempFile(t *testing.T, s string) *os.File { + fp, err := ioutil.TempFile(os.TempDir(), "cosmos_cli_test_") + require.Nil(t, err) + _, err = fp.WriteString(s) + require.Nil(t, err) + return fp +} + +func MarshalStdTx(t *testing.T, stdTx auth.StdTx) []byte { + cdc := app.MakeCodec() + bz, err := cdc.MarshalBinaryBare(stdTx) + require.NoError(t, err) + return bz +} + +func UnmarshalStdTx(t *testing.T, s string) (stdTx auth.StdTx) { + cdc := app.MakeCodec() + require.Nil(t, cdc.UnmarshalJSON([]byte(s), &stdTx)) + return +} + +func UnmarshalTxResponse(t *testing.T, s string) (txResp sdk.TxResponse) { + cdc := app.MakeCodec() + require.Nil(t, cdc.UnmarshalJSON([]byte(s), &txResp)) + return +} + +// ___________________________________________________________________________________ +// Fixture Group + +type FixtureGroup struct { + T *testing.T + DockerImage string + fixturesMap map[string]*Fixtures + networkName string + subnet string + genesisFileContent []byte +} + +func NewFixtureGroup(t *testing.T) *FixtureGroup { + fg := &FixtureGroup{ + T: t, + DockerImage: "line/link", + fixturesMap: make(map[string]*Fixtures), + } + + fg.networkName = networkNamePrefix + t.Name() + + return fg +} + +func InitFixturesGroup(t *testing.T, subnet string, numOfNodes ...int) *FixtureGroup { + nodeNumber := 4 + if len(numOfNodes) == 1 { + nodeNumber = numOfNodes[0] + } + fg := NewFixtureGroup(t) + fg.initNodes(subnet, nodeNumber) + fg.createNetwork() + return fg +} + +func (fg *FixtureGroup) createNetwork() { + cmd := fmt.Sprintf("docker network create %s --subnet %s/24", fg.networkName, fg.subnet) + _, _ = tests.ExecuteT(fg.T, cmd, "") +} + +func (fg *FixtureGroup) rmNetwork() { + cmd := fmt.Sprintf("docker network rm %s", fg.networkName) + _, _ = tests.ExecuteT(fg.T, cmd, "") +} + +func (fg *FixtureGroup) initNodes(subnet string, numberOfNodes int) { + t := fg.T + chainID := t.Name() + + fg.subnet = subnet + + stdout, _ := tests.ExecuteT(t, fmt.Sprintf("docker images %s -q", fg.DockerImage), "") + if len(stdout) == 0 { + panic(errors.New("docker image is not found")) + } + + // Initialize temporary directories + gentxDir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer func() { + require.NoError(t, os.RemoveAll(gentxDir)) + }() + + for idx := 0; idx < numberOfNodes; idx++ { + name := fmt.Sprintf("%s-%s%d", t.Name(), namePrefix, idx) + f := NewFixtures(t) + f.UnsafeResetAll() + f.LDInit(name, fmt.Sprintf("--chain-id %s", chainID)) + fg.fixturesMap[name] = f + ip, err := calculateIP(subnet, idx+2) + require.NoError(fg.T, err) + f.BridgeIP = ip + } + for name, f := range fg.fixturesMap { + f.KeysDelete(name) + f.KeysAdd(name) + f.CLIConfig("output", "json") + f.CLIConfig("chain-id", f.ChainID) + f.CLIConfig("broadcast-mode", "block") + f.CLIConfig("trust-node", "true") + } + + for _, f := range fg.fixturesMap { + for nameInner, fInner := range fg.fixturesMap { + f.AddGenesisAccount(fInner.KeyAddress(nameInner), startCoins) + } + } + + for name, f := range fg.fixturesMap { + gentxDoc := filepath.Join(gentxDir, fmt.Sprintf("%s.json", name)) + f.GenTx(name, fmt.Sprintf("--output-document=%s --ip=%s", gentxDoc, f.BridgeIP)) + } + + for _, f := range fg.fixturesMap { + f.CollectGenTxs(fmt.Sprintf("--gentx-dir=%s", gentxDir)) + f.ValidateGenesis() + if len(fg.genesisFileContent) == 0 { + fg.genesisFileContent, err = ioutil.ReadFile(f.GenesisFile()) + require.NoError(t, err) + } + } + for _, f := range fg.fixturesMap { + err := ioutil.WriteFile(f.GenesisFile(), fg.genesisFileContent, os.ModePerm) + require.NoError(t, err) + } +} + +func (fg *FixtureGroup) LDStopContainers() { + wg := &sync.WaitGroup{} + wg.Add(len(fg.fixturesMap)) + + for _, f := range fg.fixturesMap { + copyedF := f + go func() { + fg.LDStopContainer(copyedF) + wg.Done() + }() + } + + if timeout := waitTimeout(wg, time.Minute); timeout { + panic(errors.New("linkd stop containers failed")) + } +} + +func (fg *FixtureGroup) LDStartContainers() { + wg := &sync.WaitGroup{} + wg.Add(len(fg.fixturesMap)) + + for _, f := range fg.fixturesMap { + _ = fg.LDStartContainer(f) + } + + for _, f := range fg.fixturesMap { + port := f.Port + go func() { + WaitForTMStart(port) + tests.WaitForNextNBlocksTM(1, port) + wg.Done() + }() + } + if timeout := waitTimeout(wg, time.Minute); timeout { + panic(errors.New("linkd start containers failed")) + } +} + +func (fg *FixtureGroup) LDStartContainer(f *Fixtures, flags ...string) *tests.Process { + dockerCommand := "docker run --rm --name %s --network %s --ip %s -p %s:26656 -p %s:26657 -v %s:/root/.linkd:Z -v %s:/root/.linkcli:Z line/link linkd start --rpc.laddr=tcp://0.0.0.0:26657 --p2p.laddr=tcp://0.0.0.0:26656" + dockerCommand = fmt.Sprintf(dockerCommand, f.Moniker, fg.networkName, f.BridgeIP, f.P2PPort, f.Port, f.LinkdHome, f.LinkcliHome) + fg.T.Log(dockerCommand) + proc := tests.GoExecuteTWithStdout(f.T, addFlags(dockerCommand, flags)) + + return proc +} + +func (fg *FixtureGroup) LDStopContainer(f *Fixtures) { + cmd := "docker ps --filter name=%s --filter status=running -q" + cmd = fmt.Sprintf(cmd, f.Moniker) + stdout, _ := tests.ExecuteT(f.T, cmd, "") + containerID := stdout + if len(stdout) > 0 { + cmd := "docker stop %s" + cmd = fmt.Sprintf(cmd, containerID) + stdout, stderr := tests.ExecuteT(f.T, cmd, "") + if stdout != containerID { + panic(stderr) + } + } +} + +func (fg *FixtureGroup) WaitForContainer(f *Fixtures) { + cmd := "docker ps --filter name=%s --filter status=running -q" + cmd = fmt.Sprintf(cmd, f.Moniker) + + var err error + fmt.Printf("Wait for the container[%s] boot up\n", f.Moniker) + for i := 0; i < 100; i++ { + time.Sleep(time.Millisecond * 100) + stdout, stderr := tests.ExecuteT(f.T, cmd, "") + + if len(stdout) > 0 { + return + } + err = errors.New(stderr) + } + // still haven't started up?! panic! + panic(err) +} + +func (fg *FixtureGroup) AddFullNode(flags ...string) *Fixtures { + t := fg.T + idx := len(fg.fixturesMap) + chainID := fg.T.Name() + + name := fmt.Sprintf("%s-%s%d", t.Name(), namePrefix, idx) + f := NewFixtures(t) + + // Initialize linkd + { + f.UnsafeResetAll() + f.LDInit(name, fmt.Sprintf("--chain-id %s", chainID)) + ip, err := calculateIP(fg.subnet, idx+2) + require.NoError(fg.T, err) + f.BridgeIP = ip + } + + // Initialize linkcli + { + f.KeysDelete(name) + f.KeysAdd(name) + f.CLIConfig("output", "json") + f.CLIConfig("chain-id", f.ChainID) + f.CLIConfig("broadcast-mode", "block") + f.CLIConfig("trust-node", "true") + } + + // Copy the genesis.json + { + if len(fg.genesisFileContent) == 0 { + panic("genesis file is not loaded") + } + err := ioutil.WriteFile(f.GenesisFile(), fg.genesisFileContent, os.ModePerm) + require.NoError(t, err) + } + + // Configure for invisible options + { + if len(flags) > 0 { + configFilePath := filepath.Join(f.LinkdHome, "config/config.toml") + + conf := cfg.DefaultConfig() + err := viper.Unmarshal(conf) + require.NoError(t, err) + + for _, flag := range flags { + if flag == "--mempool.broadcast=false" { + conf.Mempool.Broadcast = false + } + } + + cfg.WriteConfigFile(configFilePath, conf) + } + } + + // Collect the persistent peers from the network + var persistentPeers []string + { + for _, other := range fg.fixturesMap { + statusInfo := other.Status() + id := string(statusInfo.NodeInfo.ID()) + persistentPeers = append(persistentPeers, fmt.Sprintf("%s@%s:%d", id, other.BridgeIP, 26656)) + } + } + + // Add fixture to the group + fg.fixturesMap[name] = f + + // Start linkd + fg.LDStartContainer(f, fmt.Sprintf("--p2p.persistent_peers %s", strings.Join(persistentPeers, ","))) + + WaitForTMStart(f.Port) + tests.WaitForNextNBlocksTM(1, f.Port) + + return f +} + +func (fg *FixtureGroup) Fixture(index int) *Fixtures { + name := fmt.Sprintf("%s-%s%d", fg.T.Name(), namePrefix, index) + if f, ok := fg.fixturesMap[name]; ok { + return f + } + return nil +} + +func (fg *FixtureGroup) Cleanup() { + fg.LDStopContainers() + fg.rmNetwork() + for _, f := range fg.fixturesMap { + f.Cleanup() + } +} + +// waitTimeout waits for the waitgroup for the specified max timeout. +// Returns true if waiting timed out. +func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { + c := make(chan struct{}) + go func() { + defer close(c) + wg.Wait() + }() + select { + case <-c: + return false // completed normally + case <-time.After(timeout): + return true // timed out + } +} + +func calculateIP(ip string, i int) (string, error) { + ipv4 := net.ParseIP(ip).To4() + if ipv4 == nil { + return "", fmt.Errorf("%v: non ipv4 address", ip) + } + + for j := 0; j < i; j++ { + ipv4[3]++ + } + + return ipv4.String(), nil +} + +// wait for tendermint to start by querying tendermint +func WaitForTMStart(port string) { + url := fmt.Sprintf("http://localhost:%v/block", port) + WaitForStart(url) +} + +// WaitForStart waits for the node to start by pinging the url +// every 100ms for 10s until it returns 200. If it takes longer than 5s, +// it panics. +func WaitForStart(url string) { + var err error + + // ping the status endpoint a few times a second + // for a few seconds until we get a good response. + // otherwise something probably went wrong + // 2 ^ 7 = 128 --> approximately 10 secs + wait := 1 + for i := 0; i < 7; i++ { + // 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 6.4, 12.8, 25.6, 51.2, 102.4 + time.Sleep(time.Millisecond * 100 * time.Duration(wait)) + wait *= 2 + + var res *http.Response + /* #nosec */ + res, err = http.Get(url) // Error is arising in testing files + if err != nil || res == nil { + continue + } + err = res.Body.Close() + if err != nil { + panic(err) + } + + if res.StatusCode == http.StatusOK { + // good! + return + } + } + // still haven't started up?! panic! + panic(err) +} diff --git a/x/wasm/linkwasmd/cmd/linkwasmcli/main.go b/x/wasm/linkwasmd/cmd/linkwasmcli/main.go new file mode 100644 index 0000000000..78fc3aa41a --- /dev/null +++ b/x/wasm/linkwasmd/cmd/linkwasmcli/main.go @@ -0,0 +1,185 @@ +package main + +import ( + "fmt" + "os" + "path" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/lcd" + "github.com/cosmos/cosmos-sdk/client/rpc" + "github.com/line/lbm-sdk/v2/x/account" + authclient "github.com/line/lbm-sdk/v2/x/account/client" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/libs/cli" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + + "github.com/cosmos/cosmos-sdk/version" + "github.com/line/lbm-sdk/v2/x/coin" + "github.com/line/lbm-sdk/v2/x/wasm/linkwasmd/app" + "github.com/line/lbm-sdk/v2/x/wasm/linkwasmd/types" +) + +const ( + flagTestnet = "testnet" +) + +func main() { + // Configure cobra to sort commands + cobra.EnableCommandSorting = false + + // Instantiate the codec for the command line application + cdc := app.MakeCodec() + + // TODO: setup keybase, viper object, etc. to be passed into + // the below functions and eliminate global vars, like we do + // with the cdc + + rootCmd := &cobra.Command{ + Use: "linkwasmcli", + Short: "Command line interface for interacting with linkwasmd", + } + + // Add --chain-id to persistent flags and mark it required + rootCmd.PersistentFlags().String(flags.FlagChainID, "", "Chain ID of tendermint node") + rootCmd.PersistentFlags().Bool(flagTestnet, false, "Run with testnet mode. The address prefix becomes tlink if this flag is set.") + rootCmd.PersistentPreRunE = func(_ *cobra.Command, _ []string) error { + return initConfig(rootCmd) + } + + // Construct Root Command + rootCmd.AddCommand( + rpc.StatusCommand(), + client.ConfigCmd(app.DefaultCLIHome), + queryCmd(cdc), + txCmd(cdc), + flags.LineBreak, + lcd.ServeCommand(cdc, registerRoutes), + flags.LineBreak, + keys.Commands(), + flags.LineBreak, + flags.NewCompletionCmd(rootCmd, true), + version.Cmd, + ) + + // Add flags and prefix all env exposed with GA + executor := cli.PrepareMainCmd(rootCmd, "GA", app.DefaultCLIHome) + + err := executor.Execute() + if err != nil { + fmt.Printf("Failed executing CLI command: %s, exiting...\n", err) + os.Exit(1) + } +} + +func queryCmd(cdc *amino.Codec) *cobra.Command { + queryCmd := &cobra.Command{ + Use: "query", + Aliases: []string{"q"}, + Short: "Querying subcommands", + } + + queryCmd.AddCommand( + authclient.GetAccountCmd(cdc), + flags.LineBreak, + rpc.ValidatorCommand(cdc), + rpc.BlockCommand(), + authclient.QueryTxsByEventsCmd(cdc), + authclient.QueryTxCmd(cdc), + authclient.QueryBlockWithTxResponsesCommand(cdc), + flags.LineBreak, + ) + + // add modules' query commands + app.ModuleBasics.AddQueryCommands(queryCmd, cdc) + + return queryCmd +} + +func txCmd(cdc *amino.Codec) *cobra.Command { + txCmd := &cobra.Command{ + Use: "tx", + Short: "Transactions subcommands", + } + + txCmd.AddCommand( + coin.SendTxCmd(cdc), + flags.LineBreak, + account.CreateAccountTxCmd(cdc), + account.EmptyTxCmd(cdc), + flags.LineBreak, + authclient.GetSignCommand(cdc), + authclient.GetMultiSignCommand(cdc), + flags.LineBreak, + authclient.GetBroadcastCommand(cdc), + authclient.GetEncodeCommand(cdc), + flags.LineBreak, + ) + + // add modules' tx commands + app.ModuleBasics.AddTxCommands(txCmd, cdc) + + // remove auth and bank and account commands as they're mounted under the root tx command + var cmdsToRemove []*cobra.Command + + for _, cmd := range txCmd.Commands() { + if cmd.Use == auth.ModuleName || cmd.Use == coin.ModuleName || cmd.Use == account.ModuleName { + cmdsToRemove = append(cmdsToRemove, cmd) + } + } + + txCmd.RemoveCommand(cmdsToRemove...) + + return txCmd +} + +// registerRoutes registers the routes from the different modules for the LCD. +// NOTE: details on the routes added for each module are in the module documentation +// NOTE: If making updates here you also need to update the test helper in client/lcd/test_helper.go +func registerRoutes(rs *lcd.RestServer) { + authclient.RegisterTxRoutes(rs.CliCtx, rs.Mux) + app.ModuleBasics.RegisterRESTRoutes(rs.CliCtx, rs.Mux) +} + +func initConfig(cmd *cobra.Command) error { + home, err := cmd.PersistentFlags().GetString(cli.HomeFlag) + if err != nil { + return err + } + + cfgFile := path.Join(home, "config", "config.toml") + if _, err := os.Stat(cfgFile); err == nil { + viper.SetConfigFile(cfgFile) + + if err := viper.ReadInConfig(); err != nil { + return err + } + } + + testnet := viper.GetBool(flagTestnet) + + // Read in the configuration file for the sdk + config := sdk.GetConfig() + config.SetBech32PrefixForAccount(types.Bech32PrefixAcc(testnet), types.Bech32PrefixAccPub(testnet)) + config.SetBech32PrefixForValidator(types.Bech32PrefixValAddr(testnet), types.Bech32PrefixValPub(testnet)) + config.SetBech32PrefixForConsensusNode(types.Bech32PrefixConsAddr(testnet), types.Bech32PrefixConsPub(testnet)) + config.SetCoinType(types.CoinType) + config.SetFullFundraiserPath(types.FullFundraiserPath) + config.Seal() + + if err := viper.BindPFlag(flags.FlagChainID, cmd.PersistentFlags().Lookup(flags.FlagChainID)); err != nil { + return err + } + if err := viper.BindPFlag(cli.EncodingFlag, cmd.PersistentFlags().Lookup(cli.EncodingFlag)); err != nil { + return err + } + return viper.BindPFlag(cli.OutputFlag, cmd.PersistentFlags().Lookup(cli.OutputFlag)) +} diff --git a/x/wasm/linkwasmd/cmd/linkwasmd/errorcodes.go b/x/wasm/linkwasmd/cmd/linkwasmd/errorcodes.go new file mode 100644 index 0000000000..0abc8c6b3c --- /dev/null +++ b/x/wasm/linkwasmd/cmd/linkwasmd/errorcodes.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "sort" + + "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/spf13/cobra" +) + +func errorCodesCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "errorcodes", + Short: "List errorcodes registered", + RunE: func(cmd *cobra.Command, _ []string) error { + registeredErrors := errors.RegisteredErrors() + keys := make([]string, 0, len(registeredErrors)) + errorCodesMap := map[string]*errors.Error{} + for _, e := range registeredErrors { + key := fmt.Sprintf("%s:%10d", e.Codespace(), e.ABCICode()) + keys = append(keys, key) + errorCodesMap[key] = e + } + + sort.Strings(keys) + + fmt.Println("| codespace | error code | description | ") + fmt.Println("| --------- | ---------- | ----------- | ") + for _, k := range keys { + e := errorCodesMap[k] + fmt.Printf("| %v | %v | %s |\n", e.Codespace(), e.ABCICode(), e.Error()) + } + + return nil + }, + } + return cmd +} diff --git a/x/wasm/linkwasmd/cmd/linkwasmd/genaccounts.go b/x/wasm/linkwasmd/cmd/linkwasmd/genaccounts.go new file mode 100644 index 0000000000..c93e1fa23c --- /dev/null +++ b/x/wasm/linkwasmd/cmd/linkwasmd/genaccounts.go @@ -0,0 +1,148 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/tendermint/libs/cli" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/server" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + authvesting "github.com/cosmos/cosmos-sdk/x/auth/vesting" + "github.com/cosmos/cosmos-sdk/x/genutil" +) + +const ( + flagClientHome = "home-client" + flagVestingStart = "vesting-start-time" + flagVestingEnd = "vesting-end-time" + flagVestingAmt = "vesting-amount" +) + +// AddGenesisAccountCmd returns add-genesis-account cobra Command. +func AddGenesisAccountCmd( + ctx *server.Context, cdc *codec.Codec, defaultNodeHome, defaultClientHome string, +) *cobra.Command { + cmd := &cobra.Command{ + Use: "add-genesis-account [address_or_key_name] [coin][,[coin]]", + Short: "Add a genesis account to genesis.json", + Long: `Add a genesis account to genesis.json. The provided account must specify +the account address or key name and a list of initial coins. If a key name is given, +the address will be looked up in the local Keybase. The list of initial tokens must +contain valid denominations. Accounts may optionally be supplied with vesting parameters. +`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + config := ctx.Config + config.SetRoot(viper.GetString(cli.HomeFlag)) + + addr, err := sdk.AccAddressFromBech32(args[0]) + inBuf := bufio.NewReader(cmd.InOrStdin()) + if err != nil { + // attempt to lookup address from Keybase if no address was provided + kb, err := keys.NewKeyring(sdk.KeyringServiceName(), + viper.GetString(flags.FlagKeyringBackend), viper.GetString(flagClientHome), inBuf) + if err != nil { + return err + } + + info, err := kb.Get(args[0]) + if err != nil { + return fmt.Errorf("failed to get address from Keybase: %w", err) + } + + addr = info.GetAddress() + } + + coins, err := sdk.ParseCoins(args[1]) + if err != nil { + return fmt.Errorf("failed to parse coins: %w", err) + } + + vestingStart := viper.GetInt64(flagVestingStart) + vestingEnd := viper.GetInt64(flagVestingEnd) + vestingAmt, err := sdk.ParseCoins(viper.GetString(flagVestingAmt)) + if err != nil { + return fmt.Errorf("failed to parse vesting amount: %w", err) + } + + // create concrete account type based on input parameters + var genAccount authexported.GenesisAccount + + baseAccount := auth.NewBaseAccount(addr, coins.Sort(), nil, 0, 0) + if !vestingAmt.IsZero() { + baseVestingAccount, err := authvesting.NewBaseVestingAccount(baseAccount, vestingAmt.Sort(), vestingEnd) + if err != nil { + return fmt.Errorf("failed to create base vesting account: %w", err) + } + + switch { + case vestingStart != 0 && vestingEnd != 0: + genAccount = authvesting.NewContinuousVestingAccountRaw(baseVestingAccount, vestingStart) + + case vestingEnd != 0: + genAccount = authvesting.NewDelayedVestingAccountRaw(baseVestingAccount) + + default: + return errors.New("invalid vesting parameters; must supply start and end time or end time") + } + } else { + genAccount = baseAccount + } + + if err := genAccount.Validate(); err != nil { + return fmt.Errorf("failed to validate new genesis account: %w", err) + } + + genFile := config.GenesisFile() + appState, genDoc, err := genutil.GenesisStateFromGenFile(cdc, genFile) + if err != nil { + return fmt.Errorf("failed to unmarshal genesis state: %w", err) + } + + authGenState := auth.GetGenesisStateFromAppState(cdc, appState) + + if authGenState.Accounts.Contains(addr) { + return fmt.Errorf("cannot add account at existing address %s", addr) + } + + // Add the new account to the set of genesis accounts and sanitize the + // accounts afterwards. + authGenState.Accounts = append(authGenState.Accounts, genAccount) + authGenState.Accounts = auth.SanitizeGenesisAccounts(authGenState.Accounts) + + authGenStateBz, err := cdc.MarshalJSON(authGenState) + if err != nil { + return fmt.Errorf("failed to marshal auth genesis state: %w", err) + } + + appState[auth.ModuleName] = authGenStateBz + + appStateJSON, err := cdc.MarshalJSON(appState) + if err != nil { + return fmt.Errorf("failed to marshal application genesis state: %w", err) + } + + genDoc.AppState = appStateJSON + return genutil.ExportGenesisFile(genDoc, genFile) + }, + } + + cmd.Flags().String(cli.HomeFlag, defaultNodeHome, "node's home directory") + cmd.Flags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|test)") + cmd.Flags().String(flagClientHome, defaultClientHome, "client's home directory") + cmd.Flags().String(flagVestingAmt, "", "amount of coins for vesting accounts") + cmd.Flags().Uint64(flagVestingStart, 0, "schedule start time (unix epoch) for vesting accounts") + cmd.Flags().Uint64(flagVestingEnd, 0, "schedule end time (unix epoch) for vesting accounts") + + return cmd +} diff --git a/x/wasm/linkwasmd/cmd/linkwasmd/main.go b/x/wasm/linkwasmd/cmd/linkwasmd/main.go new file mode 100644 index 0000000000..dd408e5235 --- /dev/null +++ b/x/wasm/linkwasmd/cmd/linkwasmd/main.go @@ -0,0 +1,160 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/version" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/cli" + "github.com/tendermint/tendermint/libs/log" + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" + + "github.com/line/lbm-sdk/v2/x/wasm/linkwasmd/app" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli" + "github.com/cosmos/cosmos-sdk/x/staking" + + "github.com/line/lbm-sdk/v2/x/wasm/linkwasmd/types" +) + +// linkd custom flags +const ( + flagInvCheckPeriod = "inv-check-period" + flagTestnet = "testnet" +) + +var invCheckPeriod uint +var testnet bool + +func main() { + cdc := app.MakeCodec() + + ctx := server.NewDefaultContext() + cobra.EnableCommandSorting = false + rootCmd := &cobra.Command{ + Use: "linkwasmd", + Short: "Link Wasm Daemon (server)", + PersistentPreRunE: LinkPreRunEFn(ctx), + } + + rootCmd.AddCommand(genutilcli.InitCmd(ctx, cdc, app.ModuleBasics, app.DefaultNodeHome)) + rootCmd.AddCommand(genutilcli.CollectGenTxsCmd(ctx, cdc, auth.GenesisAccountIterator{}, app.DefaultNodeHome)) + rootCmd.AddCommand(genutilcli.MigrateGenesisCmd(ctx, cdc)) + rootCmd.AddCommand(genutilcli.GenTxCmd(ctx, cdc, app.ModuleBasics, staking.AppModuleBasic{}, + auth.GenesisAccountIterator{}, app.DefaultNodeHome, app.DefaultCLIHome)) + rootCmd.AddCommand(genutilcli.ValidateGenesisCmd(ctx, cdc, app.ModuleBasics)) + rootCmd.AddCommand(AddGenesisAccountCmd(ctx, cdc, app.DefaultNodeHome, app.DefaultCLIHome)) + rootCmd.AddCommand(flags.NewCompletionCmd(rootCmd, true)) + rootCmd.AddCommand(testnetCmd(ctx, cdc, app.ModuleBasics, auth.GenesisAccountIterator{})) + rootCmd.AddCommand(version.Cmd) + rootCmd.AddCommand(errorCodesCmd()) + + server.AddCommands(ctx, cdc, rootCmd, newApp, exportAppStateAndTMValidators) + + // prepare and add flags + executor := cli.PrepareBaseCmd(rootCmd, "GA", app.DefaultNodeHome) + rootCmd.PersistentFlags().UintVar(&invCheckPeriod, flagInvCheckPeriod, + 0, "Assert registered invariants every N blocks") + rootCmd.PersistentFlags().BoolVar(&testnet, flagTestnet, testnet, "Run with testnet mode. The address prefix becomes tlink if this flag is set.") + + err := executor.Execute() + if err != nil { + panic(err) + } +} + +func LinkPreRunEFn(context *server.Context) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + f := server.PersistentPreRunEFn(context) + err := f(cmd, args) + + if cmd.Name() == version.Cmd.Name() { + return nil + } + testnet := viper.GetBool(flagTestnet) + config := sdk.GetConfig() + config.SetBech32PrefixForAccount(types.Bech32PrefixAcc(testnet), types.Bech32PrefixAccPub(testnet)) + config.SetBech32PrefixForValidator(types.Bech32PrefixValAddr(testnet), types.Bech32PrefixValPub(testnet)) + config.SetBech32PrefixForConsensusNode(types.Bech32PrefixConsAddr(testnet), types.Bech32PrefixConsPub(testnet)) + config.SetCoinType(types.CoinType) + config.SetFullFundraiserPath(types.FullFundraiserPath) + config.Seal() + + if cmd.Name() == server.StartCmd(nil, nil).Name() { + var networkMode string + if testnet { + networkMode = "testnet" + } else { + networkMode = "mainnet" + } + context.Logger.Info(fmt.Sprintf("Network mode is %s", networkMode)) + printDBBackend(context) + } + return err + } +} + +func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application { + skipUpgradeHeights := make(map[int64]bool) + for _, h := range viper.GetIntSlice(server.FlagUnsafeSkipUpgrades) { + skipUpgradeHeights[int64(h)] = true + } + + pruningOpts, err := server.GetPruningOptionsFromFlags() + if err != nil { + panic(err) + } + + var cache sdk.MultiStorePersistentCache + + if viper.GetBool(server.FlagInterBlockCache) { + cache = store.NewCommitKVStoreCacheManager() + } + + return app.NewLinkApp( + logger, db, traceStore, true, skipUpgradeHeights, invCheckPeriod, + baseapp.SetPruning(pruningOpts), + baseapp.SetMinGasPrices(viper.GetString(server.FlagMinGasPrices)), + baseapp.SetHaltHeight(uint64(viper.GetInt(server.FlagHaltHeight))), + baseapp.SetInterBlockCache(cache), + ) +} + +func exportAppStateAndTMValidators( + logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, forZeroHeight bool, jailWhiteList []string, +) (json.RawMessage, []tmtypes.GenesisValidator, error) { + if height != -1 { + gApp := app.NewLinkApp(logger, db, traceStore, false, map[int64]bool{}, uint(1)) + err := gApp.LoadHeight(height) + if err != nil { + return nil, nil, err + } + return gApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) + } + gApp := app.NewLinkApp(logger, db, traceStore, true, map[int64]bool{}, uint(1)) + return gApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) +} + +func printDBBackend(context *server.Context) { + var linkDBBackend dbm.BackendType + if sdk.DBBackend == "" { + linkDBBackend = dbm.GoLevelDBBackend + } else { + linkDBBackend = dbm.BackendType(sdk.DBBackend) + } + context.Logger.Info(fmt.Sprintf("LINK DB Backend is %s", linkDBBackend)) + context.Logger.Info(fmt.Sprintf("Tendermint DB Backend is %s", context.Config.DBBackend)) +} diff --git a/x/wasm/linkwasmd/cmd/linkwasmd/testnet.go b/x/wasm/linkwasmd/cmd/linkwasmd/testnet.go new file mode 100644 index 0000000000..ad48bc6b75 --- /dev/null +++ b/x/wasm/linkwasmd/cmd/linkwasmd/testnet.go @@ -0,0 +1,375 @@ +package main + +// DONTCOVER + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "os" + "path/filepath" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/input" + "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/spf13/cobra" + "github.com/spf13/viper" + tmconfig "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/crypto" + tmos "github.com/tendermint/tendermint/libs/os" + tmrand "github.com/tendermint/tendermint/libs/rand" + "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/server" + srvconfig "github.com/cosmos/cosmos-sdk/server/config" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/cosmos/cosmos-sdk/x/staking" +) + +var ( + flagNodeDirPrefix = "node-dir-prefix" + flagNumValidators = "v" + flagOutputDir = "output-dir" + flagNodeDaemonHome = "node-daemon-home" + flagNodeCLIHome = "node-cli-home" + flagStartingIPAddress = "starting-ip-address" +) + +// get cmd to initialize all files for tendermint testnet and application +func testnetCmd(ctx *server.Context, cdc *codec.Codec, + mbm module.BasicManager, genAccIterator genutiltypes.GenesisAccountsIterator, +) *cobra.Command { + cmd := &cobra.Command{ + Use: "testnet", + Short: "Initialize files for a Linkd testnet", + Long: `testnet will create "v" number of directories and populate each with +necessary files (private validator, genesis, config, etc.). + +Note, strict routability for addresses is turned off in the config file. + +Example: + linkd testnet --v 4 --output-dir ./output --starting-ip-address 192.168.10.2 + `, + RunE: func(cmd *cobra.Command, _ []string) error { + config := ctx.Config + + outputDir := viper.GetString(flagOutputDir) + chainID := viper.GetString(flags.FlagChainID) + minGasPrices := viper.GetString(server.FlagMinGasPrices) + nodeDirPrefix := viper.GetString(flagNodeDirPrefix) + nodeDaemonHome := viper.GetString(flagNodeDaemonHome) + nodeCLIHome := viper.GetString(flagNodeCLIHome) + startingIPAddress := viper.GetString(flagStartingIPAddress) + numValidators := viper.GetInt(flagNumValidators) + + return InitTestnet(cmd, config, cdc, mbm, genAccIterator, outputDir, chainID, + minGasPrices, nodeDirPrefix, nodeDaemonHome, nodeCLIHome, startingIPAddress, numValidators) + }, + } + + cmd.Flags().Int(flagNumValidators, 4, + "Number of validators to initialize the testnet with") + cmd.Flags().StringP(flagOutputDir, "o", "./mytestnet", + "Directory to store initialization data for the testnet") + cmd.Flags().String(flagNodeDirPrefix, "node", + "Prefix the directory name for each node with (node results in node0, node1, ...)") + cmd.Flags().String(flagNodeDaemonHome, "linkd", + "Home directory of the node's daemon configuration") + cmd.Flags().String(flagNodeCLIHome, "linkcli", + "Home directory of the node's cli configuration") + cmd.Flags().String(flagStartingIPAddress, "192.168.0.1", + "Starting IP address (192.168.0.1 results in persistent peers list ID0@192.168.0.1:46656, ID1@192.168.0.2:46656, ...)") + cmd.Flags().String( + flags.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") + cmd.Flags().String( + server.FlagMinGasPrices, fmt.Sprintf("0.000006%s", sdk.DefaultBondDenom), + "Minimum gas prices to accept for transactions; All fees in a tx must meet this minimum (e.g. 0.01photino,0.001stake)") + return cmd +} + +const nodeDirPerm = 0755 + +// Initialize the testnet +func InitTestnet(cmd *cobra.Command, config *tmconfig.Config, cdc *codec.Codec, + mbm module.BasicManager, genAccIterator genutiltypes.GenesisAccountsIterator, + outputDir, chainID, minGasPrices, nodeDirPrefix, nodeDaemonHome, + nodeCLIHome, startingIPAddress string, numValidators int) error { + if chainID == "" { + chainID = "chain-" + tmrand.Str(6) + } + + monikers := make([]string, numValidators) + nodeIDs := make([]string, numValidators) + valPubKeys := make([]crypto.PubKey, numValidators) + + linkConfig := srvconfig.DefaultConfig() + linkConfig.MinGasPrices = minGasPrices + + // nolint:prealloc + var ( + genAccounts []authexported.GenesisAccount + genFiles []string + ) + + inBuf := bufio.NewReader(cmd.InOrStdin()) + // generate private keys, node IDs, and initial transactions + for i := 0; i < numValidators; i++ { + nodeDirName := fmt.Sprintf("%s%d", nodeDirPrefix, i) + nodeDir := filepath.Join(outputDir, nodeDirName, nodeDaemonHome) + clientDir := filepath.Join(outputDir, nodeDirName, nodeCLIHome) + gentxsDir := filepath.Join(outputDir, "gentxs") + + config.SetRoot(nodeDir) + config.RPC.ListenAddress = "tcp://0.0.0.0:26657" + + if err := os.MkdirAll(filepath.Join(nodeDir, "config"), nodeDirPerm); err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + if err := os.MkdirAll(clientDir, nodeDirPerm); err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + monikers = append(monikers, nodeDirName) + config.Moniker = nodeDirName + + ip, err := getIP(i, startingIPAddress) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + nodeIDs[i], valPubKeys[i], err = genutil.InitializeNodeValidatorFiles(config) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + memo := fmt.Sprintf("%s@%s:26656", nodeIDs[i], ip) + genFiles = append(genFiles, config.GenesisFile()) + + prompt := fmt.Sprintf( + "Password for account '%s' (default %s):", nodeDirName, keys.DefaultKeyPass, + ) + + keyPass, err := input.GetPassword(prompt, inBuf) + if err != nil && keyPass != "" { + // An error was returned that either failed to read the password from + // STDIN or the given password is not empty but failed to meet minimum + // length requirements. + return err + } + + if keyPass == "" { + keyPass = keys.DefaultKeyPass + } + + kb, err := keys.NewKeyBaseFromDir(clientDir) + if err != nil { + return err + } + + addr, secret, err := server.GenerateSaveCoinKey(kb, nodeDirName, keyPass, true) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + info := map[string]string{"secret": secret} + + cliPrint, err := json.Marshal(info) + if err != nil { + return err + } + + // save private key seed words + if err := writeFile(fmt.Sprintf("%v.json", "key_seed"), clientDir, cliPrint); err != nil { + return err + } + + accTokens := sdk.TokensFromConsensusPower(1000) + accStakingTokens := sdk.TokensFromConsensusPower(500) + coins := sdk.Coins{ + sdk.NewCoin(fmt.Sprintf("%stoken", nodeDirName), accTokens), + sdk.NewCoin(sdk.DefaultBondDenom, accStakingTokens), + } + genAccounts = append(genAccounts, auth.NewBaseAccount(addr, coins.Sort(), nil, 0, 0)) + + valTokens := sdk.TokensFromConsensusPower(100) + msg := staking.NewMsgCreateValidator( + sdk.ValAddress(addr), + valPubKeys[i], + sdk.NewCoin(sdk.DefaultBondDenom, valTokens), + staking.NewDescription(nodeDirName, "", "", "", ""), + staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.OneInt(), + ) + + tx := auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, []auth.StdSignature{}, memo) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithChainID(chainID).WithMemo(memo).WithKeybase(kb) + + signedTx, err := txBldr.SignStdTx(nodeDirName, keys.DefaultKeyPass, tx, false) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + txBytes, err := cdc.MarshalJSON(signedTx) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + // gather gentxs folder + if err := writeFile(fmt.Sprintf("%v.json", nodeDirName), gentxsDir, txBytes); err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + linkConfigFilePath := filepath.Join(nodeDir, "config/linkd.toml") + srvconfig.WriteConfigFile(linkConfigFilePath, linkConfig) + } + + if err := initGenFiles(cdc, mbm, chainID, genAccounts, genFiles, numValidators); err != nil { + return err + } + + err := collectGenFiles( + cdc, config, chainID, monikers, nodeIDs, valPubKeys, numValidators, + outputDir, nodeDirPrefix, nodeDaemonHome, genAccIterator, + ) + if err != nil { + return err + } + + cmd.PrintErrf("Successfully initialized %d node directories\n", numValidators) + return nil +} + +func initGenFiles(cdc *codec.Codec, mbm module.BasicManager, chainID string, + genAccounts []authexported.GenesisAccount, genFiles []string, numValidators int) error { + appGenState := mbm.DefaultGenesis() + + // set the accounts in the genesis state + authDataBz := appGenState[auth.ModuleName] + var authGenState auth.GenesisState + cdc.MustUnmarshalJSON(authDataBz, &authGenState) + authGenState.Accounts = genAccounts + appGenState[auth.ModuleName] = cdc.MustMarshalJSON(authGenState) + + appGenStateJSON, err := codec.MarshalJSONIndent(cdc, appGenState) + if err != nil { + return err + } + + genDoc := types.GenesisDoc{ + ChainID: chainID, + AppState: appGenStateJSON, + Validators: nil, + } + + // generate empty genesis files for each validator and save + for i := 0; i < numValidators; i++ { + if err := genDoc.SaveAs(genFiles[i]); err != nil { + return err + } + } + return nil +} + +func collectGenFiles( + cdc *codec.Codec, config *tmconfig.Config, chainID string, + monikers, nodeIDs []string, valPubKeys []crypto.PubKey, + numValidators int, outputDir, nodeDirPrefix, nodeDaemonHome string, + genAccIterator genutiltypes.GenesisAccountsIterator) error { + var appState json.RawMessage + genTime := tmtime.Now() + + for i := 0; i < numValidators; i++ { + nodeDirName := fmt.Sprintf("%s%d", nodeDirPrefix, i) + nodeDir := filepath.Join(outputDir, nodeDirName, nodeDaemonHome) + gentxsDir := filepath.Join(outputDir, "gentxs") + moniker := monikers[i] + config.Moniker = nodeDirName + + config.SetRoot(nodeDir) + + nodeID, valPubKey := nodeIDs[i], valPubKeys[i] + initCfg := genutil.NewInitConfig(chainID, gentxsDir, moniker, nodeID, valPubKey) + + genDoc, err := types.GenesisDocFromFile(config.GenesisFile()) + if err != nil { + return err + } + + nodeAppState, err := genutil.GenAppStateFromConfig(cdc, config, initCfg, *genDoc, genAccIterator) + if err != nil { + return err + } + + if appState == nil { + // set the canonical application state (they should not differ) + appState = nodeAppState + } + + genFile := config.GenesisFile() + + // overwrite each validator's genesis file to have a canonical genesis time + if err := genutil.ExportGenesisFileWithTime(genFile, chainID, nil, appState, genTime); err != nil { + return err + } + } + + return nil +} + +func getIP(i int, startingIPAddr string) (ip string, err error) { + if len(startingIPAddr) == 0 { + ip, err = server.ExternalIP() + if err != nil { + return "", err + } + return ip, nil + } + return calculateIP(startingIPAddr, i) +} + +func calculateIP(ip string, i int) (string, error) { + ipv4 := net.ParseIP(ip).To4() + if ipv4 == nil { + return "", fmt.Errorf("%v: non ipv4 address", ip) + } + + for j := 0; j < i; j++ { + ipv4[3]++ + } + + return ipv4.String(), nil +} + +func writeFile(name string, dir string, contents []byte) error { + writePath := filepath.Join(dir) + file := filepath.Join(writePath, name) + + err := tmos.EnsureDir(writePath, 0700) + if err != nil { + return err + } + + err = tmos.WriteFile(file, contents, 0600) + if err != nil { + return err + } + + return nil +} diff --git a/x/wasm/linkwasmd/go.mod b/x/wasm/linkwasmd/go.mod new file mode 100644 index 0000000000..284cdec681 --- /dev/null +++ b/x/wasm/linkwasmd/go.mod @@ -0,0 +1,24 @@ +module "github.com/line/lbm-sdk/v2/x/wasm/linkwasmd" + +go 1.13 + +require ( + github.com/cosmos/cosmos-sdk v0.39.2 + github.com/google/go-cmp v0.5.2 // indirect + "github.com/line/lbm-sdk/v2/ v0.2.0 + github.com/onsi/ginkgo v1.13.0 // indirect + github.com/rakyll/statik v0.1.7 // indirect + github.com/spf13/cobra v1.1.1 + github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.6.1 + github.com/tendermint/go-amino v0.15.1 + github.com/tendermint/tendermint v0.33.9 + github.com/tendermint/tm-db v0.5.2 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect +) + +replace github.com/keybase/go-keychain => github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 + +replace github.com/cosmos/cosmos-sdk => github.com/line/cosmos-sdk v0.39.2-0.2.0 + +replace "github.com/line/lbm-sdk/v2/ => ../../.. diff --git a/x/wasm/linkwasmd/go.sum b/x/wasm/linkwasmd/go.sum new file mode 100644 index 0000000000..3c25890c19 --- /dev/null +++ b/x/wasm/linkwasmd/go.sum @@ -0,0 +1,736 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.1.6 h1:kVDC2uCgVwecxCk+9zoCt2uEL6dt+dfVzMvGgnVcIuM= +github.com/99designs/keyring v1.1.6/go.mod h1:16e0ds7LGQQcT59QqkTg72Hh5ShM51Byv5PEmW6uoRU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= +github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= +github.com/CosmWasm/wasmvm v0.12.0 h1:L9ez6fCg2Co1SgCm0YHjbnewZ8myemu2cm/QL0qR1OE= +github.com/CosmWasm/wasmvm v0.12.0/go.mod h1:tbXGE9Jz6sYpiJroGr71OQ5TFOufq/P5LWsruA2u6JE= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/Workiva/go-datastructures v1.0.52 h1:PLSK6pwn8mYdaoaCZEMsXBpBotr4HHn9abU0yMQt0NI= +github.com/Workiva/go-datastructures v1.0.52/go.mod h1:Z+F2Rca0qCsVYDS8z7bAGm8f3UkzuWYS/oBZz5a7VVA= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d h1:1aAija9gr0Hyv4KfQcRcwlmFIrhkDmIj2dz5bkg/s/8= +github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d/go.mod h1:icNx/6QdFblhsEjZehARqbNumymUT/ydwlLojFdv7Sk= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= +github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU= +github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= +github.com/cosmos/ledger-cosmos-go v0.11.1 h1:9JIYsGnXP613pb2vPjFeMMjBI5lEDsEaF6oYorTy6J4= +github.com/cosmos/ledger-cosmos-go v0.11.1/go.mod h1:J8//BsAGTo3OC/vDLjMRFLW6q0WAaXvHnVc7ZmE8iUY= +github.com/cosmos/ledger-go v0.9.2 h1:Nnao/dLwaVTk1Q5U9THldpUMMXU94BOTWPddSmVB6pI= +github.com/cosmos/ledger-go v0.9.2/go.mod h1:oZJ2hHAZROdlHiwTg4t7kP+GKIIkBT+o6c9QWFanOyI= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= +github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b h1:HBah4D48ypg3J7Np4N+HY/ZR76fx3HEUGxDU6Uk39oQ= +github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1-0.20190508161146-9fa652df1129/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= +github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= +github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= +github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= +github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/line/cosmos-sdk v0.39.2-0.2.0 h1:seBLSqHPFh/Qf572IQ8hl9rnfFG/GrdtpK+HCr0aLbI= +github.com/line/cosmos-sdk v0.39.2-0.2.0/go.mod h1:UTxdYWx+OeRezEP8P5BxipddlFpq4q92uYydSeYN7B0= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= +github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= +github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA= +github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0 h1:M76yO2HkZASFjXL0HSoZJ1AYEmQxNJmY41Jx1zNUq1Y= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= +github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= +github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs= +github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= +github.com/tendermint/btcd v0.1.1 h1:0VcxPfflS2zZ3RiOAHkBiFUcPvbtRj5O7zHmcJWHV7s= +github.com/tendermint/btcd v0.1.1/go.mod h1:DC6/m53jtQzr/NFmMNEu0rxf18/ktVoVtMrnDD5pN+U= +github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 h1:hqAk8riJvK4RMWx1aInLzndwxKalgi5rTqgfXxOxbEI= +github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk= +github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= +github.com/tendermint/go-amino v0.15.1 h1:D2uk35eT4iTsvJd9jWIetzthE5C0/k2QmMFkCN+4JgQ= +github.com/tendermint/go-amino v0.15.1/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/tendermint/iavl v0.14.3 h1:tuiUAqJdA3OOyPU/9P3pMYnAcd+OL7BUdzNiE3ytUwQ= +github.com/tendermint/iavl v0.14.3/go.mod h1:vHLYxU/zuxBmxxr1v+5Vnd/JzcIsyK17n9P9RDubPVU= +github.com/tendermint/tendermint v0.33.5/go.mod h1:0yUs9eIuuDq07nQql9BmI30FtYGcEC60Tu5JzB5IezM= +github.com/tendermint/tendermint v0.33.9 h1:rRKIfu5qAXX5f9bwX1oUXSZz/ALFJjDuivhkbGUQxiU= +github.com/tendermint/tendermint v0.33.9/go.mod h1:0yUs9eIuuDq07nQql9BmI30FtYGcEC60Tu5JzB5IezM= +github.com/tendermint/tm-db v0.5.1/go.mod h1:g92zWjHpCYlEvQXvy9M168Su8V1IBEeawpXVVBaK4f4= +github.com/tendermint/tm-db v0.5.2 h1:QG3IxQZBubWlr7kGQcYIavyTNmZRO+r//nENxoq0g34= +github.com/tendermint/tm-db v0.5.2/go.mod h1:VrPTx04QJhQ9d8TFUTc2GpPBvBf/U9vIdBIzkjBk7Lk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/zondax/hid v0.9.0 h1:eiT3P6vNxAEVxXMw66eZUAAnU2zD33JBkfG/EnfAKl8= +github.com/zondax/hid v0.9.0/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= +go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb h1:HS9IzC4UFbpMBLQUDSQcU+ViVT1vdFCQVjdPVpTlZrs= +golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/x/wasm/linkwasmd/initialize.sh b/x/wasm/linkwasmd/initialize.sh new file mode 100755 index 0000000000..fa106099ce --- /dev/null +++ b/x/wasm/linkwasmd/initialize.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -ex + +LINKCLI=${LINKCLI:-linkwasmcli} +LINKD=${LINKD:-linkwasmd} + +# initialize +rm -rf ~/.linkwasmd ~/.linkwasmcli + +# Configure your CLI to eliminate need for chain-id flag +${LINKCLI} config chain-id linkwasm +${LINKCLI} config output json +${LINKCLI} config indent true +${LINKCLI} config trust-node true +${LINKCLI} config keyring-backend test + +# Initialize configuration files and genesis file +# moniker is the name of your node +${LINKD} init solo --chain-id linkwasm + +${LINKCLI} keys add jack +${LINKCLI} keys add alice +${LINKCLI} keys add bob +${LINKCLI} keys add rinah +${LINKCLI} keys add sam +${LINKCLI} keys add evelyn + +# Add both accounts, with coins to the genesis file +${LINKD} add-genesis-account $(${LINKCLI} keys show jack -a) 1000link,100000000stake +${LINKD} add-genesis-account $(${LINKCLI} keys show alice -a) 1000link,100000000stake +${LINKD} add-genesis-account $(${LINKCLI} keys show bob -a) 1000link,100000000stake +${LINKD} add-genesis-account $(${LINKCLI} keys show rinah -a) 1000link,100000000stake +${LINKD} add-genesis-account $(${LINKCLI} keys show sam -a) 1000link,100000000stake +${LINKD} add-genesis-account $(${LINKCLI} keys show evelyn -a) 1000link,100000000stake + +${LINKD} --keyring-backend=test gentx --name jack + +${LINKD} collect-gentxs + +${LINKD} validate-genesis + diff --git a/x/wasm/linkwasmd/perf/contract/cw_erc20_v4.wasm b/x/wasm/linkwasmd/perf/contract/cw_erc20_v4.wasm new file mode 100644 index 0000000000..34c234d3d0 Binary files /dev/null and b/x/wasm/linkwasmd/perf/contract/cw_erc20_v4.wasm differ diff --git a/x/wasm/linkwasmd/perf/scripts/capture.sh b/x/wasm/linkwasmd/perf/scripts/capture.sh new file mode 100755 index 0000000000..698d7cc3ec --- /dev/null +++ b/x/wasm/linkwasmd/perf/scripts/capture.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env sh +num_txs=$1 + +# capture with running binary +# log format: +# t:7 => tx count of the block +# s:2020-11-16 11:11:00.000 => time before execution +# e:2020-11-16 11:11:02.000 => time after execution +perf record --call-graph dwarf -F 500 -D 10000 -o linkwasmd.dwarf.perf \ +linkwasmd start --log_level="state:info,consensus:info,*:error" \ +| awk '{if (/Finalizing commit/) {print "t:" substr($13,3,4) "\ns:" substr($1,3,10) " " substr($1,14,12)} else if(/Committed state/) {print "e:" substr($1,3,10) " " substr($1,14,12)}}' \ +> linkwasmd.log & + +echo "start linkwasmd" + +sleep 7s # wait for booting + +echo "fire txs" +./perf/scripts/fire.sh $num_txs + +sleep 6s + +echo "terminate" +killall -SIGTERM linkwasmd + +sleep 5s +mkdir reports +perf script -i linkwasmd.dwarf.perf | perf/tools/FlameGraph/stackcollapse-perf.pl | perf/tools/FlameGraph/flamegraph.pl > reports/linkwasmd.dwarf.perf.svg + +./perf/scripts/report.sh linkwasmd.log reports/report.txt diff --git a/x/wasm/linkwasmd/perf/scripts/fire.sh b/x/wasm/linkwasmd/perf/scripts/fire.sh new file mode 100755 index 0000000000..1c26c0ffe9 --- /dev/null +++ b/x/wasm/linkwasmd/perf/scripts/fire.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +num_txs=$1 + +start_time="$(date -u +%s)" +for i in $(seq 0 $num_txs) +do + linkwasmcli tx broadcast tx$i.json +done +end_time="$(date -u +%s)" +elapsed="$(($end_time-$start_time))" +echo "$elapsed seconds elapsed for firing txs" diff --git a/x/wasm/linkwasmd/perf/scripts/init-chain.sh b/x/wasm/linkwasmd/perf/scripts/init-chain.sh new file mode 100755 index 0000000000..86ac72d09f --- /dev/null +++ b/x/wasm/linkwasmd/perf/scripts/init-chain.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +rm -rf ~/.linkwasmd ~/.linkwasmcli + +linkwasmcli config chain-id perf +linkwasmcli config output json +linkwasmcli config indent true +linkwasmcli config trust-node true +linkwasmcli config keyring-backend test + +linkwasmcli keys add jack + +linkwasmd init solo --chain-id perf +linkwasmd add-genesis-account $(linkwasmcli keys show jack -a) 100000000stake +linkwasmd --keyring-backend=test gentx --name jack +linkwasmd collect-gentxs +linkwasmd validate-genesis diff --git a/x/wasm/linkwasmd/perf/scripts/prepare.sh b/x/wasm/linkwasmd/perf/scripts/prepare.sh new file mode 100755 index 0000000000..15d5cb852c --- /dev/null +++ b/x/wasm/linkwasmd/perf/scripts/prepare.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env sh +wasm_path=$1 +num_txs=$2 + +linkwasmd start > /dev/null & + +# creates account as much as txs that will be fired +echo "create accounts" +for i in $(seq 0 $num_txs) +do + linkwasmcli keys add $i +done + +code_id=$(linkwasmcli tx wasm store $wasm_path --from jack --gas 200000000 -y --broadcast-mode=block \ +| jq -cr '.logs[0].events[0].attributes | map(select(.key=="code_id"))[0].value') + +echo "code_id: $code_id" + +msg=$(jq -nc \ +--arg address $(linkwasmcli keys show jack -a) \ +--arg amount 10000000000 \ +'{decimals:6,initial_balances:[{address:$address,amount:$amount}],name:"TKN",symbol:"TKN"}') + +contract_address=$(linkwasmcli tx wasm instantiate $code_id $msg --label TKN --from jack --gas 200000000 --broadcast-mode=block -y \ +| jq -cr '.logs[0].events[0].attributes | map(select(.key=="contract_address"))[0].value') + +echo "contract_address: $contract_address" + +account_number=$(linkwasmcli query account $(linkwasmcli keys show jack -a) | jq -cr '.value.account_number') +sequence=$(linkwasmcli query account $(linkwasmcli keys show jack -a) | jq -cr '.value.sequence') + +start_time="$(date -u +%s)" +for i in $(seq 0 $num_txs) +do + msg=$(jq -nc \ + --arg amount 1 \ + --arg recipient $(linkwasmcli keys show $i -a) \ + '{transfer:{amount:$amount,recipient:$recipient}}') + + linkwasmcli tx wasm execute $contract_address $msg --from $(linkwasmcli keys show jack -a) --gas 200000000 --generate-only > tx$i.unsigned.json + linkwasmcli tx sign tx$i.unsigned.json --from jack --offline -a $account_number -s $(expr $sequence + $i) > tx$i.json & +done +end_time="$(date -u +%s)" +elapsed="$(($end_time-$start_time))" +echo "$elapsed seconds elapsed for generating txs" + +killall -SIGTERM linkwasmd diff --git a/x/wasm/linkwasmd/perf/scripts/report.sh b/x/wasm/linkwasmd/perf/scripts/report.sh new file mode 100755 index 0000000000..5e0ff75681 --- /dev/null +++ b/x/wasm/linkwasmd/perf/scripts/report.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +log_path=$1 +output_file=$2 + +echo "performance report" >> $output_file +echo "" >> $output_file + +elapsed_total=0 +txs_total=0 + +while read line +do + case ${line:0:1} in + t) + txs=${line:2} + ;; + s) + start=$(date -d "${line:2}" +%s%N) + ;; + e) + end=$(date -d "${line:2}" +%s%N) + elapsed=$((end-start)) + + if [ $((txs-10)) -ge 0 ];then + echo "$txs txs in $((elapsed/1000000)) msec." >> $output_file + txs_total=$((txs_total+txs)) + elapsed_total=$((elapsed_total+elapsed)) + else + echo "$txs txs in $((elapsed/1000000)) msec. drop!" >> $output_file + fi + ;; + esac +done < $log_path + +echo "" >> $output_file +tps=$((1000000000 * txs_total/ elapsed_total)) +echo "tps:$tps" >> $output_file +echo "TPS $tps" diff --git a/x/wasm/linkwasmd/perf/tools/FlameGraph.tar.gz b/x/wasm/linkwasmd/perf/tools/FlameGraph.tar.gz new file mode 100644 index 0000000000..e85f8d9eaf Binary files /dev/null and b/x/wasm/linkwasmd/perf/tools/FlameGraph.tar.gz differ diff --git a/x/wasm/linkwasmd/types/address.go b/x/wasm/linkwasmd/types/address.go new file mode 100644 index 0000000000..39610ac388 --- /dev/null +++ b/x/wasm/linkwasmd/types/address.go @@ -0,0 +1,37 @@ +package types + +const ( + Bech32MainPrefix = "link" + Bech32TestnetPrefix = "tlink" + + // LINK in [SLIP-044](https://github.com/satoshilabs/slips/blob/master/slip-0044.md) + CoinType = 438 + FullFundraiserPath = "44'/438'/0'/0/0" +) + +func Bech32PrefixAcc(testnet bool) string { + if testnet { + return Bech32TestnetPrefix + } + return Bech32MainPrefix +} + +func Bech32PrefixAccPub(testnet bool) string { + return Bech32PrefixAcc(testnet) + PrefixPublic +} + +func Bech32PrefixValAddr(testnet bool) string { + return Bech32PrefixAcc(testnet) + PrefixValidator + PrefixOperator +} + +func Bech32PrefixValPub(testnet bool) string { + return Bech32PrefixAcc(testnet) + PrefixValidator + PrefixOperator + PrefixPublic +} + +func Bech32PrefixConsAddr(testnet bool) string { + return Bech32PrefixAcc(testnet) + PrefixValidator + PrefixConsensus +} + +func Bech32PrefixConsPub(testnet bool) string { + return Bech32PrefixAcc(testnet) + PrefixValidator + PrefixConsensus + PrefixPublic +} diff --git a/x/wasm/linkwasmd/types/alias.go b/x/wasm/linkwasmd/types/alias.go new file mode 100644 index 0000000000..e110f15ecb --- /dev/null +++ b/x/wasm/linkwasmd/types/alias.go @@ -0,0 +1,15 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + AddrLen = sdk.AddrLen // 20 + + PrefixPublic = sdk.PrefixPublic // "pub" + PrefixValidator = sdk.PrefixValidator // "val" + PrefixOperator = sdk.PrefixOperator // "oper" + PrefixConsensus = sdk.PrefixConsensus // "cons" + PrefixAddress = sdk.PrefixAddress // "addr" +) diff --git a/x/wasm/module.go b/x/wasm/module.go new file mode 100644 index 0000000000..398309fdca --- /dev/null +++ b/x/wasm/module.go @@ -0,0 +1,140 @@ +package wasm + +import ( + "encoding/json" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + "github.com/line/lbm-sdk/v2/x/wasm/client/cli" + "github.com/line/lbm-sdk/v2/x/wasm/client/rest" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic defines the basic application module used by the wasm module. +type AppModuleBasic struct{} + +// Name returns the wasm module's name. +func (AppModuleBasic) Name() string { + return ModuleName +} + +// RegisterCodec registers the wasm module's types for the given codec. +func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) { + RegisterCodec(cdc) +} + +// DefaultGenesis returns default genesis state as raw bytes for the wasm +// module. +func (AppModuleBasic) DefaultGenesis() json.RawMessage { + return ModuleCdc.MustMarshalJSON(&GenesisState{ + Params: DefaultParams(), + }) +} + +// ValidateGenesis performs genesis state validation for the wasm module. +func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error { + var data GenesisState + err := ModuleCdc.UnmarshalJSON(bz, &data) + if err != nil { + return err + } + return ValidateGenesis(data) +} + +// RegisterRESTRoutes registers the REST routes for the wasm module. +func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) { + rest.RegisterRoutes(ctx, rtr) +} + +// GetTxCmd returns the root tx command for the wasm module. +func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetTxCmd(cdc) +} + +// GetQueryCmd returns no root query command for the wasm module. +func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command { + return cli.GetQueryCmd(cdc) +} + +// ____________________________________________________________________________ + +// AppModule implements an application module for the wasm module. +type AppModule struct { + AppModuleBasic + keeper Keeper +} + +// NewAppModule creates a new AppModule object +func NewAppModule(keeper Keeper) AppModule { + return AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: keeper, + } +} + +// Name returns the wasm module's name. +func (AppModule) Name() string { + return ModuleName +} + +// RegisterInvariants registers the wasm module invariants. +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} + +// Route returns the message routing key for the wasm module. +func (AppModule) Route() string { + return RouterKey +} + +// NewHandler returns an sdk.Handler for the wasm module. +func (am AppModule) NewHandler() sdk.Handler { + return NewHandler(am.keeper) +} + +// QuerierRoute returns the wasm module's querier route name. +func (AppModule) QuerierRoute() string { + return QuerierRoute +} + +// NewQuerierHandler returns the wasm module sdk.Querier. +func (am AppModule) NewQuerierHandler() sdk.Querier { + return NewQuerier(am.keeper) +} + +// InitGenesis performs genesis initialization for the wasm module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState GenesisState + ModuleCdc.MustUnmarshalJSON(data, &genesisState) + if err := InitGenesis(ctx, am.keeper, genesisState); err != nil { + panic(err) + } + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the wasm +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return ModuleCdc.MustMarshalJSON(gs) +} + +// BeginBlock returns the begin blocker for the wasm module. +func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} + +// EndBlock returns the end blocker for the wasm module. It returns no validator +// updates. +func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + return []abci.ValidatorUpdate{} +} diff --git a/x/wasm/module_test.go b/x/wasm/module_test.go new file mode 100644 index 0000000000..2f46121200 --- /dev/null +++ b/x/wasm/module_test.go @@ -0,0 +1,509 @@ +// nolint: staticcheck, unparam, errcheck, deadcode, varcheck, unused +package wasm + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + wasmTypes "github.com/CosmWasm/wasmvm/types" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/line/lbm-sdk/v2/x/wasm/internal/types" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/libs/kv" + + "github.com/line/lbm-sdk/v2/x/wasm/internal/keeper" +) + +type testData struct { + module module.AppModule + ctx sdk.Context + acctKeeper auth.AccountKeeper + keeper Keeper +} + +// returns a cleanup function, which must be defered on +func setupTest(t *testing.T) (testData, func()) { + tempDir, err := ioutil.TempDir("", "wasm") + require.NoError(t, err) + + ctx, keepers := CreateTestInput(t, false, tempDir, "staking", nil, nil) + acctKeeper, keeper := keepers.AccountKeeper, keepers.WasmKeeper + data := testData{ + module: NewAppModule(keeper), + ctx: ctx, + acctKeeper: acctKeeper, + keeper: keeper, + } + cleanup := func() { os.RemoveAll(tempDir) } + return data, cleanup +} + +func keyPubAddr() (crypto.PrivKey, crypto.PubKey, sdk.AccAddress) { + key := ed25519.GenPrivKey() + pub := key.PubKey() + addr := sdk.AccAddress(pub.Address()) + return key, pub, addr +} + +func mustLoad(path string) []byte { + bz, err := ioutil.ReadFile(path) + if err != nil { + panic(err) + } + return bz +} + +var ( + key1, pub1, addr1 = keyPubAddr() + testContract = mustLoad("./internal/keeper/testdata/hackatom.wasm") + maskContract = mustLoad("./internal/keeper/testdata/reflect.wasm") + oldContract = mustLoad("./testdata/escrow_0.7.wasm") +) + +func TestHandleCreate(t *testing.T) { + cases := map[string]struct { + msg sdk.Msg + isValid bool + }{ + "empty": { + msg: MsgStoreCode{}, + isValid: false, + }, + "invalid wasm": { + msg: MsgStoreCode{ + Sender: addr1, + WASMByteCode: []byte("foobar"), + }, + isValid: false, + }, + "valid wasm": { + msg: MsgStoreCode{ + Sender: addr1, + WASMByteCode: testContract, + }, + isValid: true, + }, + "other valid wasm": { + msg: MsgStoreCode{ + Sender: addr1, + WASMByteCode: maskContract, + }, + isValid: true, + }, + "old wasm (0.7)": { + msg: MsgStoreCode{ + Sender: addr1, + WASMByteCode: oldContract, + }, + isValid: false, + }, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + data, cleanup := setupTest(t) + defer cleanup() + + h := data.module.NewHandler() + q := data.module.NewQuerierHandler() + + res, err := h(data.ctx, tc.msg) + if !tc.isValid { + require.Error(t, err, "%#v", res) + assertCodeList(t, q, data.ctx, 0) + assertCodeBytes(t, q, data.ctx, 1, nil) + return + } + require.NoError(t, err) + assertCodeList(t, q, data.ctx, 1) + }) + } +} + +type initMsg struct { + Verifier sdk.AccAddress `json:"verifier"` + Beneficiary sdk.AccAddress `json:"beneficiary"` +} + +type state struct { + Verifier wasmTypes.CanonicalAddress `json:"verifier"` + Beneficiary wasmTypes.CanonicalAddress `json:"beneficiary"` + Funder wasmTypes.CanonicalAddress `json:"funder"` +} + +func TestHandleInstantiate(t *testing.T) { + data, cleanup := setupTest(t) + defer cleanup() + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + creator := createFakeFundedAccount(data.ctx, data.acctKeeper, deposit) + + h := data.module.NewHandler() + q := data.module.NewQuerierHandler() + + msg := MsgStoreCode{ + Sender: creator, + WASMByteCode: testContract, + } + res, err := h(data.ctx, msg) + require.NoError(t, err) + require.Equal(t, res.Data, []byte("1")) + + _, _, bob := keyPubAddr() + _, _, fred := keyPubAddr() + + initMsg := initMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := types.ModuleCdc.MarshalJSON(initMsg) + require.NoError(t, err) + + // create with no balance is also legal + initCmd := MsgInstantiateContract{ + Sender: creator, + CodeID: firstCodeID, + InitMsg: initMsgBz, + InitFunds: nil, + } + res, err = h(data.ctx, initCmd) + require.NoError(t, err) + contractAddr := sdk.AccAddress(res.Data) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", contractAddr.String()) + // this should be standard x/wasm init event, nothing from contract + require.Equal(t, 2, len(res.Events), prettyEvents(res.Events)) + assert.Equal(t, "wasm", res.Events[0].Type) + assertAttribute(t, "contract_address", contractAddr.String(), res.Events[0].Attributes[0]) + assert.Equal(t, "message", res.Events[1].Type) + assertAttribute(t, "module", "wasm", res.Events[1].Attributes[0]) + + assertCodeList(t, q, data.ctx, 1) + assertCodeBytes(t, q, data.ctx, 1, testContract) + + assertContractList(t, q, data.ctx, 1, []string{contractAddr.String()}) + assertContractInfo(t, q, data.ctx, contractAddr, 1, creator) + assertContractState(t, q, data.ctx, contractAddr, state{ + Verifier: []byte(fred), + Beneficiary: []byte(bob), + Funder: []byte(creator), + }) +} + +func TestHandleExecute(t *testing.T) { + data, cleanup := setupTest(t) + defer cleanup() + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(data.ctx, data.acctKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(data.ctx, data.acctKeeper, topUp) + + h := data.module.NewHandler() + q := data.module.NewQuerierHandler() + + msg := MsgStoreCode{ + Sender: creator, + WASMByteCode: testContract, + } + res, err := h(data.ctx, msg) + require.NoError(t, err) + require.Equal(t, res.Data, []byte("1")) + + _, _, bob := keyPubAddr() + initMsg := initMsg{ + Verifier: fred, + Beneficiary: bob, + } + initMsgBz, err := types.ModuleCdc.MarshalJSON(initMsg) + require.NoError(t, err) + + initCmd := MsgInstantiateContract{ + Sender: creator, + CodeID: firstCodeID, + InitMsg: initMsgBz, + InitFunds: deposit, + } + res, err = h(data.ctx, initCmd) + require.NoError(t, err) + contractAddr := sdk.AccAddress(res.Data) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", contractAddr.String()) + // this should be standard x/wasm init event, plus a bank send event (2), with no custom contract events + require.Equal(t, 3, len(res.Events), prettyEvents(res.Events)) + assert.Equal(t, "transfer", res.Events[0].Type) + assert.Equal(t, "wasm", res.Events[1].Type) + assertAttribute(t, "contract_address", contractAddr.String(), res.Events[1].Attributes[0]) + assert.Equal(t, "message", res.Events[2].Type) + assertAttribute(t, "module", "wasm", res.Events[2].Attributes[0]) + + // ensure bob doesn't exist + bobAcct := data.acctKeeper.GetAccount(data.ctx, bob) + require.Nil(t, bobAcct) + + // ensure funder has reduced balance + creatorAcct := data.acctKeeper.GetAccount(data.ctx, creator) + require.NotNil(t, creatorAcct) + // we started at 2*deposit, should have spent one above + assert.Equal(t, deposit, creatorAcct.GetCoins()) + + // ensure contract has updated balance + contractAcct := data.acctKeeper.GetAccount(data.ctx, contractAddr) + require.NotNil(t, contractAcct) + assert.Equal(t, deposit, contractAcct.GetCoins()) + + execCmd := MsgExecuteContract{ + Sender: fred, + Contract: contractAddr, + Msg: []byte(`{"release":{}}`), + SentFunds: topUp, + } + res, err = h(data.ctx, execCmd) + require.NoError(t, err) + // this should be standard x/wasm init event, plus 2 bank send event, plus a special event from the contract + require.Equal(t, 4, len(res.Events), prettyEvents(res.Events)) + assert.Equal(t, "transfer", res.Events[0].Type) + assertAttribute(t, "sender", fred.String(), res.Events[0].Attributes[0]) + assertAttribute(t, "recipient", contractAddr.String(), res.Events[0].Attributes[1]) + assertAttribute(t, "amount", "5000denom", res.Events[0].Attributes[2]) + // custom contract event + assert.Equal(t, "wasm", res.Events[1].Type) + assertAttribute(t, "contract_address", contractAddr.String(), res.Events[1].Attributes[0]) + assertAttribute(t, "action", "release", res.Events[1].Attributes[1]) + // second transfer (this without conflicting message) + assert.Equal(t, "transfer", res.Events[2].Type) + assertAttribute(t, "sender", contractAddr.String(), res.Events[2].Attributes[0]) + assertAttribute(t, "recipient", bob.String(), res.Events[2].Attributes[1]) + assertAttribute(t, "amount", "105000denom", res.Events[2].Attributes[2]) + // finally, standard x/wasm tag + assert.Equal(t, "message", res.Events[3].Type) + assertAttribute(t, "module", "wasm", res.Events[3].Attributes[0]) + + // ensure bob now exists and got both payments released + bobAcct = data.acctKeeper.GetAccount(data.ctx, bob) + require.NotNil(t, bobAcct) + balance := bobAcct.GetCoins() + assert.Equal(t, deposit.Add(topUp...), balance) + + // ensure contract has updated balance + contractAcct = data.acctKeeper.GetAccount(data.ctx, contractAddr) + require.NotNil(t, contractAcct) + assert.Equal(t, sdk.Coins(nil), contractAcct.GetCoins()) + + // 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{contractAddr.String()}) + assertContractInfo(t, q, data.ctx, contractAddr, 1, creator) + assertContractState(t, q, data.ctx, contractAddr, state{ + Verifier: []byte(fred), + Beneficiary: []byte(bob), + Funder: []byte(creator), + }) +} + +func TestHandleExecuteEscrow(t *testing.T) { + data, cleanup := setupTest(t) + defer cleanup() + + deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000)) + topUp := sdk.NewCoins(sdk.NewInt64Coin("denom", 5000)) + creator := createFakeFundedAccount(data.ctx, data.acctKeeper, deposit.Add(deposit...)) + fred := createFakeFundedAccount(data.ctx, data.acctKeeper, topUp) + + h := data.module.NewHandler() + + msg := MsgStoreCode{ + Sender: creator, + WASMByteCode: testContract, + } + res, err := h(data.ctx, msg) + require.NoError(t, err) + require.Equal(t, res.Data, []byte("1")) + + _, _, bob := keyPubAddr() + initMsg := map[string]interface{}{ + "verifier": fred.String(), + "beneficiary": bob.String(), + } + initMsgBz, err := json.Marshal(initMsg) + require.NoError(t, err) + + initCmd := MsgInstantiateContract{ + Sender: creator, + CodeID: firstCodeID, + InitMsg: initMsgBz, + InitFunds: deposit, + } + res, err = h(data.ctx, initCmd) + require.NoError(t, err) + contractAddr := sdk.AccAddress(res.Data) + require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", contractAddr.String()) + + handleMsg := map[string]interface{}{ + "release": map[string]interface{}{}, + } + handleMsgBz, err := json.Marshal(handleMsg) + require.NoError(t, err) + + execCmd := MsgExecuteContract{ + Sender: fred, + Contract: contractAddr, + Msg: handleMsgBz, + SentFunds: topUp, + } + res, err = h(data.ctx, execCmd) + require.NoError(t, err) + + // ensure bob now exists and got both payments released + bobAcct := data.acctKeeper.GetAccount(data.ctx, bob) + require.NotNil(t, bobAcct) + balance := bobAcct.GetCoins() + assert.Equal(t, deposit.Add(topUp...), balance) + + // ensure contract has updated balance + contractAcct := data.acctKeeper.GetAccount(data.ctx, contractAddr) + require.NotNil(t, contractAcct) + assert.Equal(t, sdk.Coins(nil), contractAcct.GetCoins()) +} + +type prettyEvent struct { + Type string + Attr []sdk.Attribute +} + +func prettyEvents(evts sdk.Events) string { + res := make([]prettyEvent, len(evts)) + for i, e := range evts { + res[i] = prettyEvent{ + Type: e.Type, + Attr: prettyAttrs(e.Attributes), + } + } + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, res) + if err != nil { + panic(err) + } + return string(bz) +} + +func prettyAttrs(attrs []kv.Pair) []sdk.Attribute { + pretty := make([]sdk.Attribute, len(attrs)) + for i, a := range attrs { + pretty[i] = prettyAttr(a) + } + return pretty +} + +func prettyAttr(attr kv.Pair) sdk.Attribute { + return sdk.NewAttribute(string(attr.Key), string(attr.Value)) +} + +func assertAttribute(t *testing.T, key string, value string, attr kv.Pair) { + assert.Equal(t, key, string(attr.Key), prettyAttr(attr)) + assert.Equal(t, value, string(attr.Value), prettyAttr(attr)) +} + +func assertCodeList(t *testing.T, q sdk.Querier, ctx sdk.Context, expectedNum int) { + bz, sdkerr := q(ctx, []string{QueryListCode}, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + if len(bz) == 0 { + require.Equal(t, expectedNum, 0) + return + } + + var res []CodeInfo + err := types.ModuleCdc.UnmarshalJSON(bz, &res) + require.NoError(t, err) + + assert.Equal(t, expectedNum, len(res)) +} + +func assertCodeBytes(t *testing.T, q sdk.Querier, ctx sdk.Context, codeID uint64, expectedBytes []byte) { + path := []string{QueryGetCode, fmt.Sprintf("%d", codeID)} + bz, sdkerr := q(ctx, path, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + if len(bz) == 0 { + require.Equal(t, len(expectedBytes), 0) + return + } + + var res CodeInfoResponse + err := types.ModuleCdc.UnmarshalJSON(bz, &res) + require.NoError(t, err) + + assert.Equal(t, expectedBytes, res.GetData()) + assert.Equal(t, codeID, res.GetID()) +} + +func assertContractList(t *testing.T, q sdk.Querier, ctx sdk.Context, codeID uint64, addrs []string) { + bz, sdkerr := q(ctx, []string{QueryListContractByCode, fmt.Sprintf("%d", codeID)}, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + if len(bz) == 0 { + require.Equal(t, len(addrs), 0) + return + } + + var res []ContractInfoResponse + err := types.ModuleCdc.UnmarshalJSON(bz, &res) + require.NoError(t, err) + + var hasAddrs = make([]string, len(res)) + for i, r := range res { + hasAddrs[i] = r.GetAddress().String() + } + + assert.Equal(t, hasAddrs, addrs) +} + +func assertContractState(t *testing.T, q sdk.Querier, ctx sdk.Context, addr sdk.AccAddress, expected state) { + path := []string{QueryGetContractState, addr.String(), keeper.QueryMethodContractStateAll} + bz, sdkerr := q(ctx, path, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + var res []Model + err := types.ModuleCdc.UnmarshalJSON(bz, &res) + require.NoError(t, err) + require.Equal(t, 1, len(res), "#v", res) + require.Equal(t, []byte("config"), []byte(res[0].Key)) + + expectedBz, err := types.ModuleCdc.MarshalJSON(expected) + require.NoError(t, err) + assert.Equal(t, expectedBz, res[0].Value) +} + +func assertContractInfo(t *testing.T, q sdk.Querier, ctx sdk.Context, addr sdk.AccAddress, codeID uint64, creator sdk.AccAddress) { + path := []string{QueryGetContract, addr.String()} + bz, sdkerr := q(ctx, path, abci.RequestQuery{}) + require.NoError(t, sdkerr) + + var res ContractInfoResponse + err := types.ModuleCdc.UnmarshalJSON(bz, &res) + require.NoError(t, err) + + assert.Equal(t, codeID, res.GetCodeID()) + assert.Equal(t, creator, res.GetCreator()) +} + +func createFakeFundedAccount(ctx sdk.Context, am auth.AccountKeeper, coins sdk.Coins) sdk.AccAddress { + _, _, addr := keyPubAddr() + baseAcct := auth.NewBaseAccountWithAddress(addr) + _ = baseAcct.SetCoins(coins) + am.SetAccount(ctx, &baseAcct) + + return addr +} diff --git a/x/wasm/testdata/escrow_0.7.wasm b/x/wasm/testdata/escrow_0.7.wasm new file mode 100644 index 0000000000..668aa74e3b Binary files /dev/null and b/x/wasm/testdata/escrow_0.7.wasm differ