diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 239404ee44..43b5f07e69 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,4 +4,4 @@ *.md @barriebyron # Primary repo maintainers -* @fadeev @ilgooz @lubtd @Pantani +* @fadeev @ilgooz @lubtd @Pantani @ivanovpetr diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 87d939b5c5..c273944cd7 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -1,23 +1,70 @@ -name: Test - +name: Integration on: pull_request: push: + paths-ignore: + - '**.md' branches: - master - develop jobs: - integration: + pre-test: runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v2 + - name: Finding files and store to output + id: set-matrix + run: echo "::set-output name=matrix::$({ cd integration && find . -type d -maxdepth 1 -print; } | tail -n +2 | cut -c 3- | jq -R . | jq -cs .)" + integration: + name: test ${{ matrix.test-path }} + runs-on: ubuntu-latest + needs: pre-test + if: fromJSON(needs.pre-test.outputs.matrix)[0] != null + continue-on-error: false + strategy: + fail-fast: true + matrix: + test-path: ${{fromJson(needs.pre-test.outputs.matrix)}} steps: - uses: actions/checkout@v2 + - uses: technote-space/get-diff-action@v4 + with: + PATTERNS: | + **/* + !**/*.md + - uses: actions/cache@v2 + if: env.GIT_DIFF + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- - uses: actions/setup-go@v2 + if: env.GIT_DIFF with: go-version: 1.16 - name: Install Starport + if: env.GIT_DIFF run: go install ./... - - name: Run Integration Tests - run: ./scripts/test-integration + if: env.GIT_DIFF + run: go test -v -timeout 35m ./integration/${{ matrix.test-path }} + + status: + runs-on: ubuntu-latest + needs: integration + if: always() + steps: + - name: Update result status + run: | + if [ "${{ needs.integration.result }}" = "failure" ]; then + exit 1 + else + exit 0 + fi diff --git a/.github/workflows/test-lint.yml b/.github/workflows/test-lint.yml index b7c01b62bf..6072afd8e9 100644 --- a/.github/workflows/test-lint.yml +++ b/.github/workflows/test-lint.yml @@ -2,6 +2,8 @@ name: Test on: pull_request: push: + paths-ignore: + - '**.md' branches: - master - develop @@ -11,10 +13,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 6 steps: - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.16 - uses: actions/checkout@v2 - uses: technote-space/get-diff-action@v4 with: @@ -22,10 +20,15 @@ jobs: **/**.go go.mod go.sum + - name: Set up Go + uses: actions/setup-go@v2 + if: env.GIT_DIFF + with: + go-version: 1.16 - uses: golangci/golangci-lint-action@master + if: env.GIT_DIFF with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. version: v1.37 args: --timeout 10m github-token: ${{ secrets.github_token }} - if: env.GIT_DIFF diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b624544a3..3fcce2717e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,8 @@ name: Test on: pull_request: push: + paths-ignore: + - '**.md' branches: - master - develop @@ -11,7 +13,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: technote-space/get-diff-action@v4 + with: + PATTERNS: | + **/* + !**/*.md - uses: actions/setup-go@v2 + if: env.GIT_DIFF with: go-version: 1.16 - - run: ./scripts/test-unit + - run: ./scripts/test-unit + if: env.GIT_DIFF + diff --git a/assets/starport.jpg b/assets/starport.jpg deleted file mode 100644 index 6a42a39c80..0000000000 Binary files a/assets/starport.jpg and /dev/null differ diff --git a/assets/starport.png b/assets/starport.png new file mode 100644 index 0000000000..ecce5a53b9 Binary files /dev/null and b/assets/starport.png differ diff --git a/changelog.md b/changelog.md index 44c80a5d02..3e9b406fd4 100644 --- a/changelog.md +++ b/changelog.md @@ -4,8 +4,20 @@ ### Features: +- `starport scaffold` commands support `ints`, `uints`, `strings`, `coin`, `coins` as field types (#1579) +- Simulation testing with `simapp` has been added to the default template (#1731) - Added `starport generate dart` to generate a Dart client from protocol buffer files - Added `starport scaffold flutter` to scaffold a Flutter mobile app template +- Parameters can be specified with a new `--params` flag when scaffolding modules (#1716) +- Simulations can be run with `starport chain simulate` +- `cointype` for accounts can be set in the `config.yml` (#1663) + +### Fixes: + +- Allow using a `creator` field when scaffolding a model with a `--no-message` flag (#1730) +- Improved error handling when generating code (#1907) +- Ensure account has funds after faucet transfer when using `cosmosclient` (#1846) +- Move from `io/ioutil` to `io` and `os` package (refactoring) (#1746) ## `v0.18.0` diff --git a/contributing.md b/contributing.md index 1fd9c9eb4f..2ae16e89f4 100644 --- a/contributing.md +++ b/contributing.md @@ -10,7 +10,7 @@ Thank you for your contribution! * Before you open an issue, do a web search, and check for [existing open and closed GitHub Issues](https://github.com/tendermint/starport/issues) to see if your question has already been asked and answered. If you find a relevant topic, you can comment on that issue. * To provide feedback or ask a question, create a [GitHub issue](https://github.com/tendermint/starport/issues/new/choose). Be sure to provide the relevant information, case study, or informative links as suggested by the Pull Request template. -* We recommend using GitHub issues for issues and feedback. However, you can ask quick questions on the [#🔨cosmos-sdk-starport](https://discord.com/channels/669268347736686612/737461683588431924) channel in Discord. +* We recommend using GitHub issues for issues and feedback. However, you can ask quick questions on the **#🛠️ build-chains** channel in the official [Starport Discord](https://discord.gg/starport). ## Opening pull requests @@ -36,12 +36,18 @@ A reviewer likes to see a linear commit history while reviewing. If you tend to Don't worry about adding too many commits. The commits are squashed into a single commit while merging. Your PR title is used as the commit message. +## Contributing to documentation + +When you open a PR for the Starport codebase, you must also update the relevant documentation. For changes to: + +- [Developer Guide](https://docs.starport.network/guide/) tutorials, update content in the `/docs/guide` folder. +- [Knowledge Base](https://docs.starport.network/kb/), update content in the `/docs/kb` folder. +- [Starport CLI reference](https://github.com/tendermint/starport/blob/f668bba58c04318f98db8cac0c9e154fa7e7ea34/docs/cli/index.md), navigate to the `./starport/cmd` package and update the documentation of the related command from its `cobra.Command` struct. The CLI docs are automatically generated, so do not make changes to `docs/cli/index.md`. + ### Ask for help If you started a PR but couldn't finish it for whatever reason, don't give up. Instead, just ask for help. Someone else can take over and assume the ownership. We appreciate every bit of your work! -## Contributing to documentation -[CLI reference](https://github.com/tendermint/starport/blob/f668bba58c04318f98db8cac0c9e154fa7e7ea34/docs/cli/index.md) is generated automatically with a [GitHub action](https://github.com/tendermint/starport/blob/f668bba58c04318f98db8cac0c9e154fa7e7ea34/.github/workflows/gen-docs-cli.yml). Instead of editing `docs/cli/index.md` directly (which will be overwritten by the GH action), please, make changes to the relevant parts of the source code that describe CLI commands. diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index f57afaae99..e9b1c986ee 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -18,6 +18,9 @@ module.exports = { ], ], themeConfig: { + logo: { + src: "/logo.png", + }, algolia: { id: "BH4D9OD16A", key: "d6908a9436133e03e9b0131bad808775", @@ -48,37 +51,33 @@ module.exports = { footer: { question: { text: - "Chat with Starport and Cosmos SDK developers in Discord.", + "Chat with Starport and Cosmos SDK developers in Discord.", }, logo: "/logo.svg", textLink: { - text: "cosmos.network/starport", - url: "https://cosmos.network/starport", + text: "starport.com", + url: "https://starport.com/", }, services: [ { service: "medium", - url: "https://blog.cosmos.network/", + url: "https://medium.com/tendermint", }, { service: "twitter", - url: "https://twitter.com/cosmos", + url: "https://twitter.com/starportHQ", }, { service: "linkedin", url: "https://www.linkedin.com/company/tendermint/", }, - { - service: "reddit", - url: "https://reddit.com/r/cosmosnetwork", - }, { service: "discord", - url: "https://discord.gg/vcExX9T", + url: "https://discord.gg/starport", }, { service: "youtube", - url: "https://www.youtube.com/c/CosmosProject", + url: "https://www.youtube.com/channel/UCXMndYLK7OuvjvElSeSWJ1Q", }, ], @@ -115,7 +114,7 @@ module.exports = { }, { title: "Chat", - url: "https://discord.gg/cosmosnetwork", + url: "https://discord.gg/starport", }, ], }, diff --git a/docs/.vuepress/public/logo.png b/docs/.vuepress/public/logo.png new file mode 100644 index 0000000000..d006ca3025 Binary files /dev/null and b/docs/.vuepress/public/logo.png differ diff --git a/docs/.vuepress/public/logo.svg b/docs/.vuepress/public/logo.svg index f2575260a7..880c3f6013 100644 --- a/docs/.vuepress/public/logo.svg +++ b/docs/.vuepress/public/logo.svg @@ -1,8 +1,11 @@ - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 4c7c1505fe..700f91f055 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,7 +33,7 @@ Many projects already showcase the Tendermint BFT consensus engine and the Cosmo * [crypto.org chain](https://github.com/crypto-org-chain/chain-main) ([initialized with Starport](https://github.com/crypto-org-chain/chain-main/commit/37b2ecb49a9aae7c581270a4f2dbecfcd8e8a6e9)) * [Cronos](https://github.com/crypto-org-chain/cronos) * [Plugchain](https://github.com/oracleNetworkProtocol/plugchain) -* [BitCanna](https://github.com/BitCannaGlobal/testnet-bcna-cosmos) +* [BitCanna](https://github.com/BitCannaGlobal/bcna) * [Panacea Core](https://github.com/medibloc/panacea-core) * [Rook](https://github.com/cmwaters/rook) * [PI Bridge](https://github.com/pchain-org/pi-bridge) diff --git a/docs/cli/index.md b/docs/cli/index.md index 9a48b25b30..14bc388f03 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -6,7 +6,7 @@ parent: title: CLI Reference --- -# Reference +# CLI Reference Documentation for Starport CLI. @@ -36,7 +36,6 @@ starport scaffold chain github.com/cosmonaut/mars * [starport chain](#starport-chain) - Build, initialize and start a blockchain node or perform other actions on the blockchain * [starport docs](#starport-docs) - Show Starport docs * [starport generate](#starport-generate) - Generate clients, API docs from source code -* [starport network](#starport-network) - Launch a blockchain network in production * [starport relayer](#starport-relayer) - Connect blockchains by using IBC protocol * [starport scaffold](#starport-scaffold) - Scaffold a new blockchain, module, message, query, and more * [starport tools](#starport-tools) - Tools for advanced users @@ -236,8 +235,8 @@ source. Specify the release targets with GOOS:GOARCH build tags. If the optional --release.targets is not specified, a binary is created for your current environment. Sample usages: - - starport build - - starport build --release -t linux:amd64 -t darwin:amd64 -t darwin:arm64 + - starport chain build + - starport chain build --release -t linux:amd64 -t darwin:amd64 -t darwin:arm64 ``` starport chain build [flags] @@ -477,561 +476,6 @@ starport generate vuex [flags] * [starport generate](#starport-generate) - Generate clients, API docs from source code -## starport network - -Launch a blockchain network in production - -**Options** - -``` - -h, --help help for network - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport](#starport) - Starport offers everything you need to scaffold, test, build, and launch your blockchain -* [starport network account](#starport-network-account) - Show the underlying SPN account -* [starport network chain](#starport-network-chain) - Build networks -* [starport network proposal](#starport-network-proposal) - Proposals related to starting network - - -## starport network account - -Show the underlying SPN account - -**Synopsis** - -Show the underlying SPN account. -To pick another account see "starport network account use -h" -If no account is picked, default "spn" account is used. - - -``` -starport network account [flags] -``` - -**Options** - -``` - -h, --help help for account -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network](#starport-network) - Launch a blockchain network in production -* [starport network account create](#starport-network-account-create) - Create an account -* [starport network account export](#starport-network-account-export) - Export an account -* [starport network account import](#starport-network-account-import) - Import an account -* [starport network account use](#starport-network-account-use) - Pick an account to be used with Starport Network - - -## starport network account create - -Create an account - -``` -starport network account create [name] [flags] -``` - -**Options** - -``` - -h, --help help for create -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network account](#starport-network-account) - Show the underlying SPN account - - -## starport network account export - -Export an account - -``` -starport network account export [flags] -``` - -**Options** - -``` - -a, --account string path to save private key (default "[account in use]") - -h, --help help for export - -p, --path string path to save private key (default "[account].key") -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network account](#starport-network-account) - Show the underlying SPN account - - -## starport network account import - -Import an account - -``` -starport network account import [name] [password] [path-to-private-key] [flags] -``` - -**Options** - -``` - -h, --help help for import -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network account](#starport-network-account) - Show the underlying SPN account - - -## starport network account use - -Pick an account to be used with Starport Network - -**Synopsis** - -Pick one of the accounts in OS keyring to put into use or provide one with --name flag. -Picked account will be used while interacting with Starport Network. - -``` -starport network account use [flags] -``` - -**Options** - -``` - -h, --help help for use - -n, --name string Account name to put into use -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network account](#starport-network-account) - Show the underlying SPN account - - -## starport network chain - -Build networks - -**Options** - -``` - -h, --help help for chain -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network](#starport-network) - Launch a blockchain network in production -* [starport network chain create](#starport-network-chain-create) - Create a new network -* [starport network chain join](#starport-network-chain-join) - Propose to join to a network as a validator -* [starport network chain list](#starport-network-chain-list) - List all chains with proposals summary -* [starport network chain show](#starport-network-chain-show) - Show details of a chain -* [starport network chain start](#starport-network-chain-start) - Start network - - -## starport network chain create - -Create a new network - -``` -starport network chain create [chain] [source] [flags] -``` - -**Options** - -``` - --branch string Git branch to use - --genesis string URL to a custom Genesis - -h, --help help for create - --home string Home directory used for blockchains - --tag string Git tag to use -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network chain](#starport-network-chain) - Build networks - - -## starport network chain join - -Propose to join to a network as a validator - -``` -starport network chain join [chain-id] [flags] -``` - -**Options** - -``` - --gentx string Path to a gentx file (optional) - -h, --help help for join - --home string Home directory used for blockchains - --keyring-backend string Keyring backend used for the blockchain account (default "os") - --peer string Configure peer in node-id@host:port format (optional) -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network chain](#starport-network-chain) - Build networks - - -## starport network chain list - -List all chains with proposals summary - -``` -starport network chain list [flags] -``` - -**Options** - -``` - -h, --help help for list - --search string List chains with the specified prefix in chain id -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network chain](#starport-network-chain) - Build networks - - -## starport network chain show - -Show details of a chain - -``` -starport network chain show [chain-id] [flags] -``` - -**Options** - -``` - --genesis Show exclusively the genesis of the chain - -h, --help help for show - --peers Show exclusively the peers of the chain -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network chain](#starport-network-chain) - Build networks - - -## starport network chain start - -Start network - -``` -starport network chain start [chain-id] [-- ...] [flags] -``` - -**Options** - -``` - -h, --help help for start - --home string Home directory used for blockchains -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network chain](#starport-network-chain) - Build networks - - -## starport network proposal - -Proposals related to starting network - -**Options** - -``` - -h, --help help for proposal -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network](#starport-network) - Launch a blockchain network in production -* [starport network proposal approve](#starport-network-proposal-approve) - Approve proposals -* [starport network proposal list](#starport-network-proposal-list) - List all pending proposals -* [starport network proposal reject](#starport-network-proposal-reject) - Reject proposals -* [starport network proposal show](#starport-network-proposal-show) - Show details of a proposal -* [starport network proposal verify](#starport-network-proposal-verify) - Simulate and verify proposals validity - - -## starport network proposal approve - -Approve proposals - -``` -starport network proposal approve [chain-id] [number<,...>] [flags] -``` - -**Options** - -``` - -h, --help help for approve - --home string Home directory used for blockchains - --no-verification approve the proposals without verifying them -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network proposal](#starport-network-proposal) - Proposals related to starting network - - -## starport network proposal list - -List all pending proposals - -``` -starport network proposal list [chain-id] [flags] -``` - -**Options** - -``` - -h, --help help for list - --status string Filter proposals by status (pending|approved|rejected) - --type string Filter proposals by type (add-account|add-validator) -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network proposal](#starport-network-proposal) - Proposals related to starting network - - -## starport network proposal reject - -Reject proposals - -``` -starport network proposal reject [chain-id] [number<,...>] [flags] -``` - -**Options** - -``` - -h, --help help for reject -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network proposal](#starport-network-proposal) - Proposals related to starting network - - -## starport network proposal show - -Show details of a proposal - -``` -starport network proposal show [chain-id] [number] [flags] -``` - -**Options** - -``` - -h, --help help for show -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network proposal](#starport-network-proposal) - Proposals related to starting network - - -## starport network proposal verify - -Simulate and verify proposals validity - -``` -starport network proposal verify [chain-id] [number<,...>] [flags] -``` - -**Options** - -``` - --debug show output of verification command in the console - -h, --help help for verify - --home string Home directory used for blockchains -``` - -**Options inherited from parent commands** - -``` - --local Use local SPN network - --nightly Use nightly SPN network - --spn-api-address string SPN api address (default "https://rest.alpha.starport.network") - --spn-faucet-address string SPN Faucet address (default "https://faucet.alpha.starport.network") - --spn-node-address string SPN node address (default "https://rpc.alpha.starport.network:443") -``` - -**SEE ALSO** - -* [starport network proposal](#starport-network-proposal) - Proposals related to starting network - - ## starport relayer Connect blockchains by using IBC protocol diff --git a/docs/contributing/index.md b/docs/contributing/index.md index 17c8cab984..588b3b5662 100644 --- a/docs/contributing/index.md +++ b/docs/contributing/index.md @@ -1,9 +1,4 @@ ---- -parent: - order: false ---- - -# Contributing +# Contributing to Starport docs - [Contributing](#contributing) - [Using this repo](#using-this-repo) @@ -16,23 +11,29 @@ parent: - [Preview Draft PRs on a Local Web Browser](#preview-draft-prs-on-a-local-web-browser) -Thank you for considering making contributions. We appreciate your interest in helping us to create and maintain awesome tutorials. +Thank you for considering making contributions. We appreciate your interest in helping us to create and maintain awesome tutorials and documentation. -To set up your environment for success, follow our [technical set up](technical-setup.md) guidelines. +To set up your environment for success, follow the [technical setup](technical-setup.md) guidelines. ## Using this repo -- To provide feedback, file an issue and provide abundant details to help us understand how we can make it better. -- To provide feedback and a fix, you can make a direct contribution. This repo is protected since we provide the code and the docs to help you learn. If you're not a member or maintainer, fork the repo and then submit a pull request (PR) from your forked repo to the `develop` branch. +Review existing [Starport issues](https://github.com/tendermint/starport/issues) to see if your question has already been asked and answered. + +- To provide feedback, file an issue and provide generous details to help us understand how we can make it better. +- To provide a fix, make a direct contribution. If you're not a member or maintainer, fork the repo and then submit a pull request (PR) from your forked repo to the `develop` branch. - Start by creating a draft pull request. Create your draft PR early, even if your work is just beginning or incomplete. Your draft PR indicates to the community that you're working on something and provides a space for conversations early in the development process. Merging is blocked for `Draft` PRs, so they provide a safe place to experiment and invite comments. ## Reviewing technical content PRs -Some of the best content contributions come during the PR review cycles. Follow best practices for technical content PR reviews just like you do for code reviews. For in-line suggestions, use the [GitHub suggesting feature](https://docs.github.com/en/github/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request). The PR owner can merge in your suggested commits one at a time or in batch (preferred). When you do a more granular extensive review that results in more than 20 in-line suggestions, go ahead and check out the branch and make the changes yourself. +Some of the best content contributions come during the PR review cycles. Follow best practices for technical content PR reviews just like you do for code reviews. + +- For in-line suggestions, use the [GitHub suggesting feature](https://docs.github.com/en/github/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/commenting-on-a-pull-request). +- The PR owner can merge in your suggested commits one at a time or in batch (preferred). +- When you do a more granular extensive review that results in more than 20 in-line suggestions, go ahead and check out the branch and make the changes yourself. ## Writing and contributing -We welcome contributions to the tutorials. +We welcome contributions to the docs and tutorials. Our technical content follows the [Google developer documentation style guide](https://developers.google.com/style). Highlights to help you get started: @@ -52,10 +53,10 @@ Other useful resources: ## Where can I find the tutorials and docs? -Technical content includes Knowledge Base article and interactive tutorials. +Technical content includes knowledge base articles and interactive tutorials. +- The Starport Developer Tutorials content is in the `docs/guide` folder. - The Knowledge Base content is in the `docs/kb` folder. -- The Starport Developer Guide content is in the `docs/guide` folder. Locations and folders for other content can vary. Explore the self-describing folders for the content that you are interested in. Some articles and tutorials reside in a single Markdown file while sub-folders might be present for other tutorials. @@ -69,7 +70,7 @@ The Tendermint (All in Bits) Ecosystem Development team owns the technical conte Meet the [people behind Starport and contributors](https://github.com/tendermint/starport/graphs/contributors). -## Viewing Tutorial Builds +## Viewing tutorial builds There are two ways to see what your changes will look like in production before the updated pages are published. @@ -84,9 +85,9 @@ To view a deployed preview on a **Ready for review** PR, click the **Details** l ![deploy-preview](./deploy-preview.png) -### Preview Draft PRs on a Local Web Browser +### Preview draft PRs on a local web browser -Since the deploy preview doesn't work on Draft PRs, follow these steps to preview the tutorial build on a local web browser. +Since the deploy preview doesn't work on Draft PRs, follow these steps to preview a tutorial build on a local web browser. 1. If you haven't already, clone the tutorials repo to your local machine and change to that directory. For example: diff --git a/docs/contributing/technical-setup.md b/docs/contributing/technical-setup.md index 09a1244558..431d0ecf4b 100644 --- a/docs/contributing/technical-setup.md +++ b/docs/contributing/technical-setup.md @@ -1,6 +1,6 @@ # Technical Setup -To ensure you have a successful experience working with our Developer Guide content, Tendermint recommends this technical setup. +To ensure you have a successful experience working with our Developer Tutorials content, Tendermint recommends this technical setup. ## Setting Up Visual Studio Code @@ -20,9 +20,9 @@ Be sure to set up [Visual Studio Code](https://code.visualstudio.com/docs/setup/ Click the GitHub icon in the sidebar for GitHub integration and follow the prompts. -## Clone the repos you work in +## Clone the Repos You Work In -- Fork or clone the repository. +- Fork or clone the repository. Internal Tendermint users have different permissions, if you're not sure, fork the repo. diff --git a/docs/contributing/templates/concept_template.md b/docs/contributing/templates/concept_template.md index 89c9e6dd11..8928a18ffc 100644 --- a/docs/contributing/templates/concept_template.md +++ b/docs/contributing/templates/concept_template.md @@ -1,7 +1,7 @@ # Understanding [Some Concept] @@ -60,7 +62,7 @@ To complete this tutorial, you will need: diff --git a/docs/guide/blog/connect-blockchain.md b/docs/guide/blog/connect-blockchain.md new file mode 100644 index 0000000000..5d7f46f6e9 --- /dev/null +++ b/docs/guide/blog/connect-blockchain.md @@ -0,0 +1,203 @@ +--- +description: Blockchain Client in Go +order: 2 +--- + +# Create a Blockchain Client in Go + +Learn how to connect your blockchain to an independent application with RPC. + +After creating the blog blockchain in this tutorial you will learn how to connect to your blockchain from a separate client. + +## Use the Blog Blockchain + +Navigate to a separate directory right next to the `blog` blockchain you built in the [Build a Blog](index.md) tutorial. + +## Creating a Blockchain Client + +Create a new directory called `blogclient` on the same level as `blog` directory. As the name suggests, `blogclient` will contain a standalone Go program that acts as a client to your `blog` blockchain. + +The command: + +```bash +ls +``` + +Shows just `blog` now. More results are listed when you have more directories here. + +Create your `blogclient` directory first, change your current working directory, and initialize the new Go module. + +```bash +mkdir blogclient +cd blogclient +go mod init github.com/cosmonaut/blogclient +``` + +The `go.mod` file is created inside your `blogclient` directory. + +Create the `main.go` file and add the content as follows. + +Your blockchain client has only two dependencies: + +- The `blog` blockchain `types` for message types and a query client +- `starport` for the `cosmosclient` blockchain client + +```go +module github.com/cosmonaut/blogclient + +go 1.17 + +require ( + github.com/cosmonaut/blog v0.0.0-00010101000000-000000000000 + github.com/tendermint/starport v0.18.0 +) + +replace github.com/cosmonaut/blog => ../blog +replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 +``` + +The `replace` directive uses the package from the local `blog` directory and is specified as a relative path. + +Cosmos SDK uses a custom version of the `protobuf` package, so use the `replace` directive to specify the correct dependency. + +The `blogclient` will eventually have only two files: + +- `main.go` for the main logic of the client +- `go.mod` for specifying dependencies. + +### Main Logic of the Client in main.go + +Add the following code to your `main.go` file to make a connection to your blockchain from a separate app. + +```go +package main + +import ( + "context" + "fmt" + "log" + + // importing the types package of your blog blockchain + "github.com/cosmonaut/blog/x/blog/types" + // importing the general purpose Cosmos blockchain client + "github.com/tendermint/starport/starport/pkg/cosmosclient" +) + +func main() { + + // create an instance of cosmosclient + cosmos, err := cosmosclient.New(context.Background()) + if err != nil { + log.Fatal(err) + } + + // account `alice` was initialized during `starport chain serve` + accountName := "alice" + + // get account from the keyring by account name and return a bech32 address + address, err := cosmos.Address(accountName) + if err != nil { + log.Fatal(err) + } + + // define a message to create a post + msg := &types.MsgCreatePost{ + Creator: address.String(), + Title: "Hello!", + Body: "This is the first post", + } + + // broadcast a transaction from account `alice` with the message to create a post + // store response in txResp + txResp, err := cosmos.BroadcastTx(accountName, msg) + if err != nil { + log.Fatal(err) + } + + // print response from broadcasting a transaction + fmt.Print("MsgCreatePost:\n\n") + fmt.Println(txResp) + + // instantiate a query client for your `blog` blockchain + queryClient := types.NewQueryClient(cosmos.Context) + + // query the blockchain using the client's `PostAll` method to get all posts + // store all posts in queryResp + queryResp, err := queryClient.Posts(context.Background(), &types.QueryPostsRequest{}) + if err != nil { + log.Fatal(err) + } + + // print response from querying all the posts + fmt.Print("\n\nAll posts:\n\n") + fmt.Println(queryResp) +} +``` + +Read the comments in the code carefully to learn details about each line of code. + +To learn more about the `cosmosclient` package, see the Go +[cosmosclient](https://pkg.go.dev/github.com/tendermint/starport/starport/pkg/cosmosclient) package documentation. Details are provided to learn how to use the `Client` type with `Options` and `KeyringBackend`. + +## Running the Blockchain and the Client + +Make sure your blog blockchain is still running with `starport chain serve`. + +Install dependencies for your `blogclient`: + +```bash +cd blogclient + +go mod tidy +``` + +Run the blockchain client: + +```bash +go run main.go +``` + +If successful, the results of running the command are printed to the terminal: + +```bash +go run main.go +# github.com/keybase/go-keychain +### Some warnings might be displayed which can be ignored +MsgCreatePost: + +Response: + Height: 3222 + TxHash: AFCA76B0FEE5113382C068967B610180C105FCE045FF8C7943EA45EF4B7A1E69 + Data: 0A280A222F636F736D6F6E6175742E626C6F672E626C6F672E4D7367437265617465506F737412020801 + Raw Log: [{"events":[{"type":"message","attributes":[{"key":"action","value":"CreatePost"}]}]}] + Logs: [{"events":[{"type":"message","attributes":[{"key":"action","value":"CreatePost"}]}]}] + GasWanted: 300000 + GasUsed: 45805 + + +All posts: + +Post: Post: pagination: +``` + +You can confirm the new post with using the `blogd query blog posts` command that you learned about in the previous chapter. +The result looks similar to: + +```bash +Post: +- body: bar + creator: cosmos1j8d8pyjr5vynjvcq7xgzme0ny6ha30rpakxk3n + id: "0" + title: foo +- body: This is the first post + creator: cosmos1j8d8pyjr5vynjvcq7xgzme0ny6ha30rpakxk3n + id: "1" + title: Hello! +pagination: + next_key: null + total: "2" +``` + +Congratulations, you have just created a post using a separate app. + +When you publish your blockchain project to GitHub, you won't need to use the replace function in your `go.mod` file anymore and can directly use your GitHub repository to fetch the types and interact with your program. diff --git a/docs/guide/blog.md b/docs/guide/blog/index.md similarity index 99% rename from docs/guide/blog.md rename to docs/guide/blog/index.md index 80d2960e94..843ae45fe3 100644 --- a/docs/guide/blog.md +++ b/docs/guide/blog/index.md @@ -1,6 +1,8 @@ --- -title: "Module Basics: Blog" -order: 3 +order: 1 +parent: + title: "Module Basics: Blog" + order: 3 description: Learn module basics by writing and reading blog posts to your chain. --- diff --git a/docs/guide/hello.md b/docs/guide/hello.md index 5ef9ca9373..a42a75145a 100644 --- a/docs/guide/hello.md +++ b/docs/guide/hello.md @@ -11,7 +11,7 @@ This tutorial is a great place to start your journey into the Cosmos ecosystem. In the previous chapter you've learned how to install [Starport](https://github.com/tendermint/starport), the tool that offers everything you need to build, test, and launch your blockchain with a decentralized worldwide community. -This series of tutorials is based on a specific version of Starport, so to install Starport v0.17.3 use the following command: +This series of tutorials is based on a specific version of Starport, so to install Starport v0.18.0 use the following command: ```bash curl https://get.starport.network/starport@v0.18.0! | bash @@ -31,10 +31,10 @@ This command creates a Cosmos SDK blockchain called Hello in a `hello` directory This new blockchain imports standard Cosmos SDK modules, including: -- [`staking`](https://docs.cosmos.network/v0.42/modules/staking/) (for delegated proof of stake) -- [`bank`](https://docs.cosmos.network/v0.42/modules/bank/) (for fungible token transfers between accounts) -- [`gov`](https://docs.cosmos.network/v0.42/modules/gov/) (for on-chain governance) -- And [other modules](https://docs.cosmos.network/v0.42/modules/) +- [`staking`](https://docs.cosmos.network/master/modules/staking/) (for delegated proof of stake) +- [`bank`](https://docs.cosmos.network/master/modules/bank/) (for fungible token transfers between accounts) +- [`gov`](https://docs.cosmos.network/master/modules/gov/) (for on-chain governance) +- And [other modules](https://docs.cosmos.network/master/modules/) Now that you have run your first command, take a minute to see all of the command line options for the `scaffold` command. You can use --help on any command. Run the `starport scaffold chain --help` command to learn about the command you just used. @@ -98,12 +98,12 @@ To get your Cosmos SDK blockchain to say "Hello", you need to make these changes Protocol buffer files contain proto rpc calls that define Cosmos SDK queries and message handlers, and proto messages that define Cosmos SDK types. rpc calls are also responsible for exposing an HTTP API. -For each Cosmos SDK module, the [Keeper](https://docs.cosmos.network/v0.42/building-modules/keeper.html) is an abstraction for modifying the state of the blockchain. Keeper functions let you query or write to the state. After you add the first query to your chain, you must register the query. You only need to register a query once. +For each Cosmos SDK module, the [Keeper](https://docs.cosmos.network/master/building-modules/keeper.html) is an abstraction for modifying the state of the blockchain. Keeper functions let you query or write to the state. After you add the first query to your chain, you must register the query. You only need to register a query once. In terms of workflow, developers typically follow this sequence: -- Start with proto files to define Cosmos SDK [messages](https://docs.cosmos.network/v0.42/building-modules/msg-services.html) -- Define and register [queries](https://docs.cosmos.network/v0.42/building-modules/query-services.html) +- Start with proto files to define Cosmos SDK [messages](https://docs.cosmos.network/master/building-modules/msg-services.html) +- Define and register [queries](https://docs.cosmos.network/master/building-modules/query-services.html) - Define message handlers logic - Finally, implement the logic of these queries and message handlers in keeper functions. @@ -230,7 +230,7 @@ After the chain has been started, visit [http://localhost:1317/cosmonaut/hello/h } ``` -The `query` command has also scaffolded `x/hello/client/cli/query_posts.go` that implements a CLI equivalent of the posts query and mounted this command `x/hello/client/cli/query_posts.go` . Run the following command and get the same JSON response: +The `query` command has also scaffolded `x/hello/client/cli/query_posts.go` that implements a CLI equivalent of the posts query and mounted this command in `x/hello/client/cli/query.go` . Run the following command and get the same JSON response: ```go hellod q hello posts diff --git a/docs/guide/ibc.md b/docs/guide/ibc.md index 298cd1f69f..94c2f25e72 100644 --- a/docs/guide/ibc.md +++ b/docs/guide/ibc.md @@ -1,5 +1,5 @@ --- -order: 6 +order: 7 description: Build an understanding of how to create and send packets across blockchains and navigate between blockchains. title: "Inter-Blockchain Communication: Basics" --- @@ -84,19 +84,19 @@ These `starport type` commands create CRUD code for the following transactions: - Creating blog posts ```go - starport scaffold list post title content --module blog --no-message + starport scaffold list post title content --module blog ``` - Processing acknowledgments for sent posts ```go - starport scaffold list sentPost postID title chain --module blog --no-message + starport scaffold list sentPost postID title chain --module blog ``` - Managing post timeouts ```go - starport scaffold list timedoutPost title chain --module blog --no-message + starport scaffold list timedoutPost title chain --module blog ``` The scaffolded code includes proto files for defining data structures, messages, messages handlers, keepers for modifying the state, and CLI commands. @@ -170,7 +170,7 @@ To make sure the receiving chain has content on the creator of a blog post, add var packet types.IbcPostPacketData packet.Title = msg.Title packet.Content = msg.Content - packet.Creator = msg.Sender // < --- + packet.Creator = msg.Creator // < --- // Transmit the packet err := k.TransmitIbcPostPacket( ctx, @@ -184,7 +184,7 @@ To make sure the receiving chain has content on the creator of a blog post, add ### Receive the post -The methods for primary transaction logic are in the `planet/x/blog/keeper/ibcPost.go` file. Use these methods to manage IBC packets: +The methods for primary transaction logic are in the `planet/x/blog/keeper/ibc_post.go` file. Use these methods to manage IBC packets: - `TransmitIbcPostPacket` is called manually to send the packet over IBC. This method also defines the logic before the packet is sent over IBC to another blockchain app. - `OnRecvIbcPostPacket` hook is automatically called when a packet is received on the chain. This method defines the packet reception logic. @@ -208,7 +208,7 @@ Append the type instance as `PostID` on receiving the packet: - The `title` is the Title of the blog post - The `content` is the Content of the blog post -In the `ibcPost.go` file, make sure to import `"strconv"` below `"errors"`, and then modify `OnRecvIbcPostPacket` with the following code: +In the `ibc_post.go` file, make sure to import `"strconv"` below `"errors"`, and then modify `OnRecvIbcPostPacket` with the following code: ```go // x/blog/keeper/ibc_post.go @@ -348,8 +348,6 @@ host: grpc: ":9092" grpc-web: ":9093" api: ":1318" - frontend: ":8081" - dev-ui: ":12346" genesis: chain_id: "mars" init: diff --git a/docs/guide/index.md b/docs/guide/index.md index ba5e3f0e6c..3d8c3a6b2b 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -3,12 +3,12 @@ title: Introduction order: 0 parent: order: 0 - title: Developer Guide + title: Developer Tutorials --- # Introduction -By following this guide you will learn how to: +By following these developer tutorials you will learn how to: * Install Starport CLI on your local machine * Create a new blockchain and start a node locally for development diff --git a/docs/guide/install.md b/docs/guide/install.md index f44773fe88..05cd9dacc6 100644 --- a/docs/guide/install.md +++ b/docs/guide/install.md @@ -3,11 +3,37 @@ order: 1 description: Steps to install Starport on your local computer. --- +[Install Starport](#install-starport) + - [Prerequisites](#prerequisites) + - [Operating Systems](#operating-systems) + - [Go](#go) + - [Verify Your Starport Version](#verify-your-starport-version) + - [Installing Starport](#installing-starport) + - [Write permission](#write-permission) + - [Upgrading Your Starport Installation](#upgrading-your-starport-installation) + - [Installing Starport on macOS with Homebrew](#installing-starport-on-macos-with-homebrew) + - [Build from source](#build-from-source) + - [Summary](#summary) + + # Install Starport -You can run [Starport](https://github.com/tendermint/starport) in a web-based Gitpod IDE or you can install Starport on your local computer running GNU/Linux, macOS or Windows (with WSL). +You can run [Starport](https://github.com/tendermint/starport) in a web-based Gitpod IDE or you can install Starport on your local computer. + + +## Prerequisites + +Be sure you have met the prerequisites before you install and use Starport. -## Prerequisite +### Operating Systems + +Starport is supported for the following operating systems: + +- GNU/Linux +- macOS +- Windows Subsystem for Linux (WSL) + +### Go Starport is written in the Go programming language. To use Starport on a local system: @@ -34,7 +60,7 @@ This command invokes `curl` to download the install script and pipes the output To learn more or customize the installation process, see [Starport installer docs](https://github.com/allinbits/starport-installer) on GitHub. -### Write permission +### Write Permission Starport installation requires write permission to the `/usr/local/bin/` directory. If the installation fails because you do not have write permission to `/usr/local/bin/`, run the following command: @@ -63,10 +89,14 @@ After all existing Starport installations are removed, follow the [Installing St ## Installing Starport on macOS with Homebrew +Using brew to install Starport is supported only for macOS machines without the M1 chip. + ```bash brew install tendermint/tap/starport ``` +To install Starport on macOS machines with the M1 chip, use the `curl` command as described in [Installing Starport](#installing-starport). + ## Build from source ```bash @@ -76,6 +106,7 @@ cd starport && make install ## Summary +- Verify the prerequisites. - To setup a local development environment, install Starport locally on your computer. - Install Starport by fetching the binary using cURL, Homebrew, or by building from source. - The latest version is installed by default. You can install previous versions of the precompiled `starport` binary. diff --git a/docs/guide/interchange/01-design.md b/docs/guide/interchange/01-design.md index 9ca626e517..fb8c5af3fc 100644 --- a/docs/guide/interchange/01-design.md +++ b/docs/guide/interchange/01-design.md @@ -1,63 +1,92 @@ --- order: 1 +description: Learn about the interchain exchange module design. --- # App Design -In this chapter you learn how the interchain exchange module is designed. The module has order books, buy- and sell orders. First, an order book for a pair of token has to be created. After an order book exists, you can create buy and sell orders for this pair of token. +In this chapter, you learn how the interchain exchange module is designed. The module has order books, buy orders, and sell orders. -The module will make use of the Inter-Blockchain Communication Standard [IBC](https://github.com/cosmos/ics/blob/master/ibc/2_IBC_ARCHITECTURE.md). With use of the IBC, the module can create order books for tokens to have multiple blockchains interact and exchange their tokens. +- First, create an order book for a pair of token. +- After an order book exists, you can create buy and sell orders for this pair of token. -You create an order book pair with a token from one blockchain and another token from another blockchain. We call the module you create in this tutorial `ibcdex`. -Both blockchains need to have the `ibcdex` module installed and running. +The module uses the Inter-Blockchain Communication protocol [IBC](https://github.com/cosmos/ics/blob/master/ibc/2_IBC_ARCHITECTURE.md). By using IBC, the module can create order books so that multiple blockchains can interact and exchange their token. -When a user exchanges a token with the `ibcdex`, you receive a `voucher` of that token on the other blockchain. This is similar to how a `ibc-transfer` is constructed. Since a blockchain module does not have the rights to mint new token of a blockchain into existence, the token on the target chain is locked up and the buyer receives a `voucher` of that token. +You create an order book pair with a token from one blockchain and another token from another blockchain. In this tutorial, call the module you create the `dex` module. -This process can be reversed when the `voucher` get burned again to unlock the original token. This is explained throghout the tutorial in more detail. +> When a user exchanges a token with the `dex` module, a `voucher` of that token is received on the other blockchain. This voucher is similar to how an `ibc-transfer` is constructed. Since a blockchain module does not have the rights to mint new token of a blockchain into existence, the token on the target chain is locked up, and the buyer receives a `voucher` of that token. + +This process can be reversed when the `voucher` gets burned to unlock the original token. This exchange process is explained in more detail throughout the tutorial. ## Assumption of the Design -An order book can be created for the exchange of any tokens between any pair of chains. The requirement is to have the `ibcdex` module available. There can only be one order book for a pair of token at the same time. +An order book can be created for the exchange of any tokens between any pair of chains. + +- Both blockchains require the `dex` module to be installed and running. +- There can only be one order book for a pair of token at the same time. -A specific chain cannot mint new of its native token. +A specific chain cannot mint new coins of its native token. -This module is inspired by the [`ibc-transfer`](https://github.com/cosmos/cosmos-sdk/tree/v0.42.1/x/ibc/applications/transfer) module and will have some similarities, like the `voucher` creation. It will be more complex but it will display how to create: +This module is inspired by the [`ibc transfer`](https://github.com/cosmos/ibc-go/tree/main/modules/apps/transfer) module on the Cosmos SDK. The `dex` module you create in this tutorial has similarities, like the `voucher` creation. + +However, the new `dex` module you are creating is more complex because it supports creation of: - Several types of packets to send - Several types of acknowledgments to treat -- Some more complex logic on how to treat a packet on receipt, on timeout and more +- More complex logic on how to treat a packet on receipt, on timeout, and more + +## Interchain Exchange Overview + +Assume you have two blockchains: Venus and Mars. + +- The native token on Venus is `venuscoin`. +- The native token on Mars is `marscoin`. + +When a token is exchanged from Mars to Venus: + + - The Venus blockchain has an IBC `voucher` token with a denom that looks like `ibc/B5CB286...A7B21307F`. +- The long string of characters after `ibc/` is a denom trace hash of a token that was transferred using IBC. + +Using the blockchain's API you can get a denom trace from that hash. The denom trace consists of a `base_denom` and a `path`. In our example: + +- The `base_denom` is `marscoin`. +- The `path` contains pairs of ports and channels through which the token has been transferred. + +For a single-hop transfer, the `path` is identified by `transfer/channel-0`. -## Overview +Learn more about token paths in [ICS 20 Fungible Token Transfer](https://github.com/cosmos/ibc/tree/master/spec/app/ics-020-fungible-token-transfer). -Assume you have two blockchains: `Venus` and `Mars`. The native token on Venus is called `vcx`, the token on Mars is `mcx`. +**Note:** This token `ibc/Venus/marscoin` cannot be sold back using the same order book. If you want to "reverse" the exchange and receive the Mars token back, you must create and use a new order book for the `ibc/Venux/marscoin` to `marscoin` transfer. -When exchanging a token from Mars to Venus, on the Venus blockchain you have an IBC `voucher` token with a denom that looks like `ibc/B5CB286...A7B21307F`. The long string of characters after `ibc/` is a denom trace hash of a token transferred through IBC. Using the blockchain's API you can get a denom trace from that hash. The denom trace consists of a `base_denom` and a `path`. In our example, `base_denom` is `mcx` and the `path` contains pairs of ports and channels through which the token has been transferred. For a single-hop transfer `path` is identified by `transfer/channel-0`. Learn more about token paths in [ICS 20](https://github.com/cosmos/ibc/tree/master/spec/app/ics-020-fungible-token-transfer). +## The Design of the Order Books -This token `ibc/Venus/mcx` cannot be sold back using the same order book. If you want to "reverse" the exchange and receive back the Mars token, a new order book `ibc/Venux/mcx` to `mcx` needs to be created. +As a typical exchange, a new pair implies the creation of an order book with orders to sell `marscoin` or orders to buy `venuscoin`. Here, you have two chains and this data structure must be split between Mars and Venus. -## The Design of the Order books +- Users from chain Mars sell `marscoin`. +- Users from chain Venus buy `marscoin`. -As a typical exchange, a new pair implies the creation of an order book with orders to sell `MCX` or orders to buy `VCX`. Here, you have two chains and this data-structure must be split between `Mars` and `Venus`. +Therefore, we represent: -Users from chain `Mars` sell `MCX` and users from chain `Venus` buy `MCX`. Therefore, we represent all orders to sell `MCX` on chain `Mars` and all the orders to buy `MCX` on chain `Venus`. +- All orders to sell `marscoin` on chain Mars. +- All orders to buy `marscoin` on chain Venus. -In this example blockchain `Mars` holds the sell oders and blockchain `Venus` holds the buy orders. +In this example, blockchain Mars holds the sell orders and blockchain Venus holds the buy orders. -## Exchanging tokens back +## Exchanging Tokens Back -Like `ibc-transfer` each blockchain keeps a trace of the token voucher created on the other blockchain. +Like `ibc-transfer`, each blockchain keeps a trace of the token voucher that was created on the other blockchain. -If a blockchain `Mars` sells `MCX` to `Venus` and `ibc/Venus/mcx` is minted on `Venus` then, if `ibc/Venus/mcx` is sold back on `Mars` the token unlocked and received will be `MCX`. +If blockchain Mars sells `marscoin` to chain Venus and `ibc/Venus/marscoin` is minted on Venus then, if `ibc/Venus/marscoin` is sold back to Mars, the token is unlocked and the token that is received is `marscoin`. ## Features -The features supported by the module are: +The features supported by the interchain exchange module are: -- Creating an exchange order book for a token pair between two chains +- Create an exchange order book for a token pair between two chains - Send sell orders on source chain - Send buy orders on target chain - Cancel sell or buy orders diff --git a/docs/guide/interchange/02-init.md b/docs/guide/interchange/02-init.md index 4ba49c1651..0d1c658b33 100644 --- a/docs/guide/interchange/02-init.md +++ b/docs/guide/interchange/02-init.md @@ -1,85 +1,94 @@ --- order: 2 +description: Create the blockchain for the interchain exchange app. --- # App Init ## Initialize the Blockchain -In this chapter you create the basic blockchain module for the interchain exchange app. You scaffold the blockchain, the module, the transaction, the IBC packets and messages. In the later chapters you integrate more code into each of the transaction handlers. +In this chapter, you create the basic blockchain module for the interchain exchange app. You scaffold the blockchain, the module, the transaction, the IBC packets, and messages. In later chapters, you integrate more code into each of the transaction handlers. ## Create the Blockchain -Scaffold a new blockchain called `interchange` +Scaffold a new blockchain called `interchange`: ```bash starport scaffold chain github.com/cosmonaut/interchange --no-module ``` -A new directory named interchange is created. Make sure to now change into this directory, from there you will scaffold modules, types and maps. +A new directory named `interchange` is created. + +Change into this directory where you can scaffold modules, types, and maps: ```bash cd interchange ``` -This directory contains a working blockchain app. +The `interchange` directory contains a working blockchain app. + +A local GitHub repository has been created for you with the initial scaffold. + Next, create a new IBC module. -## Create the ibcdex Module +## Create the dex Module + +Scaffold a module inside your blockchain named `dex` with IBC capabilities. -Scaffold a module inside your blockchain named `ibcdex` with IBC capabilities. -The ibcdex module contains the logic to create and maintain order books and route them through IBC to the second blockchain. +The dex module contains the logic to create and maintain order books and route them through IBC to the second blockchain. ```bash -starport scaffold module ibcdex --ibc --ordering unordered --dep bank +starport scaffold module dex --ibc --ordering unordered --dep bank ``` ## Create CRUD logic for Buy and Sell Order Books -To scaffold two types with create, read, update and delete (CRUD) actions use the Starport `type` command. -The following commands create `sellOrderBook` and `buyOrderBook` types. +Scaffold two types with create, read, update, and delete (CRUD) actions. + +Run the following Starport `type` commands to create `sellOrderBook` and `buyOrderBook` types: ```bash -starport scaffold map sell-order-book amountDenom priceDenom --no-message --module ibcdex -starport scaffold map buy-order-book amountDenom priceDenom --no-message --module ibcdex +starport scaffold map sell-order-book amountDenom priceDenom --no-message --module dex +starport scaffold map buy-order-book amountDenom priceDenom --no-message --module dex ``` The values are: -- `amountDenom`: which token will be sold and in which quantity +- `amountDenom`: the token to be sold and in which quantity - `priceDenom`: the token selling price -The flag `--indexed` flag creates an "indexed type". Without this flag, a type is implemented like a list with new items appended. Indexed types act like key-value stores. +The `--indexed` flag creates an "indexed type". Without this flag, a type is implemented like a list with new items appended. Indexed types act like key-value stores. -The `--module ibcdex` flag specifies that the type should be scaffolded in the `ibcdex` module. +The `--module dex` flag specifies to scaffold the type in the `dex` module. ## Create the IBC Packets Create three packets for IBC: -- an order book pair `createPair` -- a sell order `sellOrder` -- a buy order `buyOrder` +- An order book pair `createPair` +- A sell order `sellOrder` +- A buy order `buyOrder` ```bash -starport scaffold packet create-pair sourceDenom targetDenom --module ibcdex -starport scaffold packet sell-order amountDenom amount:int priceDenom price:int --ack remainingAmount:int,gain:int --module ibcdex -starport scaffold packet buy-order amountDenom amount:int priceDenom price:int --ack remainingAmount:int,purchase:int --module ibcdex +starport scaffold packet create-pair sourceDenom targetDenom --module dex +starport scaffold packet sell-order amountDenom amount:int priceDenom price:int --ack remainingAmount:int,gain:int --module dex +starport scaffold packet buy-order amountDenom amount:int priceDenom price:int --ack remainingAmount:int,purchase:int --module dex ``` -The optional `--ack` flag defines field names and types of the acknowledgment returned after the packet has been received by the target chain. Value of `--ack` is a comma-separated (no spaces) list of names with optional types appended after a colon. +The optional `--ack` flag defines field names and types of the acknowledgment returned after the packet has been received by the target chain. The value of the `--ack` flag is a comma-separated list of names (no spaces). Append optional types after a colon (`:`). ## Cancel messages Cancelling orders is done locally in the network, there is no packet to send. -Use the `message` command to create a message to cancel a sell or buy order. + +Use the `message` command to create a message to cancel a sell or buy order: ```go -starport scaffold message cancel-sell-order port channel amountDenom priceDenom orderID:int --desc "Cancel a sell order" --module ibcdex -starport scaffold message cancel-buy-order port channel amountDenom priceDenom orderID:int --desc "Cancel a buy order" --module ibcdex +starport scaffold message cancel-sell-order port channel amountDenom priceDenom orderID:int --desc "Cancel a sell order" --module dex +starport scaffold message cancel-buy-order port channel amountDenom priceDenom orderID:int --desc "Cancel a buy order" --module dex ``` -The optional `--desc` flag lets you define a description of the CLI command that is used to broadcast a transaction with the message. +Use the optional `--desc` flag to define a description of the CLI command that is used to broadcast a transaction with the message. ## Trace the Denom @@ -88,19 +97,21 @@ The token denoms must have the same behavior as described in the `ibc-transfer` - An external token received from a chain has a unique `denom`, reffered to as `voucher`. - When a token is sent to a blockchain and then sent back and received, the chain can resolve the voucher and convert it back to the original token denomination. -`Voucher` tokens are represented as hashes, therefore you must store which original denomination is related to a voucher, you can do this with an indexed type. +`Voucher` tokens are represented as hashes, therefore you must store which original denomination is related to a voucher. You can do this with an indexed type. -For a `voucher` you store: the source port ID, source channel ID, and the original denom. +For a `voucher` you store, define the source port ID, source channel ID, and the original denom: ```go -starport scaffold map denom-trace port channel origin --no-message --module ibcdex +starport scaffold map denom-trace port channel origin --no-message --module dex ``` -## Create the Configuration for two Blockchains +## Create the Configuration for Two Blockchains Add two config files `mars.yml` and `venus.yml` to test two blockchain networks with specific token for each. + Add the config files in the `interchange` folder. -The native denoms for Mars are `mcx`, also known as `marscoin`, and for Venus `vcx`, also known as `venuscoin`. + +The native denoms for Mars are `marscoin`, and for Venus `venuscoin`. Create the `mars.yml` file with your content: @@ -108,9 +119,9 @@ Create the `mars.yml` file with your content: # mars.yml accounts: - name: alice - coins: ["1000token", "100000000stake", "1000mcx"] + coins: ["1000token", "100000000stake", "1000marscoin"] - name: bob - coins: ["500token", "1000mcx", "100000000stake"] + coins: ["500token", "1000marscoin", "100000000stake"] validator: name: alice staked: "100000000stake" @@ -129,9 +140,9 @@ Create the `venus.yml` file with your content: # venus.yml accounts: - name: alice - coins: ["1000token", "1000000000stake", "1000vcx"] + coins: ["1000token", "1000000000stake", "1000venuscoin"] - name: bob - coins: ["500token", "1000vcx", "100000000stake"] + coins: ["500token", "1000venuscoin", "100000000stake"] validator: name: alice staked: "100000000stake" @@ -145,12 +156,21 @@ host: prof: ":6061" grpc: ":9091" api: ":1318" - frontend: ":8081" - dev-ui: ":12346" genesis: chain_id: "venus" init: home: "$HOME/.venus" ``` +In the `venus.yml` file, you can see the specific `host` parameter that you can use to change the ports for various running services (rpc, p2p, prof, grpc, api, frontend, and dev-ui). This `host` parameter can be used later so you can run two blockchains in parallel and prevent conflicts when the chains are using the same ports. + +You can also use the `host` parameter to use specific ports for any of the services. + +After scaffolding, now is a good time to make a commit to the local GitHub repository that was created for you. + +```bash +git add . +git commit -m "Scaffold module, maps, packages and messages for the dex" +``` + Implement the code for the order book in the next chapter. diff --git a/docs/guide/interchange/03-walkthrough.md b/docs/guide/interchange/03-walkthrough.md index a8e8924ab4..5b81227b74 100644 --- a/docs/guide/interchange/03-walkthrough.md +++ b/docs/guide/interchange/03-walkthrough.md @@ -1,234 +1,269 @@ --- order: 3 +description: Walkthrough of commands to use the interchain exchange module. --- -# Using the exchange +# Use the Interchain Exchange + +In this chapter, you learn details about the order book and commands to: + +- Create an exchange order book for a token pair between two chains +- Send sell orders on source chain +- Send buy orders on target chain +- Cancel sell or buy orders + +The next chapter contains the code for the implementation. ## Order Book -In this chapter you will build learn how to build the order book. -The next chapter contains the code for the implementation. First, learn what you are going to implement. -To use the exchange start from creating a order book for a pair of tokens: +To use the exchange, start by creating an order book for a pair of tokens: ```bash -interchanged tx ibcdex send-create-pair [src-port] [src-channel] [sourceDenom] [targetDenom] - +interchanged tx dex send-create-pair [src-port] [src-channel] [sourceDenom] [targetDenom] # Create pair broadcasted to the source blockchain -interchanged tx ibcdex send-create-pair ibcdex channel-0 mcx vcx + +interchanged tx dex send-create-pair dex channel-0 marscoin venuscoin ``` -A pair of token is defined by two denominations: source denom (in this example, `mcx`) and target denom (`vcx`). Creating an orderbook affects state on the source blockchain (to which the transaction was broadcasted) and the target blockchain. On the source blockchain `send-createPair` creates an empty sell order book and on the target blockchain a buy order book is created. +Define a pair of token with two denominations: + +- Source denom (in this example, `marscoin`) +- Target denom (`venuscoin`) + +Creating an order book affects state on the source blockchain to which the transaction was broadcast and the target blockchain. + +On the source blockchain, the `send-create-pair` command creates an empty sell order book: ```yml # Created a sell order book on the source blockchain SellOrderBook: -- amountDenom: mcx +- amountDenom: marscoin creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 0 orders: [] - priceDenom: vcx + priceDenom: venuscoin ``` +On the target blockchain, the same `send-createPair` command creates a buy order book: + ```yml # Created a buy order book on the target blockchain BuyOrderBook: -- amountDenom: mcx +- amountDenom: marscoin creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 1 orders: [] - priceDenom: vcx + priceDenom: venuscoin ``` -To make it possible `createPair` first sends an IBC packet to the target chain. Upon receiving a packet the target chain creates a buy order book and sends back an acknowledgement to the source chain. Upon receiving an acknowledgement, the source chain creates a sell order book. Sending an IBC packet requires a user specify a port and a channel through which a packet will be transferred. +To make an exchange possible, the `createPair` transaction sends an IBC packet to the target chain. + +- When the target chain receives a packet, the target chain creates a buy order book and sends an acknowledgement back to the source chain. +- When the source chain receives an acknowledgement, the source chain creates a sell order book. + +Sending an IBC packet requires a user to specify a port and a channel through which a packet is transferred. ## Sell Order -Once an order book is created, the next step is to create a sell order: +After an order book is created, the next step is to create a sell order: ```bash -interchanged tx ibcdex send-sell-order [src-port] [src-channel] [amountDenom] [amount] [priceDenom] [price] +interchanged tx dex send-sell-order [src-port] [src-channel] [amountDenom] [amount] [priceDenom] [price] # Sell order broadcasted to the source blockchain -interchanged tx ibcdex send-sell-order ibcdex channel-0 mcx 10 vcx 15 +interchanged tx dex send-sell-order dex channel-0 marscoin 10 venuscoin 15 ``` -The `send-sellOrder` command broadcasts a message that locks tokens on the source blockchain and creates a new sell order on the source blockchain. +The `send-sellOrder` command broadcasts a message that locks token on the source blockchain and creates a sell order on the source blockchain: ```yml # Source blockchain balances: - amount: "990" # decreased from 1000 - denom: mcx + denom: marscoin SellOrderBook: -- amountDenom: mcx +- amountDenom: marscoin creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 2 orders: # a new sell order is created - amount: 10 creator: cosmos1v3p3j7c64c4ls32pcjct333e8vqe45gwwa289q id: 0 price: 15 - priceDenom: vcx + priceDenom: venuscoin ``` ## Buy Order -A buy order has the same set of arguments: amount of tokens to be purchased and a price. +A buy order has the same arguments, the amount of token to be purchased and a price: ```bash -`interchanged tx ibcdex send-buy-order [src-port] [src-channel] [amountDenom] [amount] [priceDenom] [price]` +`interchanged tx dex send-buy-order [src-port] [src-channel] [amountDenom] [amount] [priceDenom] [price]` # Buy order broadcasted to the target blockchain -interchanged tx ibcdex send-buy-order ibcdex channel-0 mcx 10 vcx 5 +interchanged tx dex send-buy-order dex channel-0 marscoin 10 venuscoin 5 ``` -The `send-buyOrder` command locks tokens on the target blockchain and creates a buy order book on the target blockchain. +The `send-buy-order` command locks token on the target blockchain: ```yml # Target blockchain balances: - amount: "950" # decreased from 1000 - denom: vcx + denom: venuscoin BuyOrderBook: -- amountDenom: mcx +- amountDenom: marscoin creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 3 orders: # a new buy order is created - amount: 10 creator: cosmos1qlrz3peenc6s3xjv9k97e8ef72nk3qn3a0xax2 id: 1 price: 5 - priceDenom: vcx + priceDenom: venuscoin ``` -## Performing an Exchange with a Sell Order +## Perform an Exchange with a Sell Order + +You now have two orders open for marscoin: -We now have two orders open for MCX: a sell order on the source chain (for 10mcx at 15vcx) and a buy order on the target chain (for 5mcx at 5vcx). Let's perform an exchange by sending a sell order to the source chain. +- A sell order on the source chain (for 10marscoin at 15venuscoin) +- A buy order on the target chain (for 5marscoin at 5venuscoin) + +Now, perform an exchange by sending a sell order to the source chain: ```bash # Sell order broadcasted to the source chain -interchanged tx ibcdex send-sell-order ibcdex channel-0 mcx 5 vcx 3 +interchanged tx dex send-sell-order dex channel-0 marscoin 5 venuscoin 3 ``` -The sell order (for 5mcx at 3vcx) was filled on the target chain by the buy order. The amount of the buy order on the target chain has decreased by 5mcx. +The sell order (for 5marscoin at 3venuscoin) is filled on the target chain by the buy order. + +The amount of the buy order on the target chain is decreased by 5marscoin: ```yml # Target blockchain BuyOrderBook: -- amountDenom: mcx +- amountDenom: marscoin creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 5 orders: - amount: 5 # decreased from 10 creator: cosmos1qlrz3peenc6s3xjv9k97e8ef72nk3qn3a0xax2 id: 3 price: 5 - priceDenom: vcx + priceDenom: venuscoin ``` -The sender of the filled sell order exchanged 5mcx for 25 vcx vouchers. 25 vouchers is a product of the amount of the sell order (5mcx) and price of the buy order (5vcx). +The sender of the filled sell order exchanged 5marscoin for 25 venuscoin vouchers. + +25 vouchers is a product of the amount of the sell order (5marscoin) and price of the buy order (5venuscoin): ```yml # Source blockchain balances: - amount: "25" # increased from 0 - denom: ibc/50D70B7748FB8AA69F09114EC9E5615C39E07381FE80E628A1AF63A6F5C79833 # vcx voucher + denom: ibc/50D70B7748FB8AA69F09114EC9E5615C39E07381FE80E628A1AF63A6F5C79833 # venuscoin voucher - amount: "985" # decreased from 990 - denom: mcx + denom: marscoin ``` -The counterparty (sender of the buy mcx order) received 5 mcx vouchers. vcx balance hasn't changed, because the correct amount of vcx (50) were locked at the creation of the buy order during the previous step. +The counterparty (the sender of the buy marscoin order) receives 5 marscoin vouchers: ```yml # Target blockchain balances: - amount: "5" # increased from 0 - denom: ibc/99678A10AF684E33E88959727F2455AE42CCC64CD76ECFA9691E1B5A32342D33 # mcx voucher + denom: ibc/99678A10AF684E33E88959727F2455AE42CCC64CD76ECFA9691E1B5A32342D33 # marscoin voucher ``` -## Performing an Exchange with a Buy Order +The venuscoin balance hasn't changed because the correct amount of venuscoin (50) was locked at the creation of the buy order during the previous step. -An order is sent to buy 5mcx for 15vcx. +## Perform an Exchange with a Buy Order + +Now, send an order to buy 5marscoin for 15venuscoin: ```bash # Buy order broadcasted to the target chain -interchanged tx ibcdex send-buy-order ibcdex channel-0 mcx 5 vcx 15 +interchanged tx dex send-buy-order dex channel-0 marscoin 5 venuscoin 15 ``` -A buy order is immediately filled on the source chain and sell order creator recived 75 vcx vouchers. The sell order amount is decreased by the amount of the filled buy order (by 5mcx). +A buy order is immediately filled on the source chain and the sell order creator receives 75 venuscoin vouchers. + +The sell order amount is decreased by the amount of the filled buy order (by 5marscoin): ```yml # Source blockchain balances: - amount: "100" # increased from 25 - denom: ibc/50D70B7748FB8AA69F09114EC9E5615C39E07381FE80E628A1AF63A6F5C79833 # vcx voucher + denom: ibc/50D70B7748FB8AA69F09114EC9E5615C39E07381FE80E628A1AF63A6F5C79833 # venuscoin voucher SellOrderBook: -- amountDenom: mcx +- amountDenom: marscoin creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 4 orders: - amount: 5 # decreased from 10 creator: cosmos1v3p3j7c64c4ls32pcjct333e8vqe45gwwa289q id: 2 price: 15 - priceDenom: vcx + priceDenom: venuscoin ``` -Creator of the buy order received 5 mcx vouchers for 75 vcx (5mcx * 15vcx). +The creator of the buy order receives 5 marscoin vouchers for 75 venuscoin (5marscoin * 15venuscoin): ```yml # Target blockchain balances: - amount: "10" # increased from 5 - denom: ibc/99678A10AF684E33E88959727F2455AE42CCC64CD76ECFA9691E1B5A32342D33 # mcx vouchers + denom: ibc/99678A10AF684E33E88959727F2455AE42CCC64CD76ECFA9691E1B5A32342D33 # marscoin vouchers - amount: "875" # decreased from 950 - denom: vcx + denom: venuscoin ``` ## Complete Exchange with a Partially Filled Sell Order -An order is sent to sell 10mcx for 3vcx. +Send an order to sell 10marscoin for 3venuscoin: ```bash # Source blockchain -interchanged tx ibcdex send-sell-order ibcdex channel-0 mcx 10 vcx 3 +interchanged tx dex send-sell-order dex channel-0 marscoin 10 venuscoin 3 ``` -The sell amount is 10mcx, but the opened buy order amount is only 5mcx. The buy order gets filled completely and removed from the order book. The author of the previously created buy order recived 10 mcx vouchers from the exchange. +The sell amount is 10marscoin, but the opened buy order amount is only 5marscoin. The buy order gets filled completely and removed from the order book. The author of the previously created buy order receives 10 marscoin vouchers from the exchange: ```yml # Target blockchain balances: - amount: "15" # increased from 5 - denom: ibc/99678A10AF684E33E88959727F2455AE42CCC64CD76ECFA9691E1B5A32342D33 # mcx voucher + denom: ibc/99678A10AF684E33E88959727F2455AE42CCC64CD76ECFA9691E1B5A32342D33 # marscoin voucher BuyOrderBook: -- amountDenom: mcx +- amountDenom: marscoin creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 5 - orders: [] # buy order with amount 5mcx has been closed - priceDenom: vcx + orders: [] # buy order with amount 5marscoin has been closed + priceDenom: venuscoin ``` -The author of the sell order successfuly exchanged 5 mcx and recived 25 vcx vouchers. The other 5mcx created a sell order. +The author of the sell order successfuly exchanged 5 marscoin and received 25 venuscoin vouchers. The other 5marscoin created a sell order: ```yml # Source blockchain balances: - amount: "125" # increased from 100 - denom: ibc/50D70B7748FB8AA69F09114EC9E5615C39E07381FE80E628A1AF63A6F5C79833 # vcx vouchers + denom: ibc/50D70B7748FB8AA69F09114EC9E5615C39E07381FE80E628A1AF63A6F5C79833 # venuscoin vouchers - amount: "975" # decreased from 985 - denom: mcx -- amountDenom: mcx + denom: marscoin +- amountDenom: marscoin SellOrderBook: creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 6 orders: - amount: 5 # hasn't changed @@ -243,88 +278,103 @@ SellOrderBook: ## Complete Exchange with a Partially Filled Buy Order -An order is created to buy 10 mcx for 5 vcx. +Create an order to buy 10 marscoin for 5 venuscoin: ```bash # Target blockchain -interchanged tx ibcdex send-buy-order ibcdex channel-0 mcx 10 vcx 5 +interchanged tx dex send-buy-order dex channel-0 marscoin 10 venuscoin 5 ``` -The buy order is partially filled for 5mcx. An existing sell order for 5 mcx (with a price of 3 vcx) on the source chain has been completely filled and removed from the order book. The author of the closed sell order recived 15 vcx vouchers (product of 5mcx and 3vcx). +The buy order is partially filled for 5marscoin. An existing sell order for 5 marscoin (with a price of 3 venuscoin) on the source chain is completely filled and is removed from the order book. The author of the closed sell order receives 15 venuscoin vouchers (product of 5marscoin and 3venuscoin): ```yml # Source blockchain balances: - amount: "140" # increased from 125 - denom: ibc/50D70B7748FB8AA69F09114EC9E5615C39E07381FE80E628A1AF63A6F5C79833 # vcx vouchers + denom: ibc/50D70B7748FB8AA69F09114EC9E5615C39E07381FE80E628A1AF63A6F5C79833 # venuscoin vouchers SellOrderBook: -- amountDenom: mcx +- amountDenom: marscoin creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 6 orders: - amount: 5 # order hasn't changed creator: cosmos1v3p3j7c64c4ls32pcjct333e8vqe45gwwa289q id: 2 price: 15 - # a sell order for 5 mcx has been closed - priceDenom: vcx + # a sell order for 5 marscoin has been closed + priceDenom: venuscoin ``` -Author of buy order recieves 5 mcx vouchers and 50 vcx of their tokens get locked. The 5mcx amount not filled by the sell order creates a buy order on the target chain. +The author of the buy order receives 5 marscoin vouchers which locks 50 venuscoin of their token. The 5marscoin amount that is not filled by the sell order creates a buy order on the target chain: ```yml # Target blockchain balances: - amount: "20" # increased from 15 - denom: ibc/99678A10AF684E33E88959727F2455AE42CCC64CD76ECFA9691E1B5A32342D33 # mcx vouchers + denom: ibc/99678A10AF684E33E88959727F2455AE42CCC64CD76ECFA9691E1B5A32342D33 # marscoin vouchers - amount: "825" # decreased from 875 - denom: vcx + denom: venuscoin BuyOrderBook: -- amountDenom: mcx +- amountDenom: marscoin creator: "" - index: ibcdex-channel-0-mcx-vcx + index: dex-channel-0-marscoin-venuscoin orderIDTrack: 7 orders: - amount: 5 # new buy order is created creator: cosmos1qlrz3peenc6s3xjv9k97e8ef72nk3qn3a0xax2 id: 5 price: 5 - priceDenom: vcx + priceDenom: venuscoin ``` -## Cancelling Orders +## Cancel an Order + +After these exchanges, you still have two orders open: -After the exchanges we have two orders open: sell order on the source chain (5mcx for 15vcx) and a buy order on the target chain (5mcx for 5vcx). +- A sell order on the source chain (5marscoin for 15venuscoin) +- A buy order on the target chain (5marscoin for 5venuscoin) -Cancelling a sell order: +To cancel a sell order: ```bash # Source blockchain -interchanged tx ibcdex cancel-sell-order ibcdex channel-0 mcx vcx 2 +interchanged tx dex cancel-sell-order dex channel-0 marscoin venuscoin 2 ``` +The balance of marscoin is increased: + ```yml # Source blockchain balances: - amount: "980" # increased from 975 - denom: mcx + denom: marscoin ``` -The sell order book on the source blokchain is now empty. +The sell order book on the source blockchain is now empty. -Cancelling a buy order: +To cancel a buy order: ```bash # Target blockchain -interchanged tx ibcdex cancel-buy-order ibcdex channel-0 mcx vcx 5 +interchanged tx dex cancel-buy-order dex channel-0 marscoin venuscoin 5 ``` +The amount of venuscoin is increased: + ```yml # Target blockchain balances: - amount: "850" # increased from 825 - denom: vcx + denom: venuscoin ``` The buy order book on the target blokchain is now empty. + +This walkthrough of the interchain exchange showed you how to: + +- Create an exchange order book for a token pair between two chains +- Send sell orders on source chain +- Send buy orders on target chain +- Cancel sell or buy orders + diff --git a/docs/guide/interchange/04-creating-order-books.md b/docs/guide/interchange/04-creating-order-books.md index c93d063d57..6f2d0f3ee3 100644 --- a/docs/guide/interchange/04-creating-order-books.md +++ b/docs/guide/interchange/04-creating-order-books.md @@ -1,14 +1,25 @@ --- order: 4 +description: Implement logic to create order books. --- + # Implement the Order Books -In this chapter you will implement the logic to create order books. +In this chapter, you implement the logic to create order books. + +In the Cosmos SDK, the state is stored in a key-value store. Each order book is stored under a unique key that is composed of four values: + +- Port ID +- Channel ID +- Source denom +- Target denom + +For example, an order book for marscoin and venuscoin could be stored under `dex-channel-4-marscoin-venuscoin`. -In Cosmos SDK the state is stored in a key-value store. Each order book will be stored under a unique key composed of four values: port ID, channel ID, source denom and target denom. For example, an order book for `mcx` and `vcx` could be stored under `ibcdex-channel-4-mcx-vcx`. Define a function that returns an order book store key. +First, define a function that returns an order book store key: ```go -// x/ibcdex/types/keys.go +// x/dex/types/keys.go import "fmt" //... @@ -17,26 +28,33 @@ func OrderBookIndex( portID string, channelID string, sourceDenom string, target } ``` -`send-create-pair` is used to create order books. This command creates and broadcasts a transaction with a message of type `SendCreatePair`. The message gets routed to the `ibcdex` module, processed by the message handler in `x/ibcdex/handler.go` and finally a `SendCreatePair` keeper method is called. +The `send-create-pair` command is used to create order books. This command: -You need `send-create-pair` to do the following: +- Creates and broadcasts a transaction with a message of type `SendCreatePair`. +- The message gets routed to the `dex` module. +- The message is processed by the message handler in `x/dex/handler.go`. +- Finally, a `SendCreatePair` keeper method is called. -* When processing `SendCreatePair` message on the source chain - * Check that an order book with the given pair of denoms does not yet exist - * Transmit an IBC packet with information about port, channel, source and target denoms -* Upon receiving the packet on the target chain - * Check that an order book with the given pair of denoms does not yet exist on the target chain - * Create a new order book for buy orders - * Transmit an IBC acknowledgement back to the source chain -* Upon receiving the acknowledgement on the source chain - * Create a new order book for sell orders +You need the `send-create-pair` command to do the following: + +* When processing `SendCreatePair` message on the source chain: + * Check that an order book with the given pair of denoms does not yet exist. + * Transmit an IBC packet with information about port, channel, source denoms, and target denoms. +* After the packet is received on the target chain: + * Check that an order book with the given pair of denoms does not yet exist on the target chain. + * Create a new order book for buy orders. + * Transmit an IBC acknowledgement back to the source chain. +* After the acknowledgement is received on the source chain: + * Create a new order book for sell orders. ## Message Handling in SendCreatePair -`SendCreatePair` function was created during the IBC packet scaffolding. Currently, it creates an IBC packet, populates it with source and target denoms and transmits this packet over IBC. Add the logic to check for an existing order book for a particular pair of denoms. +The `SendCreatePair` function was created during the IBC packet scaffolding. The function creates an IBC packet, populates it with source and target denoms, and transmits this packet over IBC. + +Now, add the logic to check for an existing order book for a particular pair of denoms: ```go -// x/ibcdex/keeper/msg_server_create_pair.go +// x/dex/keeper/msg_server_create_pair.go import ( "errors" //... @@ -77,28 +95,33 @@ func (k msgServer) SendCreatePair(goCtx context.Context, msg *types.MsgSendCreat ## Lifecycle of an IBC Packet -During a successful transmission, an IBC packet goes through 4 stages: +During a successful transmission, an IBC packet goes through these stages: + +1. Message processing before packet transmission on the source chain +2. Reception of a packet on the target chain +3. Acknowledgment of a packet on the source chain +4. Timeout of a packet on the source chain + +In the following section, implement the packet reception logic in the `OnRecvCreatePairPacket` function and the packet acknowledgement logic in the `OnAcknowledgementCreatePairPacket` function. -1. Message processing before packet transmission (on the source cahin) -2. Reception of a packet (on the target chain) -3. Acknowledgment of a packet (on the source chain) -4. Timeout of a packet (on the source chain) +Leave the Timeout function empty. -In the following section you'll be implementing packet reception logic in the `OnRecvCreatePairPacket` function and packet acknowledgement logic in the `OnAcknowledgementCreatePairPacket` function. Timeout function will be left empty. +## Receive an IBC packet -## Receiving an IBC packet +The protocol buffer definition defines the data that an order book contains. -The protocol buffer definition defines the data that an order book has. Add the `OrderBook` and `Order` messages to the `order.proto` file. -First you will need to add the proto buffer files. This builds the according go code files that you can then modify for the purpose of your app. +Add the `OrderBook` and `Order` messages to the `order.proto` file. -Create a new `order.proto` file in the `proto/ibcdex` directory and add the content. +First, add the proto buffer files to build the Go code files. You can modify these files for the purpose of your app. + +Create a new `order.proto` file in the `proto/dex` directory and add the content: ```proto -// proto/ibcdex/order.proto +// proto/dex/order.proto syntax = "proto3"; -package cosmonaut.interchange.ibcdex; +package cosmonaut.interchange.dex; -option go_package = "github.com/cosmonaut/interchange/x/ibcdex/types"; +option go_package = "github.com/cosmonaut/interchange/x/dex/types"; message OrderBook { int32 idCount = 1; @@ -114,31 +137,36 @@ message Order { ``` Modify the `buy_order_book.proto` file to have the fields for creating a buy order on the order book. +Don't forget to add the import as well. + +**Tip:** Don't forget to add the import as well. ```proto -// proto/ibcdex/buy_order_book.proto -import "ibcdex/order.proto"; +// proto/dex/buy_order_book.proto +import "dex/order.proto"; message BuyOrderBook { // ... - OrderBook book = 5; + OrderBook book = 4; } ``` -Modify the `sell_order_book.proto` file to add the order book into the buy order book. The proto definition for the `SellOrderBook` should look like follows: +Modify the `sell_order_book.proto` file to add the order book into the buy order book. + +The proto definition for the `SellOrderBook` looks like: ```proto -// proto/ibcdex/sell_order_book.proto +// proto/dex/sell_order_book.proto // ... -import "ibcdex/order.proto"; +import "dex/order.proto"; message SellOrderBook { // ... - OrderBook book = 6; + OrderBook book = 4; } ``` -Now build the proto files to go with the command: +Now, use Starport CLI to build the proto files for the `send-create-pair` command: ```bash starport generate proto-go @@ -146,11 +174,12 @@ starport generate proto-go Start enhancing the functions for the IBC packets. -Create a new file `x/ibcdex/types/order_book.go`. -Add the new order book function to the corresponing Go file. +Create a new file `x/dex/types/order_book.go`. + +Add the new order book function to the corresponding Go file: ```go -// x/ibcdex/types/order_book.go +// x/dex/types/order_book.go package types func NewOrderBook() OrderBook { @@ -160,10 +189,10 @@ func NewOrderBook() OrderBook { } ``` -Define `NewBuyOrderBook` in a new file `x/ibcdex/types/buy_order_book.go` creates a new buy order book. +To create a new buy order book type, define `NewBuyOrderBook` in a new file `x/dex/types/buy_order_book.go` : ```go -// x/ibcdex/types/buy_order_book.go +// x/dex/types/buy_order_book.go package types func NewBuyOrderBook(AmountDenom string, PriceDenom string) BuyOrderBook { @@ -176,10 +205,11 @@ func NewBuyOrderBook(AmountDenom string, PriceDenom string) BuyOrderBook { } ``` -On the target chain when an IBC packet is recieved, the module should check whether a book already exists, if not, create a new buy order book for specified denoms. +When an IBC packet is received on the target chain, the module must check whether a book already exists. If not, then create a buy order book for the specified denoms. + ```go -// x/ibcdex/keeper/create_pair.go +// x/dex/keeper/create_pair.go func (k Keeper) OnRecvCreatePairPacket(ctx sdk.Context, packet channeltypes.Packet, data types.CreatePairPacketData) (packetAck types.CreatePairPacketAck, err error) { // ... // Get an order book index @@ -199,15 +229,16 @@ func (k Keeper) OnRecvCreatePairPacket(ctx sdk.Context, packet channeltypes.Pack } ``` -## Receiving an IBC Acknowledgement +## Receive an IBC Acknowledgement + -On the source chain when an IBC acknowledgement is recieved, the module should check whether a book already exists, if not, create a new sell order book for specified denoms. +When an IBC acknowledgement is recieved on the source chain, the module must check whether a book already exists. If not, create a sell order book for the specified denoms. -Create a new file `x/ibcdex/types/sell_order_book.go`. +Create a new file `x/dex/types/sell_order_book.go`. Insert the `NewSellOrderBook` function which creates a new sell order book. ```go -// x/ibcdex/types/sell_order_book.go +// x/dex/types/sell_order_book.go package types func NewSellOrderBook(AmountDenom string, PriceDenom string) SellOrderBook { @@ -220,10 +251,10 @@ func NewSellOrderBook(AmountDenom string, PriceDenom string) SellOrderBook { } ``` -Modify the Acknowledgement function in the `create_pair.go` file. +Modify the Acknowledgement function in the `keeper/create_pair.go` file: ```go -// x/ibcdex/keeper/create_pair.go +// x/dex/keeper/create_pair.go func (k Keeper) OnAcknowledgementCreatePairPacket(ctx sdk.Context, packet channeltypes.Packet, data types.CreatePairPacketData, ack channeltypes.Acknowledgement) error { switch dispatchedAck := ack.Response.(type) { case *channeltypes.Acknowledgement_Error: @@ -248,19 +279,28 @@ func (k Keeper) OnAcknowledgementCreatePairPacket(ctx sdk.Context, packet channe } ``` -In this chapter we implemented the logic behind `send-create-pair` command that upon recieving of an IBC packet on the target chain creates a buy order book and upon recieving of an IBC acknowledgement on the source chain creates a sell order book. +In this section, you implemented the logic behind the new `send-create-pair` command: + +- When an IBC packet is received on the target chain, `send-create-pair` command creates a buy order book. +- When an IBC acknowledgement is received on the source chain, the `send-create-pair` command creates a sell order book. -### Implement the `appendOrder` Function to Add Orders to the Order Book +### Implement the appendOrder Function to Add Orders to the Order Book ```go -// x/ibcdex/types/order_book.go +// x/dex/types/order_book.go package types import ( - "errors" + "errors" "sort" ) +func NewOrderBook() OrderBook { + return OrderBook{ + IdCount: 0, + } +} + const ( MaxAmount = int32(100000) MaxPrice = int32(100000) @@ -282,10 +322,10 @@ var ( ) ``` -`AppendOrder` initializes and appends a new order to an order book from the order information. +The `AppendOrder` function initializes and appends a new order to an order book from the order information: ```go -// x/ibcdex/types/order_book.go +// x/dex/types/order_book.go func (book *OrderBook) appendOrder(creator string, amount int32, price int32, ordering Ordering) (int32, error) { if err := checkAmountAndPrice(amount, price); err != nil { return 0, err @@ -304,12 +344,12 @@ func (book *OrderBook) appendOrder(creator string, amount int32, price int32, or } ``` -#### Implement the checkAmountAndPrice For an Order +#### Implement the checkAmountAndPrice Function For an Order -`checkAmountAndPrice` checks correct amount or price. +The `checkAmountAndPrice` function checks for the correct amount or price: ```go -// x/ibcdex/types/order_book.go +// x/dex/types/order_book.go func checkAmountAndPrice(amount int32, price int32) error { if amount == int32(0) { return ErrZeroAmount @@ -329,10 +369,10 @@ func checkAmountAndPrice(amount int32, price int32) error { #### Implement the GetNextOrderID Function -`GetNextOrderID` gets the ID of the next order to append +The `GetNextOrderID` function gets the ID of the next order to append: ```go -// x/ibcdex/types/order_book.go +// x/dex/types/order_book.go func (book OrderBook) GetNextOrderID() int32 { return book.IdCount } @@ -340,10 +380,10 @@ func (book OrderBook) GetNextOrderID() int32 { #### Implement the IncrementNextOrderID Function -`IncrementNextOrderID` updates the ID count for orders +The `IncrementNextOrderID` function updates the ID count for orders: ```go -// x/ibcdex/types/order_book.go +// x/dex/types/order_book.go func (book *OrderBook) IncrementNextOrderID() { // Even numbers to have different ID than buy orders book.IdCount++ @@ -352,10 +392,10 @@ func (book *OrderBook) IncrementNextOrderID() { #### Implement the insertOrder Function -`insertOrder` inserts the order in the book with the provided order +The `insertOrder` function inserts the order in the book with the provided order: ```go -// x/ibcdex/types/order_book.go +// x/dex/types/order_book.go func (book *OrderBook) insertOrder(order Order, ordering Ordering) { if len(book.Orders) > 0 { var i int @@ -376,4 +416,17 @@ func (book *OrderBook) insertOrder(order Order, ordering Ordering) { } ``` -This completes the order book setup. In the next chapter, you will learn how to deal with vouchers, minting and burning vouchers as well as locking and unlocking native blockchain token in your app. + +This completes the order book setup. + +Now is a good time to save the state of your implementation. +Because your project is in a local repository, you can use git. Saving your current state lets you jump back and forth in case you introduce errors or need a break. + + +```bash +git add . +git commit -m "Create Order Books" +``` + + +In the next chapter, you learn how to deal with vouchers by minting and burning vouchers and locking and unlocking native blockchain token in your app. diff --git a/docs/guide/interchange/05-creating-sell-orders.md b/docs/guide/interchange/05-creating-sell-orders.md deleted file mode 100644 index 459063ccdb..0000000000 --- a/docs/guide/interchange/05-creating-sell-orders.md +++ /dev/null @@ -1,692 +0,0 @@ ---- -order: 5 ---- - -# Creating Sell Orders - -In this chapter you will implement the logic for creating sell orders. - -The packet proto file for a sell order is already generated. Add the seller information. - -```proto -// proto/ibcdex/packet.proto -message SellOrderPacketData { - // ... - string seller = 5; -} -``` - -Now build the proto with the already known command. - -```bash -starport generate proto-go -``` - -## Message Handling in SendSellOrder - -Sell orders are created using `send-sell-order`. This command creates a transaction with a `SendSellOrder` message, which triggers the `SendSellOrder` keeper method. - -`SendSellOrder` should: - -* Check that an order book for specified denom pair exists -* Safely burn or lock tokens - * If the token is an IBC token, burn the tokens - * If the token is a native token, lock the tokens -* Save the voucher received on the target chain to later resolve a denom -* Transmit an IBC packet to the target chain - -```go -// x/ibcdex/keeper/msg_server_sell_order.go -import "errors" - -func (k msgServer) SendSellOrder(goCtx context.Context, msg *types.MsgSendSellOrder) (*types.MsgSendSellOrderResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - // If an order book doesn't exist, throw an error - pairIndex := types.OrderBookIndex(msg.Port, msg.ChannelID, msg.AmountDenom, msg.PriceDenom) - _, found := k.GetSellOrderBook(ctx, pairIndex) - if !found { - return &types.MsgSendSellOrderResponse{}, errors.New("the pair doesn't exist") - } - // Get sender's address - sender, err := sdk.AccAddressFromBech32(msg.Sender) - if err != nil { - return &types.MsgSendSellOrderResponse{}, err - } - // Use SafeBurn to ensure no new native tokens are minted - if err := k.SafeBurn(ctx, msg.Port, msg.ChannelID, sender, msg.AmountDenom, msg.Amount); err != nil { - return &types.MsgSendSellOrderResponse{}, err - } - // Save the voucher received on the other chain, to have the ability to resolve it into the original denom - k.SaveVoucherDenom(ctx, msg.Port, msg.ChannelID, msg.AmountDenom) - var packet types.SellOrderPacketData - packet.Seller = msg.Sender - packet.AmountDenom = msg.AmountDenom - packet.Amount = msg.Amount - packet.PriceDenom = msg.PriceDenom - packet.Price = msg.Price - // Transmit the packet - err = k.TransmitSellOrderPacket(ctx, packet, msg.Port, msg.ChannelID, clienttypes.ZeroHeight(), msg.TimeoutTimestamp) - if err != nil { - return nil, err - } - return &types.MsgSendSellOrderResponse{}, nil -} -``` - -`SendSellOrder` depends on two new keeper methods: `SafeBurn` and `SaveVoucherDenom`. - -### Create the SafeBurn Function to Burn Vouchers or Lock Tokens - -`SafeBurn` burns tokens if they are IBC vouchers (have an `ibc/` prefix) and locks tokens if they are native to the chain. - -```go -// x/ibcdex/keeper/mint.go -package keeper - -import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" - ibctransfertypes "github.com/cosmos/cosmos-sdk/x/ibc/applications/transfer/types" - "github.com/username/interchange/x/ibcdex/types" - "strings" -) - -// isIBCToken checks if the token came from the IBC module -func isIBCToken(denom string) bool { - return strings.HasPrefix(denom, "ibc/") -} - -func (k Keeper) SafeBurn(ctx sdk.Context, port string, channel string, sender sdk.AccAddress, denom string, amount int32) error { - if isIBCToken(denom) { - // Burn the tokens - if err := k.BurnTokens(ctx, sender, sdk.NewCoin(denom, sdk.NewInt(int64(amount)))); err != nil { - return err - } - } else { - // Lock the tokens - if err := k.LockTokens(ctx, port, channel, sender, sdk.NewCoin(denom, sdk.NewInt(int64(amount)))); err != nil { - return err - } - } - return nil -} -``` - -Implement the `BurnTokens` keeper method. - -```go -// x/ibcdex/keeper/mint.go -func (k Keeper) BurnTokens(ctx sdk.Context, sender sdk.AccAddress, tokens sdk.Coin) error { - // transfer the coins to the module account and burn them - if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, sdk.NewCoins(tokens)); err != nil { - return err - } - if err := k.bankKeeper.BurnCoins( - ctx, types.ModuleName, sdk.NewCoins(tokens), - ); err != nil { - // NOTE: should not happen as the module account was - // retrieved on the step above and it has enough balance - // to burn. - panic(fmt.Sprintf("cannot burn coins after a successful send to a module account: %v", err)) - } - - return nil -} -``` - -Implement the `LockTokens` keeper method. - -```go -// x/ibcdex/keeper/mint.go -import ( - ibctransfertypes "github.com/cosmos/cosmos-sdk/x/ibc/applications/transfer/types" -) - -func (k Keeper) LockTokens(ctx sdk.Context, sourcePort string, sourceChannel string, sender sdk.AccAddress, tokens sdk.Coin) error { - // create the escrow address for the tokens - escrowAddress := ibctransfertypes.GetEscrowAddress(sourcePort, sourceChannel) - // escrow source tokens. It fails if balance insufficient - if err := k.bankKeeper.SendCoins( - ctx, sender, escrowAddress, sdk.NewCoins(tokens), - ); err != nil { - return err - } - return nil -} -``` - -`BurnTokens` and `LockTokens` use `SendCoinsFromAccountToModule`, `BurnCoins`, and `SendCoins` keeper methods of the `bank` module. To start using these function from the `ibcdex` module, first add them to the `BankKeeper` interface. - -```go -// x/ibcdex/types/expected_keeper.go -package types - -import sdk "github.com/cosmos/cosmos-sdk/types" - -// BankKeeper defines the expected bank keeper -type BankKeeper interface { - SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error - BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error - SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error -} -``` - -Next, in the `keeper` directory, specify the bank so that you can access it in your module. - -```go -// x/ibcdex/keeper/keeper.go -type ( - Keeper struct { - // ... - bankKeeper types.BankKeeper - } -) - -func NewKeeper( - // ... - bankKeeper types.BankKeeper, -) *Keeper { - return &Keeper{ - // ... - bankKeeper: bankKeeper, - } -} -``` - -Lastly, the `app.go` file that describes which modules are used in the blockchain application, add the bank keeper to the `ibcdexKeeper` - -```go -// app/app.go -app.ibcdexKeeper = *ibcdexmodulekeeper.NewKeeper( - // ... - app.BankKeeper, -) -``` - -The `ibcdex` module will need to mint and burn voucher token using the `bank` account. The use this feature, the module must have a _module account_. To enable the _module account_ declare this permission in the _module account permissions_ structure of the auth module. - -```go -// app/app.go -maccPerms = map[string][]string{ - // ... - ibcdexmoduletypes.ModuleName: {authtypes.Minter, authtypes.Burner}, -} -``` - -### SaveVoucherDenom - -`SaveVoucherDenom` saves the voucher denom to be able to convert it back later. - -```go -// x/ibcdex/keeper/denom.go -package keeper - -func (k Keeper) SaveVoucherDenom(ctx sdk.Context, port string, channel string, denom string) { - voucher := VoucherDenom(port, channel, denom) - - // Store the origin denom - _, saved := k.GetDenomTrace(ctx, voucher) - if !saved { - k.SetDenomTrace(ctx, types.DenomTrace{ - Index: voucher, - Port: port, - Channel: channel, - Origin: denom, - }) - } -} -``` - -Finally, last function we need to implement is `VoucherDenom`. `VoucherDenom` returns the voucher of the denom from the port ID and channel ID. - -```go -// x/ibcdex/keeper/denom.go -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - ibctransfertypes "github.com/cosmos/cosmos-sdk/x/ibc/applications/transfer/types" - "github.com/username/interchange/x/ibcdex/types" -) - -func VoucherDenom(port string, channel string, denom string) string { - // since SendPacket did not prefix the denomination, we must prefix denomination here - sourcePrefix := ibctransfertypes.GetDenomPrefix(port, channel) - // NOTE: sourcePrefix contains the trailing "/" - prefixedDenom := sourcePrefix + denom - // construct the denomination trace from the full raw denomination - denomTrace := ibctransfertypes.ParseDenomTrace(prefixedDenom) - voucher := denomTrace.IBCDenom() - return voucher[:16] -} -``` - -## On Receiving a Sell Order - -When a "sell order" packet is received on the target chain, the module should: - -- Update the sell order book -- Distribute sold token to the buyer -- Send to chain A the sell order after the fill attempt - -```go -// x/ibcdex/keeper/sell_order.go -func (k Keeper) OnRecvSellOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.SellOrderPacketData) (packetAck types.SellOrderPacketAck, err error) { - if err := data.ValidateBasic(); err != nil { - return packetAck, err - } - pairIndex := types.OrderBookIndex(packet.SourcePort, packet.SourceChannel, data.AmountDenom, data.PriceDenom) - book, found := k.GetBuyOrderBook(ctx, pairIndex) - if !found { - return packetAck, errors.New("the pair doesn't exist") - } - // Fill sell order - remaining, liquidated, gain, _ := book.FillSellOrder(types.Order{ - Amount: data.Amount, - Price: data.Price, - }) - // Return remaining amount and gains - packetAck.RemainingAmount = remaining.Amount - packetAck.Gain = gain - // Before distributing sales, we resolve the denom - // First we check if the denom received comes from this chain originally - finalAmountDenom, saved := k.OriginalDenom(ctx, packet.DestinationPort, packet.DestinationChannel, data.AmountDenom) - if !saved { - // If it was not from this chain we use voucher as denom - finalAmountDenom = VoucherDenom(packet.SourcePort, packet.SourceChannel, data.AmountDenom) - } - // Dispatch liquidated buy orders - for _, liquidation := range liquidated { - liquidation := liquidation - addr, err := sdk.AccAddressFromBech32(liquidation.Creator) - if err != nil { - return packetAck, err - } - if err := k.SafeMint(ctx, packet.DestinationPort, packet.DestinationChannel, addr, finalAmountDenom, liquidation.Amount); err != nil { - return packetAck, err - } - } - // Save the new order book - k.SetBuyOrderBook(ctx, book) - return packetAck, nil -} -``` - -### Implement a FillSellOrder Function - -`FillSellOrder` try to fill the sell order with the order book and returns all the side effects. - -```go -// x/ibcdex/types/buy_order_book.go -func (b *BuyOrderBook) FillSellOrder(order Order) (remainingSellOrder Order, liquidated []Order, gain int32, filled bool) { - var liquidatedList []Order - totalGain := int32(0) - remainingSellOrder = order - // Liquidate as long as there is match - for { - var match bool - var liquidation Order - remainingSellOrder, liquidation, gain, match, filled = b.LiquidateFromSellOrder( - remainingSellOrder, - ) - if !match { - break - } - // Update gains - totalGain += gain - // Update liquidated - liquidatedList = append(liquidatedList, liquidation) - if filled { - break - } - } - return remainingSellOrder, liquidatedList, totalGain, filled -} -``` - -#### Implement a `LiquidateFromSellOrder` Function - -`LiquidateFromSellOrder` liquidates the first buy order of the book from the sell order if no match is found, return false for match. - -```go -// x/ibcdex/types/buy_order_book.go -func (b *BuyOrderBook) LiquidateFromSellOrder(order Order) ( remainingSellOrder Order, liquidatedBuyOrder Order, gain int32, match bool, filled bool) { - remainingSellOrder = order - // No match if no order - orderCount := len(b.Book.Orders) - if orderCount == 0 { - return order, liquidatedBuyOrder, gain, false, false - } - // Check if match - highestBid := b.Book.Orders[orderCount-1] - if order.Price > highestBid.Price { - return order, liquidatedBuyOrder, gain, false, false - } - liquidatedBuyOrder = *highestBid - // Check if sell order can be entirely filled - if highestBid.Amount >= order.Amount { - remainingSellOrder.Amount = 0 - liquidatedBuyOrder.Amount = order.Amount - gain = order.Amount * highestBid.Price - // Remove highest bid if it has been entirely liquidated - highestBid.Amount -= order.Amount - if highestBid.Amount == 0 { - b.Book.Orders = b.Book.Orders[:orderCount-1] - } else { - b.Book.Orders[orderCount-1] = highestBid - } - return remainingSellOrder, liquidatedBuyOrder, gain, true, true - } - // Not entirely filled - gain = highestBid.Amount * highestBid.Price - b.Book.Orders = b.Book.Orders[:orderCount-1] - remainingSellOrder.Amount -= highestBid.Amount - return remainingSellOrder, liquidatedBuyOrder, gain, true, false -} -``` - -### Implement a OriginalDenom Function - -`OriginalDenom` returns back the original denom of the voucher. False is returned if the port ID and channel ID provided are not the origins of the voucher - -```go -// x/ibcdex/keeper/denom.go -func (k Keeper) OriginalDenom(ctx sdk.Context, port string, channel string, voucher string) (string, bool) { - trace, exist := k.GetDenomTrace(ctx, voucher) - if exist { - // Check if original port and channel - if trace.Port == port && trace.Channel == channel { - return trace.Origin, true - } - } - // Not the original chain - return "", false -} -``` - - -### Implement a SafeMint Function -If a token is an IBC token (has an `ibc/` prefix) `SafeMint` mints IBC tokens with `MintTokens`, otherwise, it unlocks native tokens with `UnlockTokens`. -```go -// x/ibcdex/keeper/mint.go -func (k Keeper) SafeMint(ctx sdk.Context, port string, channel string, receiver sdk.AccAddress, denom string, amount int32) error { - if isIBCToken(denom) { - // Mint IBC tokens - if err := k.MintTokens(ctx, receiver, sdk.NewCoin(denom, sdk.NewInt(int64(amount)))); err != nil { - return err - } - } else { - // Unlock native tokens - if err := k.UnlockTokens( - ctx, - port, - channel, - receiver, - sdk.NewCoin(denom, sdk.NewInt(int64(amount))), - ); err != nil { - return err - } - } - return nil -} -``` - -#### Implement a `MintTokens` Function - -```go -// x/ibcdex/keeper/mint.go -func (k Keeper) MintTokens(ctx sdk.Context, receiver sdk.AccAddress, tokens sdk.Coin) error { - // mint new tokens if the source of the transfer is the same chain - if err := k.bankKeeper.MintCoins( - ctx, types.ModuleName, sdk.NewCoins(tokens), - ); err != nil { - return err - } - // send to receiver - if err := k.bankKeeper.SendCoinsFromModuleToAccount( - ctx, types.ModuleName, receiver, sdk.NewCoins(tokens), - ); err != nil { - panic(fmt.Sprintf("unable to send coins from module to account despite previously minting coins to module account: %v", err)) - } - return nil -} -``` - -`MintTokens` uses two keeper methods from the `bank` module: `MintCoins` and `SendCoinsFromModuleToAccount`. Import them by adding their signatures to the `BankKeeper` interface. - -```go -// x/ibcdex/types/expected_keeper.go -type BankKeeper interface { - // ... - MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error - SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error -} -``` - -```go -// x/ibcdex/keeper/mint.go -func (k Keeper) UnlockTokens(ctx sdk.Context, sourcePort string, sourceChannel string, receiver sdk.AccAddress, tokens sdk.Coin) error { - // create the escrow address for the tokens - escrowAddress := ibctransfertypes.GetEscrowAddress(sourcePort, sourceChannel) - // escrow source tokens. It fails if balance insufficient - if err := k.bankKeeper.SendCoins( - ctx, escrowAddress, receiver, sdk.NewCoins(tokens), - ); err != nil { - return err - } - return nil -} -``` - -## Implement the `OnAcknowledgement` Function for Sell Order Packets - -Once an IBC packet is processed on the target chain, an acknowledgement is returned to the source chain and processed in `OnAcknowledgementSellOrderPacket`. The module on the source chain will store the remaining sell order in the sell order book and will distribute sold tokens to the buyers and will distribute to the seller the price of the amount sold. On error the module mints the burned tokens. - -```go -// x/ibcdex/keeper/sell_order.go -func (k Keeper) OnAcknowledgementSellOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.SellOrderPacketData, ack channeltypes.Acknowledgement) error { - switch dispatchedAck := ack.Response.(type) { - case *channeltypes.Acknowledgement_Error: - // In case of error we mint back the native token - receiver, err := sdk.AccAddressFromBech32(data.Seller) - if err != nil { - return err - } - if err := k.SafeMint(ctx, packet.SourcePort, packet.SourceChannel, receiver, data.AmountDenom, data.Amount); err != nil { - return err - } - return nil - case *channeltypes.Acknowledgement_Result: - // Decode the packet acknowledgment - var packetAck types.SellOrderPacketAck - if err := types.ModuleCdc.UnmarshalJSON(dispatchedAck.Result, &packetAck); err != nil { - // The counter-party module doesn't implement the correct acknowledgment format - return errors.New("cannot unmarshal acknowledgment") - } - // Get the sell order book - pairIndex := types.OrderBookIndex(packet.SourcePort, packet.SourceChannel, data.AmountDenom, data.PriceDenom) - book, found := k.GetSellOrderBook(ctx, pairIndex) - if !found { - panic("sell order book must exist") - } - // Append the remaining amount of the order - if packetAck.RemainingAmount > 0 { - _, err := book.AppendOrder(data.Seller, packetAck.RemainingAmount, data.Price) - if err != nil { - return err - } - // Save the new order book - k.SetSellOrderBook(ctx, book) - } - // Mint the gains - if packetAck.Gain > 0 { - receiver, err := sdk.AccAddressFromBech32(data.Seller) - if err != nil { - return err - } - finalPriceDenom, saved := k.OriginalDenom(ctx, packet.SourcePort, packet.SourceChannel, data.PriceDenom) - if !saved { - // If it was not from this chain we use voucher as denom - finalPriceDenom = VoucherDenom(packet.DestinationPort, packet.DestinationChannel, data.PriceDenom) - } - if err := k.SafeMint(ctx, packet.SourcePort, packet.SourceChannel, receiver, finalPriceDenom, packetAck.Gain); err != nil { - return err - } - } - return nil - default: - // The counter-party module doesn't implement the correct acknowledgment format - return errors.New("invalid acknowledgment format") - } -} -``` - -```go -// x/ibcdex/types/sell_order_book.go -func (s *SellOrderBook) AppendOrder(creator string, amount int32, price int32) (int32, error) { - return s.Book.appendOrder(creator, amount, price, Decreasing) -} -``` - -### Implement the `appendOrder` Function to Add Orders to the Order Book - -```go -// x/ibcdex/types/order_book.go -package types - -import ( - "errors" - "sort" -) - -const ( - MaxAmount = int32(100000) - MaxPrice = int32(100000) -) - -type Ordering int - -const ( - Increasing Ordering = iota - Decreasing -) - -var ( - ErrMaxAmount = errors.New("max amount reached") - ErrMaxPrice = errors.New("max price reached") - ErrZeroAmount = errors.New("amount is zero") - ErrZeroPrice = errors.New("price is zero") - ErrOrderNotFound = errors.New("order not found") -) -``` - -`AppendOrder` initializes and appends a new order to an order book from the order information. - -```go -// x/ibcdex/types/order_book.go -func (book *OrderBook) appendOrder(creator string, amount int32, price int32, ordering Ordering) (int32, error) { - if err := checkAmountAndPrice(amount, price); err != nil { - return 0, err - } - // Initialize the order - var order Order - order.Id = book.GetNextOrderID() - order.Creator = creator - order.Amount = amount - order.Price = price - // Increment ID tracker - book.IncrementNextOrderID() - // Insert the order - book.insertOrder(order, ordering) - return order.Id, nil -} -``` - -#### Implement the checkAmountAndPrice For an Order - -`checkAmountAndPrice` checks correct amount or price. - -```go -// x/ibcdex/types/order_book.go -func checkAmountAndPrice(amount int32, price int32) error { - if amount == int32(0) { - return ErrZeroAmount - } - if amount > MaxAmount { - return ErrMaxAmount - } - if price == int32(0) { - return ErrZeroPrice - } - if price > MaxPrice { - return ErrMaxPrice - } - return nil -} -``` - -#### Implement the GetNextOrderID Function - -`GetNextOrderID` gets the ID of the next order to append - -```go -// x/ibcdex/types/order_book.go -func (book OrderBook) GetNextOrderID() int32 { - return book.IdCount -} -``` - -#### Implement the IncrementNextOrderID Function - -`IncrementNextOrderID` updates the ID count for orders - -```go -// x/ibcdex/types/order_book.go -func (book *OrderBook) IncrementNextOrderID() { - // Even numbers to have different ID than buy orders - book.IdCount++ -} -``` - -#### Implement the insertOrder Function - -`insertOrder` inserts the order in the book with the provided order - -```go -// x/ibcdex/types/order_book.go -func (book *OrderBook) insertOrder(order Order, ordering Ordering) { - if len(book.Orders) > 0 { - var i int - // get the index of the new order depending on the provided ordering - if ordering == Increasing { - i = sort.Search(len(book.Orders), func(i int) bool { return book.Orders[i].Price > order.Price }) - } else { - i = sort.Search(len(book.Orders), func(i int) bool { return book.Orders[i].Price < order.Price }) - } - // insert order - orders := append(book.Orders, &order) - copy(orders[i+1:], orders[i:]) - orders[i] = &order - book.Orders = orders - } else { - book.Orders = append(book.Orders, &order) - } -} -``` - -## Add the OnTimeout of a Sell Order Packet Function - -If a timeout occurs, we mint back the native token. - -```go -// x/ibcdex/keeper/sell_order.go -func (k Keeper) OnTimeoutSellOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.SellOrderPacketData) error { - // In case of error we mint back the native token - receiver, err := sdk.AccAddressFromBech32(data.Seller) - if err != nil { - return err - } - if err := k.SafeMint(ctx, packet.SourcePort, packet.SourceChannel, receiver, data.AmountDenom, data.Amount); err != nil { - return err - } - return nil -} -``` diff --git a/docs/guide/interchange/05-mint-and-burn-voucher.md b/docs/guide/interchange/05-mint-and-burn-voucher.md index cd3ea021b1..a667a7512c 100644 --- a/docs/guide/interchange/05-mint-and-burn-voucher.md +++ b/docs/guide/interchange/05-mint-and-burn-voucher.md @@ -1,30 +1,40 @@ --- order: 5 +description: Mint vouchers and lock and unlock native token from a blockchain. --- -# Mint and Burn Voucher +# Mint and Burn Vouchers + +In this chapter, you learn about vouchers. The `dex` module implementation mints vouchers and locks and unlocks native token from a blockchain. + +There is a lot to learn from this `dex` module implementation: + +- You work with the `bank` keeper and use several methods it offers. +- You interact with another module and use the module account to lock tokens. + +This implementation can teach you how to use various interactions with module accounts or minting, locking or burning tokens. -In this chapter you will learn more about vouchers and how the implementation mints voucher or locks native token from a blockchain. ## Create the SafeBurn Function to Burn Vouchers or Lock Tokens -`SafeBurn` burns tokens if they are IBC vouchers (have an `ibc/` prefix) and locks tokens if they are native to the chain. +The `SafeBurn` function burns tokens if they are IBC vouchers (have an `ibc/` prefix) and locks tokens if they are native to the chain. -Create a new file in the `ibcdex/keeper` called `mint.go` +Create a new file in the `dex/keeper` called `mint.go`: ```go -// x/ibcdex/keeper/mint.go +// x/dex/keeper/mint.go package keeper import ( "fmt" sdk "github.com/cosmos/cosmos-sdk/types" - ibctransfertypes "github.com/cosmos/cosmos-sdk/x/ibc/applications/transfer/types" - "github.com/username/interchange/x/ibcdex/types" + ibctransfertypes "github.com/cosmos/ibc-go/modules/apps/transfer/types" + "github.com/cosmonaut/interchange/x/dex/types" "strings" ) // isIBCToken checks if the token came from the IBC module +// Each IBC token starts with an ibc/ denom, the check is rather simple func isIBCToken(denom string) bool { return strings.HasPrefix(denom, "ibc/") } @@ -45,10 +55,12 @@ func (k Keeper) SafeBurn(ctx sdk.Context, port string, channel string, sender sd } ``` -Implement the `BurnTokens` keeper method. +If the token comes from another blockchain as an IBC token, the burning method actually burns those IBC tokens on one chain and unlocks them on the other chain. The native token are locked away. + +Now, implement the `BurnTokens` keeper method as used in the previous function. The `bankKeeper` has a useful function for this: ```go -// x/ibcdex/keeper/mint.go +// x/dex/keeper/mint.go func (k Keeper) BurnTokens(ctx sdk.Context, sender sdk.AccAddress, tokens sdk.Coin) error { // transfer the coins to the module account and burn them if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleName, sdk.NewCoins(tokens)); err != nil { @@ -67,14 +79,12 @@ func (k Keeper) BurnTokens(ctx sdk.Context, sender sdk.AccAddress, tokens sdk.Co } ``` -Implement the `LockTokens` keeper method. +Implement the `LockTokens` keeper method. -```go -// x/ibcdex/keeper/mint.go -import ( - ibctransfertypes "github.com/cosmos/cosmos-sdk/x/ibc/applications/transfer/types" -) +To lock token from a native chain, you can send the native token to the Escrow Address: +```go +// x/dex/keeper/mint.go func (k Keeper) LockTokens(ctx sdk.Context, sourcePort string, sourceChannel string, sender sdk.AccAddress, tokens sdk.Coin) error { // create the escrow address for the tokens escrowAddress := ibctransfertypes.GetEscrowAddress(sourcePort, sourceChannel) @@ -88,10 +98,12 @@ func (k Keeper) LockTokens(ctx sdk.Context, sourcePort string, sourceChannel str } ``` -`BurnTokens` and `LockTokens` use `SendCoinsFromAccountToModule`, `BurnCoins`, and `SendCoins` keeper methods of the `bank` module. To start using these function from the `ibcdex` module, first add them to the `BankKeeper` interface. +`BurnTokens` and `LockTokens` use `SendCoinsFromAccountToModule`, `BurnCoins`, and `SendCoins` keeper methods of the `bank` module. + +To start using these function from the `dex` module, first add them to the `BankKeeper` interface in the `expected_keepers.go` file. ```go -// x/ibcdex/types/expected_keeper.go +// x/dex/types/expected_keepers.go package types import sdk "github.com/cosmos/cosmos-sdk/types" @@ -104,56 +116,14 @@ type BankKeeper interface { } ``` - - - - -## SaveVoucherDenom - -`SaveVoucherDenom` saves the voucher denom to be able to convert it back later. - -Create a new `denom.go` file in the `keeper` directory. - -```go -// x/ibcdex/keeper/denom.go +// x/dex/keeper/denom.go package keeper func (k Keeper) SaveVoucherDenom(ctx sdk.Context, port string, channel string, denom string) { @@ -172,14 +142,13 @@ func (k Keeper) SaveVoucherDenom(ctx sdk.Context, port string, channel string, d } ``` -Finally, last function we need to implement is `VoucherDenom`. `VoucherDenom` returns the voucher of the denom from the port ID and channel ID. +Finally, the last function to implement is the `VoucherDenom` function that returns the voucher of the denom from the port ID and channel ID: ```go -// x/ibcdex/keeper/denom.go +// x/dex/keeper/denom.go import ( - sdk "github.com/cosmos/cosmos-sdk/types" - ibctransfertypes "github.com/cosmos/cosmos-sdk/x/ibc/applications/transfer/types" - "github.com/cosmonaut/interchange/x/ibcdex/types" + // ... + ibctransfertypes "github.com/cosmos/ibc-go/modules/apps/transfer/types" ) func VoucherDenom(port string, channel string, denom string) string { @@ -194,12 +163,14 @@ func VoucherDenom(port string, channel string, denom string) string { } ``` -### Implement a OriginalDenom Function +### Implement an OriginalDenom Function + +The `OriginalDenom` function returns back the original denom of the voucher. -`OriginalDenom` returns back the original denom of the voucher. False is returned if the port ID and channel ID provided are not the origins of the voucher +False is returned if the port ID and channel ID provided are not the origins of the voucher: ```go -// x/ibcdex/keeper/denom.go +// x/dex/keeper/denom.go func (k Keeper) OriginalDenom(ctx sdk.Context, port string, channel string, voucher string) (string, bool) { trace, exist := k.GetDenomTrace(ctx, voucher) if exist { @@ -213,11 +184,14 @@ func (k Keeper) OriginalDenom(ctx sdk.Context, port string, channel string, vouc } ``` - ### Implement a SafeMint Function -If a token is an IBC token (has an `ibc/` prefix) `SafeMint` mints IBC tokens with `MintTokens`, otherwise, it unlocks native tokens with `UnlockTokens`. + +If a token is an IBC token (has an `ibc/` prefix), the `SafeMint` function mints IBC token with `MintTokens`. Otherwise, it unlocks native token with `UnlockTokens`. + +Go back to the `mint.go` file in the `keeper` directory and add the following code: + ```go -// x/ibcdex/keeper/mint.go +// x/dex/keeper/mint.go func (k Keeper) SafeMint(ctx sdk.Context, port string, channel string, receiver sdk.AccAddress, denom string, amount int32) error { if isIBCToken(denom) { // Mint IBC tokens @@ -242,8 +216,10 @@ func (k Keeper) SafeMint(ctx sdk.Context, port string, channel string, receiver #### Implement a `MintTokens` Function +You can use the `bankKeeper` function again to MintCoins. These token will then be sent to the receiver account: + ```go -// x/ibcdex/keeper/mint.go +// x/dex/keeper/mint.go func (k Keeper) MintTokens(ctx sdk.Context, receiver sdk.AccAddress, tokens sdk.Coin) error { // mint new tokens if the source of the transfer is the same chain if err := k.bankKeeper.MintCoins( @@ -261,10 +237,10 @@ func (k Keeper) MintTokens(ctx sdk.Context, receiver sdk.AccAddress, tokens sdk. } ``` -Finally, add the function to unlock tokens when they are sent back to the native blockchain. +Finally, add the function to unlock token after they are sent back to the native blockchain: ```go -// x/ibcdex/keeper/mint.go +// x/dex/keeper/mint.go func (k Keeper) UnlockTokens(ctx sdk.Context, sourcePort string, sourceChannel string, receiver sdk.AccAddress, tokens sdk.Coin) error { // create the escrow address for the tokens escrowAddress := ibctransfertypes.GetEscrowAddress(sourcePort, sourceChannel) @@ -278,13 +254,28 @@ func (k Keeper) UnlockTokens(ctx sdk.Context, sourcePort string, sourceChannel s } ``` -`MintTokens` uses two keeper methods from the `bank` module: `MintCoins` and `SendCoinsFromModuleToAccount`. Import them by adding their signatures to the `BankKeeper` interface. +The `MintTokens` function uses two keeper methods from the `bank` module: `MintCoins` and `SendCoinsFromModuleToAccount`. To import these methods, add their signatures to the `BankKeeper` interface in the `expected_keepers.go` file: ```go -// x/ibcdex/types/expected_keeper.go +// x/dex/types/expected_keepers.go type BankKeeper interface { // ... MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error } ``` + + +## Summary + +You finished the mint and burn voucher logic. + +It is a good time to make another git commit to save the state of your work: + + +```bash +git add . +git commit -m "Add Mint and Burn Voucher" +``` + +In the next chapter, you look into creating sell orders. diff --git a/docs/guide/interchange/06-creating-sell-orders.md b/docs/guide/interchange/06-creating-sell-orders.md index 385364ad3c..3410a31e3d 100644 --- a/docs/guide/interchange/06-creating-sell-orders.md +++ b/docs/guide/interchange/06-creating-sell-orders.md @@ -1,22 +1,23 @@ --- order: 6 +description: Implement logic to create sell orders. --- -# Creating Sell Orders +# Create Sell Orders -In this chapter you will implement the logic for creating sell orders. +In this chapter, you implement the logic for creating sell orders. -The packet proto file for a sell order is already generated. Add the seller information. +The packet proto file for a sell order is already generated. Add the seller information: ```proto -// proto/ibcdex/packet.proto +// proto/dex/packet.proto message SellOrderPacketData { // ... string seller = 5; } ``` -Now build the proto with the already known command. +Now, use Starport CLI to build the proto files for the `send-sell-order` command. You used this command in a previous chapter. ```bash starport generate proto-go @@ -24,19 +25,19 @@ starport generate proto-go ## Message Handling in SendSellOrder -Sell orders are created using `send-sell-order`. This command creates a transaction with a `SendSellOrder` message, which triggers the `SendSellOrder` keeper method. +Sell orders are created using the `send-sell-order` command. This command creates a transaction with a `SendSellOrder` message that triggers the `SendSellOrder` keeper method. -`SendSellOrder` should: +The `SendSellOrder` command: -* Check that an order book for specified denom pair exists -* Safely burn or lock tokens - * If the token is an IBC token, burn the tokens - * If the token is a native token, lock the tokens -* Save the voucher received on the target chain to later resolve a denom -* Transmit an IBC packet to the target chain +* Checks that an order book for a specified denom pair exists. +* Safely burns or locks token. + * If the token is an IBC token, burn the token. + * If the token is a native token, lock the token. +* Saves the voucher that is received on the target chain to later resolve a denom. +* Transmits an IBC packet to the target chain. ```go -// x/ibcdex/keeper/msg_server_sell_order.go +// x/dex/keeper/msg_server_sell_order.go import "errors" func (k msgServer) SendSellOrder(goCtx context.Context, msg *types.MsgSendSellOrder) (*types.MsgSendSellOrderResponse, error) { @@ -48,7 +49,7 @@ func (k msgServer) SendSellOrder(goCtx context.Context, msg *types.MsgSendSellOr return &types.MsgSendSellOrderResponse{}, errors.New("the pair doesn't exist") } // Get sender's address - sender, err := sdk.AccAddressFromBech32(msg.Sender) + sender, err := sdk.AccAddressFromBech32(msg.Creator) if err != nil { return &types.MsgSendSellOrderResponse{}, err } @@ -59,7 +60,7 @@ func (k msgServer) SendSellOrder(goCtx context.Context, msg *types.MsgSendSellOr // Save the voucher received on the other chain, to have the ability to resolve it into the original denom k.SaveVoucherDenom(ctx, msg.Port, msg.ChannelID, msg.AmountDenom) var packet types.SellOrderPacketData - packet.Seller = msg.Sender + packet.Seller = msg.Creator packet.AmountDenom = msg.AmountDenom packet.Amount = msg.Amount packet.PriceDenom = msg.PriceDenom @@ -73,18 +74,16 @@ func (k msgServer) SendSellOrder(goCtx context.Context, msg *types.MsgSendSellOr } ``` -`SendSellOrder` depends on two new keeper methods: `SafeBurn` and `SaveVoucherDenom`. - ## On Receiving a Sell Order -When a "sell order" packet is received on the target chain, the module should: +When a "sell order" packet is received on the target chain, you want the module to: -- Update the sell order book -- Distribute sold token to the buyer -- Send to chain A the sell order after the fill attempt +* Update the sell order book +* Distribute sold token to the buyer +* Send the sell order to chain A after the fill attempt ```go -// x/ibcdex/keeper/sell_order.go +// x/dex/keeper/sell_order.go func (k Keeper) OnRecvSellOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.SellOrderPacketData) (packetAck types.SellOrderPacketAck, err error) { if err := data.ValidateBasic(); err != nil { return packetAck, err @@ -126,85 +125,116 @@ func (k Keeper) OnRecvSellOrderPacket(ctx sdk.Context, packet channeltypes.Packe } ``` -### Implement a FillSellOrder Function +### Implement a FillBuyOrder Function -`FillSellOrder` try to fill the sell order with the order book and returns all the side effects. +The `FillBuyOrder` function tries to fill the sell order with the order book and returns all the side effects: ```go -// x/ibcdex/types/sell_order_book.go -func (b *BuyOrderBook) FillSellOrder(order Order) (remainingSellOrder Order, liquidated []Order, gain int32, filled bool) { +// x/dex/types/sell_order_book.go +func (s *SellOrderBook) FillBuyOrder(order Order) ( + remainingBuyOrder Order, + liquidated []Order, + purchase int32, + filled bool, +) { var liquidatedList []Order - totalGain := int32(0) - remainingSellOrder = order + totalPurchase := int32(0) + remainingBuyOrder = order + // Liquidate as long as there is match for { var match bool var liquidation Order - remainingSellOrder, liquidation, gain, match, filled = b.LiquidateFromSellOrder( - remainingSellOrder, + remainingBuyOrder, liquidation, purchase, match, filled = s.LiquidateFromBuyOrder( + remainingBuyOrder, ) if !match { break } + // Update gains - totalGain += gain + totalPurchase += purchase + // Update liquidated liquidatedList = append(liquidatedList, liquidation) + if filled { break } } - return remainingSellOrder, liquidatedList, totalGain, filled + + return remainingBuyOrder, liquidatedList, totalPurchase, filled } ``` -#### Implement a `LiquidateFromSellOrder` Function +### Implement a LiquidateFromBuyOrder Function -`LiquidateFromSellOrder` liquidates the first buy order of the book from the sell order if no match is found, return false for match. +The `LiquidateFromBuyOrder` function liquidates the first buy order of the book from the sell order. If no match is found, return false for match: ```go -// x/ibcdex/types/sell_order_book.go -func (b *BuyOrderBook) LiquidateFromSellOrder(order Order) ( remainingSellOrder Order, liquidatedBuyOrder Order, gain int32, match bool, filled bool) { - remainingSellOrder = order - // No match if no order - orderCount := len(b.Book.Orders) - if orderCount == 0 { - return order, liquidatedBuyOrder, gain, false, false - } - // Check if match - highestBid := b.Book.Orders[orderCount-1] - if order.Price > highestBid.Price { - return order, liquidatedBuyOrder, gain, false, false - } - liquidatedBuyOrder = *highestBid - // Check if sell order can be entirely filled - if highestBid.Amount >= order.Amount { - remainingSellOrder.Amount = 0 - liquidatedBuyOrder.Amount = order.Amount - gain = order.Amount * highestBid.Price - // Remove highest bid if it has been entirely liquidated - highestBid.Amount -= order.Amount - if highestBid.Amount == 0 { - b.Book.Orders = b.Book.Orders[:orderCount-1] - } else { - b.Book.Orders[orderCount-1] = highestBid - } - return remainingSellOrder, liquidatedBuyOrder, gain, true, true - } - // Not entirely filled - gain = highestBid.Amount * highestBid.Price - b.Book.Orders = b.Book.Orders[:orderCount-1] - remainingSellOrder.Amount -= highestBid.Amount - return remainingSellOrder, liquidatedBuyOrder, gain, true, false +// x/dex/types/sell_order_book.go +func (s *SellOrderBook) LiquidateFromBuyOrder(order Order) ( + remainingBuyOrder Order, + liquidatedSellOrder Order, + purchase int32, + match bool, + filled bool, +) { + remainingBuyOrder = order + + // No match if no order + orderCount := len(s.Book.Orders) + if orderCount == 0 { + return order, liquidatedSellOrder, purchase, false, false + } + + // Check if match + lowestAsk := s.Book.Orders[orderCount-1] + if order.Price < lowestAsk.Price { + return order, liquidatedSellOrder, purchase, false, false + } + + liquidatedSellOrder = *lowestAsk + + // Check if buy order can be entirely filled + if lowestAsk.Amount >= order.Amount { + remainingBuyOrder.Amount = 0 + liquidatedSellOrder.Amount = order.Amount + purchase = order.Amount + + // Remove lowest ask if it has been entirely liquidated + lowestAsk.Amount -= order.Amount + if lowestAsk.Amount == 0 { + s.Book.Orders = s.Book.Orders[:orderCount-1] + } else { + s.Book.Orders[orderCount-1] = lowestAsk + } + + return remainingBuyOrder, liquidatedSellOrder, purchase, true, true + } + + // Not entirely filled + purchase = lowestAsk.Amount + s.Book.Orders = s.Book.Orders[:orderCount-1] + remainingBuyOrder.Amount -= lowestAsk.Amount + + return remainingBuyOrder, liquidatedSellOrder, purchase, true, false } ``` -## Implement the `OnAcknowledgement` Function for Sell Order Packets +### Implement the OnAcknowledgement Function for Sell Order Packets + +After an IBC packet is processed on the target chain, an acknowledgement is returned to the source chain and processed by the `OnAcknowledgementSellOrderPacket` function. + +The dex module on the source chain: -Once an IBC packet is processed on the target chain, an acknowledgement is returned to the source chain and processed in `OnAcknowledgementSellOrderPacket`. The module on the source chain will store the remaining sell order in the sell order book and will distribute sold tokens to the buyers and will distribute to the seller the price of the amount sold. On error the module mints the burned tokens. +- Stores the remaining sell order in the sell order book. +- Distributes sold tokens to the buyers. +- Distributes the price of the amount sold to the seller. +- On error, mints the burned tokens. ```go -// x/ibcdex/keeper/sell_order.go +// x/dex/keeper/sell_order.go func (k Keeper) OnAcknowledgementSellOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.SellOrderPacketData, ack channeltypes.Acknowledgement) error { switch dispatchedAck := ack.Response.(type) { case *channeltypes.Acknowledgement_Error: @@ -263,18 +293,18 @@ func (k Keeper) OnAcknowledgementSellOrderPacket(ctx sdk.Context, packet channel ``` ```go -// x/ibcdex/types/sell_order_book.go +// x/dex/types/sell_order_book.go func (s *SellOrderBook) AppendOrder(creator string, amount int32, price int32) (int32, error) { return s.Book.appendOrder(creator, amount, price, Decreasing) } ``` -## Add the OnTimeout of a Sell Order Packet Function +### Add the OnTimeout of a Sell Order Packet Function -If a timeout occurs, we mint back the native token. +If a timeout occurs, mint back the native token: ```go -// x/ibcdex/keeper/sell_order.go +// x/dex/keeper/sell_order.go func (k Keeper) OnTimeoutSellOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.SellOrderPacketData) error { // In case of error we mint back the native token receiver, err := sdk.AccAddressFromBech32(data.Seller) @@ -287,3 +317,14 @@ func (k Keeper) OnTimeoutSellOrderPacket(ctx sdk.Context, packet channeltypes.Pa return nil } ``` + +## Summary + +Great, you have completed the sell order logic. + +It is a good time to make another git commit again to save the state of your work: + +```bash +git add . +git commit -m "Add Sell Orders" +``` diff --git a/docs/guide/interchange/07-creating-buy-orders.md b/docs/guide/interchange/07-creating-buy-orders.md index 95b1481f6d..4c3975978d 100644 --- a/docs/guide/interchange/07-creating-buy-orders.md +++ b/docs/guide/interchange/07-creating-buy-orders.md @@ -1,24 +1,25 @@ --- order: 7 +description: Implement the buy order logic. --- # Creating Buy Orders -In this chapter you will be implementing creation of buy orders. The logic is very similar to the previous chapter. +In this chapter, you implement the creation of buy orders. The logic is very similar to the sell order logic you implemented in the previous chapter. ## Modify the Proto Definition -Add the buyer to the proto file definition +Add the buyer to the proto file definition: ```proto -// proto/ibcdex/packet.proto +// proto/dex/packet.proto message BuyOrderPacketData { // ... string buyer = 5; } ``` -Now build the proto with the already known command. +Now, use Starport CLI to build the proto files for the `send-buy-order` command. You used this command in previous chapters. ```bash starport generate proto-go @@ -32,7 +33,7 @@ starport generate proto-go * Save the voucher received on the target chain to later resolve a denom ```go -// x/ibcdex/keeper/msg_server_buy_order.go +// x/dex/keeper/msg_server_buy_order.go import ( //... "errors" @@ -47,7 +48,7 @@ func (k msgServer) SendBuyOrder(goCtx context.Context, msg *types.MsgSendBuyOrde return &types.MsgSendBuyOrderResponse{}, errors.New("the pair doesn't exist") } // Lock the token to send - sender, err := sdk.AccAddressFromBech32(msg.Sender) + sender, err := sdk.AccAddressFromBech32(msg.Creator) if err != nil { return &types.MsgSendBuyOrderResponse{}, err } @@ -59,7 +60,7 @@ func (k msgServer) SendBuyOrder(goCtx context.Context, msg *types.MsgSendBuyOrde k.SaveVoucherDenom(ctx, msg.Port, msg.ChannelID, msg.PriceDenom) // Construct the packet var packet types.BuyOrderPacketData - packet.Buyer = msg.Sender + packet.Buyer = msg.Creator // Transmit an IBC packet... return &types.MsgSendBuyOrderResponse{}, nil } @@ -67,12 +68,12 @@ func (k msgServer) SendBuyOrder(goCtx context.Context, msg *types.MsgSendBuyOrde ## On Receiving a Buy Order -- Update the buy order book -- Distribute sold token to the buyer -- Send to chain A the sell order after the fill attempt +* Update the buy order book +* Distribute sold token to the buyer +* Send to chain A the sell order after the fill attempt ```go -// x/ibcdex/keeper/buy_order.go +// x/dex/keeper/buy_order.go func (k Keeper) OnRecvBuyOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.BuyOrderPacketData) (packetAck types.BuyOrderPacketAck, err error) { // validate packet data upon receiving if err := data.ValidateBasic(); err != nil { @@ -123,86 +124,114 @@ func (k Keeper) OnRecvBuyOrderPacket(ctx sdk.Context, packet channeltypes.Packet } ``` -### Implement the FillBuyOrder Function +### Implement the FillSellOrder Function -`FillBuyOrder` try to fill the buy order with the order book and returns all the side effects. +The `FillSellOrder` function tries to fill the buy order with the order book and returns all the side effects: ```go -// x/ibcdex/types/sell_order_book.go -func (s *SellOrderBook) FillBuyOrder(order Order) (remainingBuyOrder Order, liquidated []Order, purchase int32, filled bool) { +// x/dex/types/buy_order_book.go +func (b *BuyOrderBook) FillSellOrder(order Order) ( + remainingSellOrder Order, + liquidated []Order, + gain int32, + filled bool, +) { var liquidatedList []Order - totalPurchase := int32(0) - remainingBuyOrder = order + totalGain := int32(0) + remainingSellOrder = order + // Liquidate as long as there is match for { var match bool var liquidation Order - remainingBuyOrder, liquidation, purchase, match, filled = s.LiquidateFromBuyOrder( - remainingBuyOrder, + remainingSellOrder, liquidation, gain, match, filled = b.LiquidateFromSellOrder( + remainingSellOrder, ) if !match { break } + // Update gains - totalPurchase += purchase + totalGain += gain + // Update liquidated liquidatedList = append(liquidatedList, liquidation) + if filled { break } } - return remainingBuyOrder, liquidatedList, totalPurchase, filled + + return remainingSellOrder, liquidatedList, totalGain, filled } ``` -#### Implement The LiquidateFromBuyOrder Function +### Implement The LiquidateFromSellOrder Function -`LiquidateFromBuyOrder` liquidates the first sell order of the book from the buy order. If no match is found, return false for match +The `LiquidateFromSellOrder` function liquidates the first sell order of the book from the buy order. If no match is found, return false for match: ```go -// x/ibcdex/types/sell_order_book.go -func (s *SellOrderBook) LiquidateFromBuyOrder(order Order) (remainingBuyOrder Order, liquidatedSellOrder Order, purchase int32, match bool, filled bool) { - remainingBuyOrder = order - // No match if no order - orderCount := len(s.Book.Orders) - if orderCount == 0 { - return order, liquidatedSellOrder, purchase, false, false - } - // Check if match - lowestAsk := s.Book.Orders[orderCount-1] - if order.Price < lowestAsk.Price { - return order, liquidatedSellOrder, purchase, false, false - } - liquidatedSellOrder = *lowestAsk - // Check if buy order can be entirely filled - if lowestAsk.Amount >= order.Amount { - remainingBuyOrder.Amount = 0 - liquidatedSellOrder.Amount = order.Amount - purchase = order.Amount - // Remove lowest ask if it has been entirely liquidated - lowestAsk.Amount -= order.Amount - if lowestAsk.Amount == 0 { - s.Book.Orders = s.Book.Orders[:orderCount-1] - } else { - s.Book.Orders[orderCount-1] = lowestAsk - } - return remainingBuyOrder, liquidatedSellOrder, purchase, true, true - } - // Not entirely filled - purchase = lowestAsk.Amount - s.Book.Orders = s.Book.Orders[:orderCount-1] - remainingBuyOrder.Amount -= lowestAsk.Amount - return remainingBuyOrder, liquidatedSellOrder, purchase, true, false +// x/dex/types/buy_order_book.go +func (b *BuyOrderBook) LiquidateFromSellOrder(order Order) ( + remainingSellOrder Order, + liquidatedBuyOrder Order, + gain int32, + match bool, + filled bool, + ) { + remainingSellOrder = order + + // No match if no order + orderCount := len(b.Book.Orders) + if orderCount == 0 { + return order, liquidatedBuyOrder, gain, false, false + } + + // Check if match + highestBid := b.Book.Orders[orderCount-1] + if order.Price > highestBid.Price { + return order, liquidatedBuyOrder, gain, false, false + } + + liquidatedBuyOrder = *highestBid + + // Check if sell order can be entirely filled + if highestBid.Amount >= order.Amount { + remainingSellOrder.Amount = 0 + liquidatedBuyOrder.Amount = order.Amount + gain = order.Amount * highestBid.Price + + // Remove highest bid if it has been entirely liquidated + highestBid.Amount -= order.Amount + if highestBid.Amount == 0 { + b.Book.Orders = b.Book.Orders[:orderCount-1] + } else { + b.Book.Orders[orderCount-1] = highestBid + } + return remainingSellOrder, liquidatedBuyOrder, gain, true, true + } + + // Not entirely filled + gain = highestBid.Amount * highestBid.Price + b.Book.Orders = b.Book.Orders[:orderCount-1] + remainingSellOrder.Amount -= highestBid.Amount + + return remainingSellOrder, liquidatedBuyOrder, gain, true, false } ``` ## Receiving a Buy Order Acknowledgment -- Chain `Mars` will store the remaining sell order in the sell order book and will distribute sold `MCX` to the buyers and will distribute to the seller the price of the amount sold -- On error we mint back the burned tokens + +After a buy order acknowledgement is received, chain `Mars`: + +* Stores the remaining sell order in the sell order book. +* Distributes sold `marscoin` to the buyers. +* Distributes to the seller the price of the amount sold. +* On error, mints back the burned tokens. ```go -// x/ibcdex/keeper/buy_order.go +// x/dex/keeper/buy_order.go func (k Keeper) OnAcknowledgementBuyOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.BuyOrderPacketData, ack channeltypes.Acknowledgement) error { switch dispatchedAck := ack.Response.(type) { case *channeltypes.Acknowledgement_Error: @@ -280,9 +309,10 @@ func (k Keeper) OnAcknowledgementBuyOrderPacket(ctx sdk.Context, packet channelt ``` `AppendOrder` appends an order in the buy order book. +Add the following function to the `buy_order_book.go` file in the `types` directory. ```go -// x/ibcdex/types/buy_order_book.go +// x/dex/types/buy_order_book.go func (b *BuyOrderBook) AppendOrder(creator string, amount int32, price int32) (int32, error) { return b.Book.appendOrder(creator, amount, price, Increasing) } @@ -290,19 +320,39 @@ func (b *BuyOrderBook) AppendOrder(creator string, amount int32, price int32) (i ## OnTimeout of a Buy Order Packet -If a timeout occurs, we mint back the native token. +If a timeout occurs, mint back the native token: ```go -// x/ibcdex/keeper/buy_order.go -func (k Keeper) OnTimeoutBuyOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.SellOrderPacketData) error { +// x/dex/keeper/buy_order.go +func (k Keeper) OnTimeoutBuyOrderPacket(ctx sdk.Context, packet channeltypes.Packet, data types.BuyOrderPacketData) error { // In case of error we mint back the native token - receiver, err := sdk.AccAddressFromBech32(data.Seller) + receiver, err := sdk.AccAddressFromBech32(data.Buyer) if err != nil { return err } - if err := k.SafeMint(ctx, packet.SourcePort, packet.SourceChannel, receiver, data.AmountDenom, data.Amount); err != nil { + + if err := k.SafeMint( + ctx, + packet.SourcePort, + packet.SourceChannel, + receiver, + data.PriceDenom, + data.Amount*data.Price, + ); err != nil { return err } + return nil } ``` + +## Summary + +Congratulations, you implemented the buy order logic. + +Again, it's a good time to save your current state to your local GitHub repository: + +```bash +git add . +git commit -m "Add Buy Orders" +``` diff --git a/docs/guide/interchange/08-cancelling-orders.md b/docs/guide/interchange/08-cancelling-orders.md index 330dacaee7..b26b18a8f3 100644 --- a/docs/guide/interchange/08-cancelling-orders.md +++ b/docs/guide/interchange/08-cancelling-orders.md @@ -1,17 +1,20 @@ --- order: 8 +description: Enable cancelling of buy and sell orders. --- # Cancelling Orders -You have implemented order books, buy and sell orders. In this chapter you will enable cancelling buy and sell orders. +You have implemented order books, buy and sell orders. In this chapter, you enable cancelling of buy and sell orders. ## Cancel a Sell Order To cancel a sell order, you have to get the ID of the specific sell order. Then you can use the function `RemoveOrderFromID` to remove the specific order from the order book and update the keeper accordingly. +Move to the keeper directory and edit the `msg_server_cancel_sell_order.go` file: + ```go -// x/ibcdex/keeper/msg_server_cancel_sell_order.go +// x/dex/keeper/msg_server_cancel_sell_order.go import "errors" func (k msgServer) CancelSellOrder(goCtx context.Context, msg *types.MsgCancelSellOrder) (*types.MsgCancelSellOrderResponse, error) { @@ -47,10 +50,14 @@ func (k msgServer) CancelSellOrder(goCtx context.Context, msg *types.MsgCancelSe } ``` -`GetOrderFromID` gets an order from the book from its ID. +### Implement the GetOrderFromID Function + +The `GetOrderFromID` function gets an order from the book from its ID. + +Add this function to the `order_book.go` function in the `types` directory: ```go -// x/ibcdex/types/order_book.go +// x/dex/types/order_book.go func (book OrderBook) GetOrderFromID(id int32) (Order, error) { for _, order := range book.Orders { if order.Id == id { @@ -61,10 +68,12 @@ func (book OrderBook) GetOrderFromID(id int32) (Order, error) { } ``` -`RemoveOrderFromID` removes an order from the book and keep it ordered. +### Implement the RemoveOrderFromID Function + +The `RemoveOrderFromID` function removes an order from the book and keeps it ordered: ```go -// x/ibcdex/types/order_book.go +// x/dex/types/order_book.go func (book *OrderBook) RemoveOrderFromID(id int32) error { for i, order := range book.Orders { if order.Id == id { @@ -78,10 +87,10 @@ func (book *OrderBook) RemoveOrderFromID(id int32) error { ## Cancel a Buy Order -To cancel a buy order, you have to get the ID of the specific buy order. Then you can use the function `RemoveOrderFromID` to remove the specific order from the order book and update the keeper accordingly. +To cancel a buy order, you have to get the ID of the specific buy order. Then you can use the function `RemoveOrderFromID` to remove the specific order from the order book and update the keeper accordingly: ```go -// x/ibcdex/keeper/msg_server_cancel_buy_order.go +// x/dex/keeper/msg_server_cancel_buy_order.go import "errors" func (k msgServer) CancelBuyOrder(goCtx context.Context, msg *types.MsgCancelBuyOrder) (*types.MsgCancelBuyOrderResponse, error) { @@ -124,4 +133,21 @@ func (k msgServer) CancelBuyOrder(goCtx context.Context, msg *types.MsgCancelBuy } ``` -That finishes all necessary functions needed for the `ibcdex` module. In this chapter you have implemented the design for cancelling specific buy or sell orders. +## Summary + +You have completed implementing the functions that are required for the `dex` module. In this chapter, you have implemented the design for cancelling specific buy or sell orders. + +To test if your Starport blockchain builds correctly, use the `chain build` command: + +```bash +starport chain build +``` + +Again, it is a good time (a great time!) to add your state to the local GitHub repository: + +```bash +git add . +git commit -m "Add Cancelling Orders" +``` + +Finally, it's now time to write test files. diff --git a/docs/guide/interchange/09-tests.md b/docs/guide/interchange/09-tests.md index 93d59a248d..6cc69b7365 100644 --- a/docs/guide/interchange/09-tests.md +++ b/docs/guide/interchange/09-tests.md @@ -1,19 +1,24 @@ --- order: 9 +description: Add test files. --- -# Write tests +# Write Test Files To test your application, add the test files to your code. -After you add the test files below, change into the `interchange` directory with your terminal, then run +After you add the test files, change into the `interchange` directory with your terminal, then run: ```bash -go test -timeout 30s ./x/ibcdex/types +go test -timeout 30s ./x/dex/types ``` ## Order Book Tests +Create a new `order_book_test.go` file in the `types` directory. + +Add the following testsuite: + ```go // types/order_book_test.go package types_test @@ -25,7 +30,7 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "github.com/tendermint/interchange/x/ibcdex/types" + "github.com/cosmonaut/interchange/x/dex/types" ) func GenString(n int) string { @@ -130,6 +135,8 @@ func TestRemoveOrderFromID(t *testing.T) { ## Buy Order Tests +Create a new `buy_order_book_test.go` file in the `types` directory to add the tests for the Buy Order Book: + ```go // types/buy_order_book_test.go package types_test @@ -139,7 +146,7 @@ import ( "sort" "github.com/stretchr/testify/require" - "github.com/tendermint/interchange/x/ibcdex/types" + "github.com/cosmonaut/interchange/x/dex/types" ) func OrderListToBuyOrderBook(list []types.Order) types.BuyOrderBook { @@ -420,12 +427,14 @@ func TestFillSellOrder(t *testing.T) { ## Sell Order Tests +Create a new testsuite for Sell Orders in a new file `types/sell_order_book_test.go`: + ```go // types/sell_order_book_test.go package types_test import ( - "github.com/tendermint/interchange/x/ibcdex/types" + "github.com/tendermint/interchange/x/dex/types" "testing" "github.com/stretchr/testify/require" "sort" @@ -704,8 +713,10 @@ func TestFillBuyOrder(t *testing.T) { } ``` -When the tests are successful, your output should be +## Successful Test Output + +When the tests are successful, your output is: ```go -ok github.com/username/interchange/x/ibcdex/types 0.550s +ok github.com/cosmonaut/interchange/x/dex/types 0.550s ``` \ No newline at end of file diff --git a/docs/guide/interchange/index.md b/docs/guide/interchange/index.md index 1ff64798d0..352c274f44 100644 --- a/docs/guide/interchange/index.md +++ b/docs/guide/interchange/index.md @@ -1,6 +1,7 @@ --- order: 1 parent: + order: 8 title: "Advanced Module: Interchange" --- @@ -8,11 +9,11 @@ parent: The Interchain Exchange is a module to create buy and sell orders between blockchains. -In this tutorial you will learn how to create a Cosmos SDK module that can create order pairs, buy and sell orders. You will be able to create order books, buy and sell orders across blockchains, which enables to swap tokens from one blockchain to another. +In this tutorial, you learn how to create a Cosmos SDK module that can create order pairs, buy orders, and sell orders. You create order books and buy and sell orders across blockchains, which in turn enables you to swap token from one blockchain to another. -The code in this tutorial is purely written for a tutorial and only for educational purpose. It is not intended to be used in production. +**Note:** The code in this tutorial is written specifically for this tutorial and is intended only for educational purposes. This tutorial code is not intended to be used in production. -If you want to see the end result, please refer to the [example implementation](https://github.com/tendermint/interchange). +If you want to see the end result, see the example implementation in the [interchange repo](https://github.com/tendermint/interchange). **You will learn how to:** @@ -22,14 +23,19 @@ If you want to see the end result, please refer to the [example implementation]( - Send IBC packets from one blockchain to another - Deal with timeouts and acknowledgements of IBC packets -## How the module works +## How the Interchange Exchange Module Works -You will learn how to build an exchange that works with two or more blockchains. The module is called `ibcdex`. +To build an exchange that works with two or more blockchains, follow the steps in this tutorial to create a Cosmos SDK module called `dex`. -The module allows to open an exchange order book between a pair of token from one blockchain and a token on another blockchain. The blockchains are required to have the `ibcdex` module available. +The new `dex` module allows you to open an exchange order book for a pair of token: a from one blockchain and a token on another blockchain. The blockchains are required to have the `dex` module available. -Tokens can be bought or sold with Limit Orders on a simple order book, there is no notion of Liquidity Pool or AMM. +Token can be bought or sold with limit orders on a simple order book. In this tutorial, there is no notion of a liquidity pool or automated market maker (AMM). -The market is unidirectional: the token sold on the source chain cannot be bought back as it is, and the token bought from the target chain cannot be sold back using the same pair. If a token on a source chain is sold, it can only be bought back by creating a new pair on the order book. This is due to the nature of IBC, creating a `voucher` token on the target blockchain. In this tutorial you will learn the difference of a native blockchain token and a `voucher` token that is minted on another blockchain. You will learn how to create a second order book pair in order to receive the native token back. +The market is unidirectional: -In the next chapter you will learn details about the design of the interblockchain exchange. +- The token sold on the source chain cannot be bought back as it is +- The token bought from the target chain cannot be sold back using the same pair. + +If a token on a source chain is sold, it can only be bought back by creating a new pair on the order book. This workflow is due to the nature of the Inter-Blockchain Communication protocol (IBC) which creates a `voucher` token on the target blockchain. There is a difference of a native blockchain token and a `voucher` token that is minted on another blockchain. You must create a second order book pair in order to receive the native token back. + +In the next chapter, you learn details about the design of the interblockchain exchange. diff --git a/docs/guide/loan.md b/docs/guide/loan.md new file mode 100644 index 0000000000..a7144187b9 --- /dev/null +++ b/docs/guide/loan.md @@ -0,0 +1,906 @@ +--- +description: Loan blockchain using Starport +order: 6 +title: "Advanced Module: DeFi Loan" +--- + +# Loan Module + +As a rapidly growing industry in the blockchain ecosystem, (decentralized finance) DeFi is spurring innovation and revolution in spending, sending, locking, and loaning cryptocurrency tokens. + +One of the many goals of blockchain is to make financial instruments available to everyone. A loan in blockchain DeFi can be used in combination with lending, borrowing, spot trading, margin trading, and flash loans. + +With DeFi, end users can quickly and easily access loans without having to submit their passports or background checks like in the traditional banking system. + +In this tutorial, you learn about a basic loan system as you use Starport to build a loan module. + +**You will learn how to** + +* Scaffold a blockchain +* Scaffold a Cosmos SDK loan module +* Scaffold a list for loan objects +* Create messages in the loan module to interact with the loan object +* Interact with other Cosmos SDK modules +* Use an escrow module account +* Add application messages for a loan system + * Request loan + * Approve loan + * Repay loan + * Liquidate loan + * Cancel loan + +**Note:** The code in this tutorial is written specifically for this learning experience and is intended only for educational purposes. This tutorial code is not intended to be used in production. + +## Module Design + +A loan consists of: + +* An `id` +* The `amount` that is being lent +* A `fee` as cost for the loan +* The borrowing party provides a `collateral` to request a loan +* A loan has a `deadline` for repayment, after which the loan can be liquidated +* A loan has a `state` that describes the status as: + + * requested + * approved + * paid + * cancelled + * liquidated + +The two accounts involved in the loan are: + +* `borrower` +* `lender` + +### The Borrower + +A borrower posts a loan request with loan information such as: + +* `amount` +* `fee` +* `collateral` +* `deadline` + +The borrower must repay the loan amount and the loan fee to the lender by the deadline risk losing the collateral. + +### The Lender + +A lender can approve a loan request from a borrower. + +- After the lender approves the loan, the loan amount is transferred to the borrower. +- If the borrower is unable to pay the loan, the lender can liquidate the loan. +- Loan liquidation transfers the collateral and the fees to the lender. + +## Scaffold the Blockchain + +Use Starport to scaffold a fully functional Cosmos SDK blockchain app named `loan`: + +```bash +starport scaffold chain github.com/cosmonaut/loan --no-module +``` + +The `--no-module` flag prevents scaffolding a default module. Don't worry, you will add the loan module later. + +Change into the newly created `loan` directory: + +```bash +cd loan +``` + +## Scaffold the Module + +Scaffold the module to create a new `loan` module. Following the Cosmos SDK convention, all modules are scaffolded inside the `x` directory: + +```bash +starport scaffold module loan --dep bank +``` + +Use the `--dep` flag to specify that this module depends on and is going to interact with the Cosmos SDK `bank` module. + +## Scaffold a List + +Use the [scaffold list](https://docs.starport.com/cli/#starport-scaffold-list) command to scaffold code necessary to store loans in an array-like data structure: + +```bash +starport scaffold list loan amount fee collateral deadline state borrower lender --no-message +``` + +Use the `--no-message` flag to disable CRUD messages in the scaffold. + +The data you store in an array-like data structure are the loans, with these parameters that are defined in the `Loan` message in `proto/loan/loan.proto`: + +```proto +message Loan { + uint64 id = 1; + string amount = 2; + string fee = 3; + string collateral = 4; + string deadline = 5; + string state = 6; + string borrower = 7; + string lender = 8; +} +``` + +Later, you define the messages to interact with the loan list. + +Now it is time to use messages to interact with the loan module. But first, make sure to store your current state in a git commit: + +```bash +git add . +git commit -m "Scaffold loan module and loan list" +``` + +## Scaffold the Messages + +In order to create a loan app, you need the following messages: + +* Request loan +* Approve loan +* Repay loan +* Liquidate loan +* Cancel loan + +You can use the `starport scaffold message` command to create each of the messages. + +You define the details of each message when you scaffold them. + +Create the messages one at a time with the according application logic. + +### Request Loan Message + +For a loan, the initial message handles the transaction when a cosmonaut requests a loan. + +The cosmonaut wants a certain `amount` and is willing to pay `fees` as well as give `collateral`. The `deadline` marks the time when the loan has to be repaid. + +The first message is the `request-loan` message that requires these input parameters: + +* `amount` +* `fee` +* `collateral` +* `deadline` + +```bash +starport scaffold message request-loan amount fee collateral deadline +``` + +For the sake of simplicity, define every parameter as a string. + +The `request-loan` message creates a new loan object and locks the tokens to be spent as fee and collateral into an escrow account. Describe these conditions in the module keeper `x/loan/keeper/msg_server_request_loan.go`: + +```go +package keeper + +import ( + "context" + + "github.com/cosmonaut/loan/x/loan/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (k msgServer) RequestLoan(goCtx context.Context, msg *types.MsgRequestLoan) (*types.MsgRequestLoanResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + // Create a new Loan with the following user input + var loan = types.Loan{ + Amount: msg.Amount, + Fee: msg.Fee, + Collateral: msg.Collateral, + Deadline: msg.Deadline, + State: "requested", + Borrower: msg.Creator, + } + + // TODO: collateral has to be more than the amount (+fee?) + + // moduleAcc := sdk.AccAddress(crypto.AddressHash([]byte(types.ModuleName))) + // Get the borrower address + borrower, _ := sdk.AccAddressFromBech32(msg.Creator) + + // Get the collateral as sdk.Coins + collateral, err := sdk.ParseCoinsNormalized(loan.Collateral) + if err != nil { + panic(err) + } + + // Use the module account as escrow account + sdkError := k.bankKeeper.SendCoinsFromAccountToModule(ctx, borrower, types.ModuleName, collateral) + if sdkError != nil { + return nil, sdkError + } + + // Add the loan to the keeper + k.AppendLoan( + ctx, + loan, + ) + + return &types.MsgRequestLoanResponse{}, nil +} +``` + +Since this function is using the `bankKeeper` with the function `SendCoinsFromAccountToModule`, you must add the `SendCoinsFromAccountToModule` function to `x/loan/types/expected_keepers.go` like this: + +```go +package types + +import sdk "github.com/cosmos/cosmos-sdk/types" + +type BankKeeper interface { + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error +} +``` + +When a loan is created, a certain input validation is required. You want to throw error messages in case the end user tries impossible inputs. + +You can describe message validation errors in the modules `types` directory. + +Add the following code to the `ValidateBasic()` function in the `/x/loan/types/message_request_loan.go` file: + +```go +func (msg *MsgRequestLoan) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(msg.Creator) + + amount, err := sdk.ParseCoinsNormalized(msg.Amount) + fee, _ := sdk.ParseCoinsNormalized(msg.Fee) + collateral, _ := sdk.ParseCoinsNormalized(msg.Collateral) + + if err != nil { + return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid creator address (%s)", err) + } + if !amount.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "amount is not a valid Coins object") + } + if amount.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "amount is empty") + } + if !fee.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "fee is not a valid Coins object") + } + if !collateral.IsValid() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "collateral is not a valid Coins object") + } + if collateral.Empty() { + return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "collateral is empty") + } + return nil +} +``` + +Congratulations, you have created the `request-loan` message. + +You can run the chain and test your first message. + +Start the blockchain: + +```bash +starport chain serve +``` + +Add your first loan: + +```bash +loand tx loan request-loan 100token 2token 200token 500 --from alice +``` + +Query your loan: + +```bash +loand query loan list-loan +``` + +The loan shows in the list: + +```bash +Loan: +- amount: 100token + borrower: cosmos17mnrhwchwc8trg4w09s0gvvfsvt58ejtsykkm6 + collateral: 200token + deadline: "500" + fee: 2token + id: "0" + lender: "" + state: requested +``` + +You can stop the blockchain again with CTRL+C. + +This is a good time to add your advancements to git: + +```bash +git add . +git commit -m "Add request-loan message" +``` + +### Approve Loan Message + +After a loan request has been published, another account can approve the loan and agree to the terms of the borrower. + +The message `approve-loan` has one parameter, the `id`. +Specify the type of `id` as `uint`. By default, ids are stored as `uint`. + +```bash +starport scaffold message approve-loan id:uint +``` + +This message must be available for all loan types that are in `"requested"` status. + +The loan approval sends the requested coins for the loan to the borrower and sets the loan state to `"approved"`. + +Modify the `x/loan/keeper/msg_server_approve_loan.go` to implement this logic: + +```go +package keeper + +import ( + "context" + "fmt" + + "github.com/cosmonaut/loan/x/loan/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func (k msgServer) ApproveLoan(goCtx context.Context, msg *types.MsgApproveLoan) (*types.MsgApproveLoanResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + loan, found := k.GetLoan(ctx, msg.Id) + if !found { + return nil, sdkerrors.Wrapf(sdkerrors.ErrKeyNotFound, "key %d doesn't exist", msg.Id) + } + + // TODO: for some reason the error doesn't get printed to the terminal + if loan.State != "requested" { + return nil, sdkerrors.Wrapf(types.ErrWrongLoanState, "%v", loan.State) + } + + lender, _ := sdk.AccAddressFromBech32(msg.Creator) + borrower, _ := sdk.AccAddressFromBech32(loan.Borrower) + amount, err := sdk.ParseCoinsNormalized(loan.Amount) + if err != nil { + return nil, sdkerrors.Wrap(types.ErrWrongLoanState, "Cannot parse coins in loan amount") + } + + k.bankKeeper.SendCoins(ctx, lender, borrower, amount) + + loan.Lender = msg.Creator + loan.State = "approved" + + k.SetLoan(ctx, loan) + + return &types.MsgApproveLoanResponse{}, nil +} +``` + +This module uses the `SendCoins` function of `bankKeeper`. Add this `SendCoins` function to the `x/loan/types/expected_keepers.go` file: + +```go +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type BankKeeper interface { + // Methods imported from bank should be defined here + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error +} +``` + +Now, define the `ErrWrongLoanState` new error type by adding it to the errors definitions in `x/loan/types/errors.go`: + +```go +package types + +// DONTCOVER + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// x/loan module sentinel errors +var ( + ErrWrongLoanState = sdkerrors.Register(ModuleName, 1, "wrong loan state") +) +``` + +Start the blockchain and use the two commands you already have available: + +```bash +starport chain serve -r +``` + +Use the `-r` flag to reset the blockchain state and start with a new database. + +Now, request a loan from `bob`: + +```bash +loand tx loan request-loan 100token 2token 200token 500 --from bob -y +``` + +Query your loan request: + +```bash +loand query loan list-loan +``` + +Approve the loan: + +```bash +loand tx loan approve-loan 0 --from alice -y +``` + +This approve loan transaction sends the balances according to the loan request. + +Check for the loan list again to verify that the loan state is now `approved`. + +```bash +Loan: +- amount: 100token + borrower: cosmos1sx8k358xw5pulv7acjhm6klvn3tukk2r2a74gg + collateral: 200token + deadline: "500" + fee: 2token + id: "0" + lender: cosmos1qxm2dtupmr8pp20m0t7tmjq6gq2z8j3d6ltr9d + state: approved +pagination: + next_key: null + total: "0" +``` + +You can query for alice's balance to see the loan in effect. Take the lender address from above, this is alice address: + +```bash +loand query bank balances +``` + +In case everything works as expected, this is a good time to save the state with a git commit: + +```bash +git add . +git commit -m "Add approve loan message" +``` + +### Repay Loan Message + +After the loan has been approved, the cosmonaut must be able to repay an approved loan. + +Scaffold the message `repay-loan` that a borrower uses to return tokens that were borrowed from the lender: + +```bash +starport scaffold message repay-loan id:uint +``` + +Repaying a loan requires that the loan is in `"approved"` status. + +The coins as described in the loan are collected and sent from the borrower to the lender, along with the agreed fees. + +The `collateral` is released from the escrow module account. + +Only the `borrower` can repay the loan. + +This loan repayment logic is defined in `x/loan/keeper/msg_server_repay_loan.go`: + +```go +package keeper + +import ( + "context" + "fmt" + + "github.com/cosmonaut/loan/x/loan/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func (k msgServer) RepayLoan(goCtx context.Context, msg *types.MsgRepayLoan) (*types.MsgRepayLoanResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + loan, found := k.GetLoan(ctx, msg.Id) + if !found { + return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, fmt.Sprintf("key %d doesn't exist", msg.Id)) + } + + if loan.State != "approved" { + return nil, sdkerrors.Wrapf(types.ErrWrongLoanState, "%v", loan.State) + } + + lender, _ := sdk.AccAddressFromBech32(loan.Lender) + borrower, _ := sdk.AccAddressFromBech32(loan.Borrower) + + if msg.Creator != loan.Borrower { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Cannot repay: not the borrower") + } + + amount, _ := sdk.ParseCoinsNormalized(loan.Amount) + fee, _ := sdk.ParseCoinsNormalized(loan.Fee) + collateral, _ := sdk.ParseCoinsNormalized(loan.Collateral) + + err = k.bankKeeper.SendCoins(ctx, borrower, lender, amount) + if err != nil { + return nil, sdkerrors.Wrap(types.ErrWrongLoanState, "Cannot send coins") + } + err = k.bankKeeper.SendCoins(ctx, borrower, lender, fee) + if err != nil { + return nil, sdkerrors.Wrap(types.ErrWrongLoanState, "Cannot send coins") + } + err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, borrower, collateral) + if err != nil { + return nil, sdkerrors.Wrap(types.ErrWrongLoanState, "Cannot send coins") + } + + loan.State = "repayed" + + k.SetLoan(ctx, loan) + + return &types.MsgRepayLoanResponse{}, nil +} +``` + +After the coins have been successfully exchanged, the state of the loan is set to `repayed`. + +To release tokens with the `SendCoinsFromModuleToAccount` function of `bankKeepers`, you need to add the `SendCoinsFromModuleToAccount` function to the `x/loan/types/expected_keepers.go`: + +```go +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type BankKeeper interface { + // Methods imported from bank should be defined here + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error +} +``` + +Start the blockchain and use the two commands you already have available: + +```bash +starport chain serve -r +``` + +Use the `-r` flag to reset the blockchain state and start with a new database: + +```bash +loand tx loan request-loan 100token 2token 200token 500 --from bob -y +``` + +Query your loan request: + +```bash +loand query loan list-loan +``` + +Approve the loan: + +```bash +loand tx loan approve-loan 0 --from alice -y +``` + +You can query for alice's balance to see the loan in effect. + +Take the lender address from above, this is alice address: + +```bash +loand query bank balances +``` + +Now repay the loan: + +```bash +loand tx loan repay-loan 0 --from bob -y +``` + +The loan status is now `repayed`: + +```bash +Loan: +- amount: 100token + borrower: cosmos1200nsqsxcyxtllfgal5x8qhqwj8km64ft0eu2d + collateral: 200token + deadline: "500" + fee: 2token + id: "0" + lender: cosmos194pn6vly2nlald3zjqcxfnvasa0xt7ect6h6qk + state: repayed +``` + +The alice balance reflects the repayed amount plus fees: + +```bash +loand query bank balances +``` + +Good job! + +Update your git with the changes you made: + +```bash +git add . +git commit -m "Add repay-loan message" +``` + +### Liquidate Loan Message + +After the deadline is passed, a lender can liquidate a loan when the borrower does not repay the tokens. The message to `liquidate-loan` refers to the loan `id`: + +```bash +starport scaffold message liquidate-loan id:uint +``` + +* The `liquidate-loan` message must be able to be executed by the `lender`. +* The status of the loan must be `approved`. +* The `deadline` block height must have passed. + +When these properties are valid, the collateral shall be liquidated from the `borrower`. + +Add this liquidate loan logic to the `keeper` in `x/loan/keeper/msg_server_liquidate_loan.go`: + +```go +package keeper + +import ( + "context" + "fmt" + "strconv" + + "github.com/cosmonaut/loan/x/loan/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func (k msgServer) LiquidateLoan(goCtx context.Context, msg *types.MsgLiquidateLoan) (*types.MsgLiquidateLoanResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + loan, found := k.GetLoan(ctx, msg.Id) + if !found { + return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, fmt.Sprintf("key %d doesn't exist", msg.Id)) + } + + if loan.Lender != msg.Creator { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Cannot liquidate: not the lender") + } + + if loan.State != "approved" { + return nil, sdkerrors.Wrapf(types.ErrWrongLoanState, "%v", loan.State) + } + + lender, _ := sdk.AccAddressFromBech32(loan.Lender) + collateral, _ := sdk.ParseCoinsNormalized(loan.Collateral) + + deadline, err := strconv.ParseInt(loan.Deadline, 10, 64) + if err != nil { + panic(err) + } + + if ctx.BlockHeight() < deadline { + return nil, sdkerrors.Wrap(types.ErrDeadline, "Cannot liquidate before deadline") + } + + k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, lender, collateral) + + loan.State = "liquidated" + + k.SetLoan(ctx, loan) + + return &types.MsgLiquidateLoanResponse{}, nil +} +``` + +Add the new error `ErrDeadline` to the error messages in `x/loan/types/errors.go`: + +```go +package types + +// DONTCOVER + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// x/loan module sentinel errors +var ( + ErrWrongLoanState = sdkerrors.Register(ModuleName, 1, "wrong loan state") + ErrDeadline = sdkerrors.Register(ModuleName, 2, "deadline") +) +``` + +These changes are required for the `liquidate-loan` message. + +You can test the liquidation message now. Start your chain and reset the state of the app: + +```bash +starport chain serve -r +``` + +Set the deadline for the loan request to 1 block: + +```bash +loand tx loan request-loan 100token 2token 200token 1 --from bob -y +``` + +Query your loan request: + +```bash +loand query loan list-loan +``` + +Approve the loan: + +```bash +loand tx loan approve-loan 0 --from alice -y +``` + +You can query for alice's balances to see the loan in effect. + +Take the lender address from above, this is alice address. + +```bash +loand query bank balances +``` + +Now, liquidate the loan: + +```bash +loand tx loan liquidate-loan 0 --from alice -y +``` + +Query the loan: + +```bash +loand query loan list-loan +``` + +The loan status is now `liquidated`: + +```bash +Loan: +- amount: 100token + borrower: cosmos1lp4ghp4mmsdgpf2fm22f0qtqmnjeh3gr9h3cau + collateral: 200token + deadline: "1" + fee: 2token + id: "0" + lender: cosmos1w6pfj52jp809pyp2a2h573cta23rc0zsulpafm + state: liquidated +``` + +And alice balance reflects the repayed amount plus fees: + +```bash +loand query bank balances +``` + +Add the changes to your local repository: + +```bash +git add . +git commit -m "Add liquidate-loan message" +``` + +### Cancel Loan Message + +After a loan request has been made and not been approved, the `borrower` must be able to cancel a loan request. + +Scaffold the message for `cancel-loan`: + +```bash +starport s message cancel-loan id:uint +``` + +* Only the `borrower` can cancel a loan request. +* The state of the request must be `requested`. +* Then the collateral coins can be released from escrow and the status set to `cancelled`. + +Add this functionality to the `keeper` in `x/loan/keeper/msg_server_cancel_loan.go`: + +```go +package keeper + +import ( + "context" + "fmt" + + "github.com/cosmonaut/loan/x/loan/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +func (k msgServer) CancelLoan(goCtx context.Context, msg *types.MsgCancelLoan) (*types.MsgCancelLoanResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + loan, found := k.GetLoan(ctx, msg.Id) + if !found { + return nil, sdkerrors.Wrap(sdkerrors.ErrKeyNotFound, fmt.Sprintf("key %d doesn't exist", msg.Id)) + } + + if loan.Borrower != msg.Creator { + return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Cannot cancel: not the borrower") + } + + if loan.State != "requested" { + return nil, sdkerrors.Wrapf(types.ErrWrongLoanState, "%v", loan.State) + } + + borrower, _ := sdk.AccAddressFromBech32(loan.Borrower) + collateral, _ := sdk.ParseCoinsNormalized(loan.Collateral) + k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, borrower, collateral) + + loan.State = "cancelled" + + k.SetLoan(ctx, loan) + + return &types.MsgCancelLoanResponse{}, nil +} +``` + +Test the changes for cancelling a loan request: + +```bash +starport chain serve -r +``` + +```bash +loand tx loan request-loan 100token 2token 200token 100 --from bob -y +``` + +Query your loan request: + +```bash +loand query loan list-loan +``` + +```bash +loand tx loan cancel-loan 0 --from bob -y +``` + +Query your loan request: + +```bash +loand query loan list-loan +``` + +Now the collateral coins can be released from escrow and the status set to `cancelled`. + +```bash +- amount: 100token + borrower: cosmos1lp4ghp4mmsdgpf2fm22f0qtqmnjeh3gr9h3cau + collateral: 200token + deadline: "100" + fee: 2token + id: "2" + lender: "" + state: cancelled +``` + +Consider again updating your local repository with a git commit. After you test and use your loan module, consider publishing your code to a public repository for others to see your accomplishments. + +```bash +git add . +git commit -m "Add cancel-loan message" +``` + +## Complete + +Congratulations. You have completed the loan module tutorial. + +You executed commands and updated files to: + +* Scaffold a blockchain +* Scaffold a module +* Scaffold a list for loan objects +* Create messages in your module to interact with the loan object +* Interact with other modules in your module +* Use an escrow module account +* Add application messages for a loan system + * Request Loan + * Approve Loan + * Repay Loan + * Liquidate Loan + * Cancel Loan diff --git a/docs/guide/nameservice/01-init.md b/docs/guide/nameservice/01-init.md index d52ba67865..f7c4318d04 100644 --- a/docs/guide/nameservice/01-init.md +++ b/docs/guide/nameservice/01-init.md @@ -9,7 +9,7 @@ Scaffold a blockchain and create a `nameservice` module for the nameservice app. ## Create a Blockchain -Scaffold a new Cosmos SDK blockchain using the `starport scaffold chain` command. The [starport scaffold chain](https://docs.starport.network/cli/#starport-scaffold-chain) command accepts one argument: the Go module path that is used for the project. +Scaffold a new Cosmos SDK blockchain using the `starport scaffold chain` command. The [starport scaffold chain](https://docs.starport.com/cli/#starport-scaffold-chain) command accepts one argument: the Go module path that is used for the project. By default, a chain is scaffolded with a new empty Cosmos SDK module. You want to create the nameservice module without scaffolding a module, so use the `--no-module` flag: diff --git a/docs/guide/nameservice/02-messages.md b/docs/guide/nameservice/02-messages.md index 60c8b1dba9..d08341d0b2 100644 --- a/docs/guide/nameservice/02-messages.md +++ b/docs/guide/nameservice/02-messages.md @@ -70,7 +70,7 @@ Now, you are ready to implement these Cosmos SDK messages to achieve the desired Use the `starport scaffold message` command to scaffold new messages for your module. -- The [`starport scaffold message`](https://docs.starport.network/cli/#starport-scaffold-message) command accepts the message name as the first argument and a list of fields for the message. +- The [`starport scaffold message`](https://docs.starport.com/cli/#starport-scaffold-message) command accepts the message name as the first argument and a list of fields for the message. - By default, a message is scaffolded in a module with a name that matches the name of the project, in this case `nameservice`. ### Add the MsgBuyName Message @@ -171,7 +171,7 @@ where: This `starport scaffold message` command modifies and creates the same set of files as the `MsgBuyName` message. -### Add The MsgDelete Message +### Add The MsgDeleteName Message You need a message so that an end user can delete a name that belongs to them. diff --git a/docs/guide/nameservice/03-types.md b/docs/guide/nameservice/03-types.md index 953eb6a283..81334797a7 100644 --- a/docs/guide/nameservice/03-types.md +++ b/docs/guide/nameservice/03-types.md @@ -11,7 +11,7 @@ Now that you've defined messages that trigger state transitions, it's time to im For the nameservice blockchain, define a `whois` type and the create and delete methods. -Because Starport does the heavy lifting for you, choose from several [starport scaffold](https://docs.starport.network/cli/#starport-scaffold) commands to create CRUD functionality code for data stored in different ways: +Because Starport does the heavy lifting for you, choose from several [starport scaffold](https://docs.starport.com/cli/#starport-scaffold) commands to create CRUD functionality code for data stored in different ways: - Array, a list-like data structure - Map (key-value pairs) @@ -24,7 +24,7 @@ Use the `starport scaffold map` command to scaffold the `whois` type and create In this example, the `whois` type is stored in a map-like data structure: ```bash -starport scaffold map whois name value price --no-message +starport scaffold map whois name value price owner --no-message ``` where: @@ -48,7 +48,7 @@ The `starport scaffold map whois name value price --no-message` command created * Queries to get data from the blockchain. * Define queries as proto messages. * Register the messages in the `Query` service. - * + * `proto/nameservice/genesis.proto` A type for exporting the state of the blockchain, for example, during software upgrades. diff --git a/docs/guide/nameservice/04-keeper.md b/docs/guide/nameservice/04-keeper.md index 6cf8f845e3..8e827537b7 100644 --- a/docs/guide/nameservice/04-keeper.md +++ b/docs/guide/nameservice/04-keeper.md @@ -40,7 +40,7 @@ func (k msgServer) BuyName(goCtx context.Context, msg *types.MsgBuyName) (*types price, _ := sdk.ParseCoinsNormalized(whois.Price) bid, _ := sdk.ParseCoinsNormalized(msg.Bid) // Convert owner and buyer address strings to sdk.AccAddress - owner, _ := sdk.AccAddressFromBech32(whois.Creator) + owner, _ := sdk.AccAddressFromBech32(whois.Owner) buyer, _ := sdk.AccAddressFromBech32(msg.Creator) // If a name is found in store if isFound { @@ -57,8 +57,8 @@ func (k msgServer) BuyName(goCtx context.Context, msg *types.MsgBuyName) (*types // Throw an error return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "Bid is less than min amount") } - // Otherwise (when the bid is higher), burn tokens from the buyer's account (as a payment for the name) - k.bankKeeper.SubtractCoins(ctx, buyer, bid) + // Otherwise (when the bid is higher), send tokens from the buyer's account to the module's account (as a payment for the name) + k.bankKeeper.SendCoinsFromAccountToModule(ctx, buyer, types.ModuleName, bid) } // Create an updated whois record newWhois := types.Whois{ @@ -66,7 +66,7 @@ func (k msgServer) BuyName(goCtx context.Context, msg *types.MsgBuyName) (*types Name: msg.Name, Value: whois.Value, Price: bid.String(), - Creator: buyer.String(), + Owner: buyer.String(), } // Write whois information to the store k.SetWhois(ctx, newWhois) @@ -89,7 +89,7 @@ import ( ) type BankKeeper interface { - SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error } ``` @@ -112,7 +112,7 @@ func (k msgServer) SetName(goCtx context.Context, msg *types.MsgSetName) (*types // Try getting name information from the store whois, _ := k.GetWhois(ctx, msg.Name) // If the message sender address doesn't match the name owner, throw an error - if !(msg.Creator == whois.Creator) { + if !(msg.Creator == whois.Owner) { return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Incorrect Owner") } // Otherwise, create an updated whois record @@ -120,7 +120,7 @@ func (k msgServer) SetName(goCtx context.Context, msg *types.MsgSetName) (*types Index: msg.Name, Name: msg.Name, Value: msg.Value, - Creator: whois.Creator, + Owner: whois.Owner, Price: whois.Price, } // Write whois information to the store @@ -131,7 +131,7 @@ func (k msgServer) SetName(goCtx context.Context, msg *types.MsgSetName) (*types ## Delete Name -To define the keeper for the delete name transaction, add this code to the `msg_server_buy_name.go` file: +To define the keeper for the delete name transaction, add this code to the `msg_server_delete_name.go` file: ```go // x/nameservice/keeper/msg_server_delete_name.go @@ -151,7 +151,7 @@ func (k msgServer) DeleteName(goCtx context.Context, msg *types.MsgDeleteName) ( return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "Name doesn't exist") } // If the message sender address doesn't match the name owner, throw an error - if !(whois.Creator == msg.Creator) { + if !(whois.Owner == msg.Creator) { return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Incorrect Owner") } // Otherwise, remove the name information from the store diff --git a/docs/guide/nameservice/index.md b/docs/guide/nameservice/index.md index bb088b0f3f..5718a54de1 100644 --- a/docs/guide/nameservice/index.md +++ b/docs/guide/nameservice/index.md @@ -10,7 +10,7 @@ parent: The nameservice tutorial provides step-by-step instructions to build a blockchain app for a nameservice. The goal of the nameservice app is to send tokens between participants so that end users can buy names and set a value to the names. -This tutorial builds on knowlege and skills developed in the earlier tutorials in the Starport Developer Guide. Before you start this building your nameservice app, we recommend that you complete these foundational tutorials: +This tutorial builds on knowlege and skills developed in the earlier tutorials in the Starport Developer Tutorials. Before you start this building your nameservice app, we recommend that you complete these foundational tutorials: - [Install Starport](../install.md) - [Hello, World](../hello.md) @@ -28,7 +28,7 @@ This tutorial guides you through these steps to build a blockchain for a nameser ## Prerequisites -- A supported version of [Starport](https://docs.starport.network/). To install Starport, see [Install Starport](../starport/install.md). +- A supported version of [Starport](https://docs.starport.com/). To install Starport, see [Install Starport](../starport/install.md). * A text editor like [Visual Studio Code](https://code.visualstudio.com/download) or [Atom](https://atom.io/). * A web browser like [Chrome](https://www.google.com/chrome/) or [Firefox](https://www.mozilla.org/en-US/firefox/new/). - Familiarity with [Cosmos SDK modules](https://docs.cosmos.network/master/building-modules/intro.html) @@ -43,7 +43,7 @@ First, see how these simple requirements translate to app design. A blockchain app is a [replicated deterministic state machine](https://en.wikipedia.org/wiki/State_machine_replication). As a blockchain app developer, you have to define the state machine with a starting state and messages that trigger state transitions. These software components make it all possible! -- [Starport](https://docs.starport.network/) is built on top of Cosmos SDK and accelerates chain development by scaffolding everything you need. +- [Starport](https://docs.starport.com/) is built on top of Cosmos SDK and accelerates chain development by scaffolding everything you need. - The [Cosmos SDK](https://github.com/cosmos/cosmos-sdk/) modular framework allows developers like you to create custom blockchains that can natively interact with other blockchains. - [Tendermint](https://docs.tendermint.com/master/introduction/what-is-tendermint.html) software securely and consistently replicates an app on many machines. The Tendermint app-agnostic engine handles the networking and consensus layers of your blockchain. diff --git a/docs/guide/scavenge/07-keeper.md b/docs/guide/scavenge/07-keeper.md index a27c3d4796..d9c6052e2d 100644 --- a/docs/guide/scavenge/07-keeper.md +++ b/docs/guide/scavenge/07-keeper.md @@ -30,7 +30,7 @@ func (k msgServer) SubmitScavenge(goCtx context.Context, msg *types.MsgSubmitSca // create a new scavenge from the data in the MsgSubmitScavenge message var scavenge = types.Scavenge{ Index: msg.SolutionHash, - Creator: msg.Creator, + Scavenger: msg.Creator, Description: msg.Description, SolutionHash: msg.SolutionHash, Reward: msg.Reward, @@ -44,7 +44,7 @@ func (k msgServer) SubmitScavenge(goCtx context.Context, msg *types.MsgSubmitSca // get address of the Scavenge module account moduleAcct := sdk.AccAddress(crypto.AddressHash([]byte(types.ModuleName))) // convert the message creator address from a string into sdk.AccAddress - scavenger, err := sdk.AccAddressFromBech32(scavenge.Creator) + scavenger, err := sdk.AccAddressFromBech32(scavenge.Scavenger) if err != nil { panic(err) } @@ -102,7 +102,6 @@ func (k msgServer) CommitSolution(goCtx context.Context, msg *types.MsgCommitSol // create a new commit from the information in the MsgCommitSolution message var commit = types.Commit{ Index: msg.SolutionScavengerHash, - Creator: msg.Creator, SolutionHash: msg.SolutionHash, SolutionScavengerHash: msg.SolutionScavengerHash, } diff --git a/docs/kb/band.md b/docs/kb/band.md index 931c9364d5..05d59bbec9 100644 --- a/docs/kb/band.md +++ b/docs/kb/band.md @@ -34,28 +34,32 @@ When you scaffold a BandChain oracle, the following files and directories are cr ## BandChain Oracle Scaffold Example -The following command scaffolds the IBC-enabled oracle. by default, the starport scaffold oracle for [coin rates](https://laozi-testnet2.cosmoscan.io/oracle-script/37#bridge) request and result. +The following command scaffolds the IBC-enabled oracle. by default, the starport scaffold oracle for [coin rates](https://laozi-testnet4.cosmoscan.io/oracle-script/37#bridge) request and result. ```shell -$ starport scaffold chain github.com/test/ibcoracle && cd ibcoracle +$ starport scaffold chain github.com/cosmonaut/oracle --no-module && cd oracle $ starport scaffold module consuming --ibc -$ starport s band coinRates --module consuming +$ starport scaffold band coinRates --module consuming ``` Note: BandChain module uses version "bandchain-1". Make sure to update the `keys.go` file accordingly. -`x/ibcoracle/types/keys.go` +`x/oracle/types/keys.go` ```go const Version = "bandchain-1" ``` -After scaffold and change the data, configure and run the starport relayer. +After scaffold and change the data, run the chain: +```shell +$ starport chain serve +``` +In another tab, configure and run the starport relayer. ```shell $ starport relayer configure -a \ ---source-rpc "http://rpc-laozi-testnet2.bandchain.org:26657" \ ---source-faucet "https://laozi-testnet2.bandchain.org/faucet/request" \ +--source-rpc "http://rpc-laozi-testnet4.bandchain.org:80" \ +--source-faucet "https://laozi-testnet4.bandchain.org/faucet" \ --source-port "oracle" \ --source-gasprice "0uband" \ --source-gaslimit 5000000 \ @@ -72,37 +76,37 @@ $ starport relayer configure -a \ $ starport relayer connect ``` -Make a request transaction, passing the script id. +Open one more terminal tab to make a request transaction, passing the script id. ```shell # Coin Rates (script 37 into the testnet) -$ ibcoracled tx consuming coin-rates-data 37 4 3 --channel channel-0 --symbols "BTC,ETH,XRP,BCH" --multiplier 1000000 --fee-limit 30uband --request-key "random_string" --prepare-gas 600000 --execute-gas 600000 --from alice --chain-id ibcoracle +$ oracled tx consuming coin-rates-data 37 4 3 --channel channel-0 --symbols "BTC,ETH,XRP,BCH" --multiplier 1000000 --fee-limit 30uband --prepare-gas 600000 --execute-gas 600000 --from alice --chain-id oracle ``` You can check the last request id returned by ack. ```shell -$ ibcoracled query consuming last-coin-rates-id +$ oracled query consuming last-coin-rates-id request_id: "101276" ``` Furthermore, check the data by request id receive the data packet. ```shell -$ ibcoracled query consuming coin-rates-result 101276 +$ oracled query consuming coin-rates-result 101276 ``` ### Multiple oracles -You can scaffold multiples oracles by module. After scaffold, you must change the `Calldata` and `Result` parameters into the proto file `moduleName.proto` and adapt the request into the `cli/client/tx_module_name.go` file. Let's create an example to return the [gold price](https://laozi-testnet2.cosmoscan.io/oracle-script/33#bridge): +You can scaffold multiples oracles by module. After scaffold, you must change the `Calldata` and `Result` parameters into the proto file `moduleName.proto` and adapt the request into the `cli/client/tx_module_name.go` file. Let's create an example to return the [gold price](https://laozi-testnet4.cosmoscan.io/oracle-script/33#bridge): ```shell -$ starport s band goldPrice --module consuming +$ starport scaffold band goldPrice --module consuming ``` -`proto/gold_price.proto`: +`proto/consuming/gold_price.proto`: ```protobuf syntax = "proto3"; -package test.ibcoracle.consuming; +package cosmonaut.oracle.consuming; -option go_package = "github.com/test/ibcoracle/x/consuming/types"; +option go_package = "github.com/cosmonaut/oracle/x/consuming/types"; message GoldPriceCallData { uint64 multiplier = 2; @@ -113,7 +117,7 @@ message GoldPriceResult { } ``` -`x/cli/client/tx_gold_price.go`: +`x/consuming/cli/client/tx_gold_price.go`: ```go package cli @@ -122,11 +126,11 @@ import ( "github.com/spf13/cobra" + "github.com/cosmonaut/oracle/x/consuming/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/tendermint/test/x/consuming/types" ) // CmdRequestGoldPriceData creates and broadcast a GoldPrice request transaction @@ -180,12 +184,6 @@ func CmdRequestGoldPriceData() *cobra.Command { return err } - // retrieve the request key corresponding to the pool account (used to pay fee) on BandChain. - requestKey, err := cmd.Flags().GetString(flagRequestkey) - if err != nil { - return err - } - // retrieve the amount of gas allowed for the prepare step of the oracle script. prepareGas, err := cmd.Flags().GetUint64(flagPrepareGas) if err != nil { @@ -211,7 +209,6 @@ func CmdRequestGoldPriceData() *cobra.Command { askCount, minCount, feeLimit, - requestKey, prepareGas, executeGas, ) @@ -226,7 +223,6 @@ func CmdRequestGoldPriceData() *cobra.Command { cmd.MarkFlagRequired(flagChannel) cmd.Flags().Uint64(flagMultiplier, 1000000, "Multiplier used in calling the oracle script") cmd.Flags().String(flagFeeLimit, "", "the maximum tokens that will be paid to all data source providers") - cmd.Flags().String(flagRequestkey, "", "Key for generating escrow address") cmd.Flags().Uint64(flagPrepareGas, 200000, "Prepare gas used in fee counting for prepare request") cmd.Flags().Uint64(flagExecuteGas, 200000, "Execute gas used in fee counting for execute request") flags.AddTxFlagsToCmd(cmd) @@ -239,14 +235,14 @@ Make the request transaction. ```shell # Gold Price (script 33 into the testnet) -$ ibcoracled tx consuming gold-price-data 33 4 3 --channel channel-0 --multiplier 1000000 --fee-limit 30uband --request-key "random_string" --prepare-gas 600000 --execute-gas 600000 --from alice --chain-id ibcoracle +$ oracled tx consuming gold-price-data 33 4 3 --channel channel-0 --multiplier 1000000 --fee-limit 30uband --prepare-gas 600000 --execute-gas 600000 --from alice --chain-id oracle ``` Check the last request id returned by ack and the package data. ```shell -$ ibcoracled query consuming last-gold-price-id +$ oracled query consuming last-gold-price-id request_id: "101290" -$ ibcoracled query consuming gold-price-result 101290 +$ oracled query consuming gold-price-result 101290 ``` diff --git a/docs/kb/config.md b/docs/kb/config.md index d9f1af21a6..d5accebd64 100644 --- a/docs/kb/config.md +++ b/docs/kb/config.md @@ -167,4 +167,4 @@ host: ## `genesis` -Use to overwrite values in `genesis.json` in the data directory to test different values in development environments. See [Genesis Overwrites for Development](https://docs.starport.network/kb/genesis.html). +Use to overwrite values in `genesis.json` in the data directory to test different values in development environments. See [Genesis Overwrites for Development](../kb/genesis.md). diff --git a/docs/kb/cosmwasm.md b/docs/kb/cosmwasm.md index 8de45e6a64..d8f4da114f 100644 --- a/docs/kb/cosmwasm.md +++ b/docs/kb/cosmwasm.md @@ -1,6 +1,6 @@ --- description: Import module to add support for CosmWasm. -order: 11 +order: false --- # Add Support for CosmWasm diff --git a/docs/kb/docker.md b/docs/kb/docker.md index 3d29840bcd..ce6ab7098b 100644 --- a/docs/kb/docker.md +++ b/docs/kb/docker.md @@ -1,6 +1,6 @@ --- description: Run Starport CLI using a Docker container. -order: 9 +order: 10 --- # Run Starport in Docker diff --git a/docs/kb/frontend.md b/docs/kb/frontend.md index 690859c127..18ab66cbcf 100644 --- a/docs/kb/frontend.md +++ b/docs/kb/frontend.md @@ -1,6 +1,6 @@ --- description: Details on the Vue frontend app created by Starport. -order: 7 +order: 8 --- # Frontend Overview @@ -29,4 +29,4 @@ By default, the filesystem is watched and the clients are regenerated automatica To regenerate all clients for custom and standard Cosmos SDK modules, run this command: -`starport chain serve --reset-once --rebuild-proto-once` +`starport generate vuex` diff --git a/docs/kb/index.md b/docs/kb/index.md index f7ab739a9b..73af5e77b0 100644 --- a/docs/kb/index.md +++ b/docs/kb/index.md @@ -9,4 +9,4 @@ parent: Knowledge base contains article that cover different aspects of Starport: from scaffolding a chain to starting an IBC relayer. -If you're new to Starport or want to go through a series of tutorials, visit the [Developer Guide](/guide/). +If you're new to Starport or want to go through a series of tutorials, visit the [Developer Tutorials](/guide/). diff --git a/docs/kb/params.md b/docs/kb/params.md new file mode 100644 index 0000000000..d6e94b4fb0 --- /dev/null +++ b/docs/kb/params.md @@ -0,0 +1,31 @@ +--- +order: 15 +description: Module Parameters +--- + +# Module Parameters + +Sometimes you need to set default parameters for a module. The Cosmos SDK [params package](https://docs.cosmos.network/master/modules/params) provides a globally available parameter that is saved into the key-value store. + +Params are managed and centralized by the Cosmos SDK `params` module and are updated with a governance proposal. + +You can use Starport to scaffold parameters to be accessible for the module. Parameters have default values that can be changed when the chain is live. Since the parameters are managed and centralized by the Cosmos SDK params module, they can be easily updated using a governance proposal. + +To scaffold a module with params using the `--params` flag: + +```shell +starport scaffold module launch --params minLaunch:uint,maxLaunch:int +``` + +After the parameters are scaffolded, change the `x//types/params.go` file to set the default values and validate the field. + +The params module supports all [built-in Starport types](types.md). + +## Params Types + +| Type | Code Type | Description | +| ------ | --------- | ----------------------- | +| string | string | Text type | +| bool | bool | Boolean type | +| int | int32 | Integer number | +| uint | uint64 | Unsigned integer number | \ No newline at end of file diff --git a/docs/kb/proto.md b/docs/kb/proto.md index db3cfecd5e..56b0f8f3c6 100644 --- a/docs/kb/proto.md +++ b/docs/kb/proto.md @@ -1,6 +1,6 @@ --- description: Protocol buffer file support in Starport -order: 6 +order: 7 --- # Protocol Buffer Files diff --git a/docs/kb/relayer.md b/docs/kb/relayer.md index e5b5da2a4b..003aea74e2 100644 --- a/docs/kb/relayer.md +++ b/docs/kb/relayer.md @@ -1,6 +1,6 @@ --- description: IBC relayer to connect local and remote blockchains. -order: 8 +order: 9 --- # IBC Relayer diff --git a/docs/kb/serve.md b/docs/kb/serve.md index 6f3810277c..4547a8bb28 100644 --- a/docs/kb/serve.md +++ b/docs/kb/serve.md @@ -8,7 +8,7 @@ description: Use the Starport serve command to start your blockchain. Blockchains are decentralized applications. - In production, blockchains often run the same software on many validator nodes that are run by different people and entities. To launch a blockchain in production, the validator entities coordinate the launch process to start their nodes simultaneously. -- During development, a blockchain can be started locally on a single validator node. This convenient process lets you can restart a chain quickly and iterate faster. Starting a chain on a single node in development is similar to starting a traditional web application on a local server. +- During development, a blockchain can be started locally on a single validator node. This convenient process lets you restart a chain quickly and iterate faster. Starting a chain on a single node in development is similar to starting a traditional web application on a local server. ## Start a Blockchain Node in Development @@ -62,10 +62,6 @@ Reset the state only once. Use this flag to resume a failed reset or to initiali Reset state on every file change. Do not import state and turn off state persistence. -`--rebuild-proto-once` use with `--reset-once` - -Force code generation from proto files for custom and third-party modules. By default, Starport statically scaffolds files generated from Cosmos SDK standard proto files, instead of generating them dynamically. Use this flag to perform code generation on all modules if a blockchain was scaffolded on an earlier Starport version or after a Cosmos SDK upgrade. - `--verbose` Enters verbose detailed mode with extensive logging. @@ -85,4 +81,4 @@ build: binary: "newchaind" ``` -Learn more about how to use the binary to [run a chain in production](https://docs.cosmos.network/v0.42/run-node/run-node.html). +Learn more about how to use the binary to [run a chain in production](https://docs.cosmos.network/master/run-node/run-node.html). diff --git a/docs/kb/simapp.md b/docs/kb/simapp.md new file mode 100644 index 0000000000..a99faac064 --- /dev/null +++ b/docs/kb/simapp.md @@ -0,0 +1,130 @@ +--- +order: 13 +description: Test different scenarios for your chain. + +--- + +# Chain Simulation + +The Starport chain simulator can help you to run your chain based in randomized inputs for you can make fuzz testing and also benchmark test for your chain, simulating the messages, blocks, and accounts. You can scaffold a template to perform simulation testing in each module along with a boilerplate simulation methods for each scaffolded message. + +## Module Simulation + +Every new module that is scaffolded with Starport implements the Cosmos SDK [Module Simulation](https://docs.cosmos.network/master/building-modules/simulator.html). + +- Each new message creates a file with the simulation methods required for the tests. +- Scaffolding a `CRUD` like a `list` or `map` creates a simulation file with `create`, `update`, and `delete` simulation methods in the `x//simulation` folder and registers these methods in `x//module_simulation.go`. +- Scaffolding a single message creates an empty simulation method to be implemented by the user. + +We recommend that you maintain the simulation methods for each new modification into the message keeper methods. + +Every simulation is weighted because the sender of the operation is assigned randomly. The weight defines how much the simulation calls the message. + +For better randomizations, you can define a random seed. The simulation with the same random seed is deterministic with the same output. + +## Scaffold a Simulation + +To create a new chain: + +```shell +starport scaffold chain github.com/cosmonaut/mars +``` + +Review the empty `x/mars/simulation` folder and the `x/mars/module_simulation.go` file to see that a simulation is not registered. + +Now, scaffold a new message: + +```shell +starport scaffold list user address balance:uint state +``` + +A new file `x/mars/simulation/user.go` is created and is registered with the weight in the `x/mars/module_simulation.go` file. + +Be sure to define the proper simulation weight with a minimum weight of 0 and a maximum weight of 100. + +For this example, change the `defaultWeightMsgDeleteUser` to 30 and the `defaultWeightMsgUpdateUser` to 50. + +Run the `BenchmarkSimulation` method into `app/simulation_test.go` to run simulation tests for all modules: + +```shell +starport chain simulate +``` + +You can also define flags that are provided by the simulation. Flags are defined by the method `simapp.GetSimulatorFlags()`: + +```shell +starport chain simulate -v --numBlocks 200 --blockSize 50 --seed 33 +``` + +Wait for the entire simulation to finish and check the result of the messages. + +The default `go test` command works to run the simulation: + +```shell +go test -v -benchmem -run=^$ -bench ^BenchmarkSimulation -cpuprofile cpu.out ./app -Commit=true +``` + +### Skip Message + +Use logic to avoid sending a message without returning an error. Return only `simtypes.NoOpMsg(...)` into the simulation message handler. + +## Params + +Scaffolding a module with params automatically adds the module in the `module_simulaton.go` file: + +```shell +starport s module earth --params channel:string,minLaunch:uint,maxLaunch:int +``` + +After the parameters are scaffolded, change the `x//module_simulation.go` file to set the random parameters into the `RandomizedParams` method. The simulation will change the params randomly according to call the function. + +## Invariants + +Simulating a chain can help you prevent [chain invariants errors](https://docs.cosmos.network/master/building-modules/invariants.html); an invariant is a function called by the chain to check if something broke, invalidating the chain data. +To create a new invariant and check the chain integrity, you should create a method to validate the invariants and register all invariants. + +e.g.: `x/earth/keeper/invariants.go` +```go +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/spn/x/launch/types" +) + +const zeroLaunchTimestampRoute = "zero-launch-timestamp" + +// RegisterInvariants registers all module invariants +func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { + ir.RegisterRoute(types.ModuleName, zeroLaunchTimestampRoute, + ZeroLaunchTimestampInvariant(k)) +} + +// ZeroLaunchTimestampInvariant invariant that checks if the +// `LaunchTimestamp is zero +func ZeroLaunchTimestampInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + all := k.GetAllChain(ctx) + for _, chain := range all { + if chain.LaunchTimestamp == 0 { + return sdk.FormatInvariant( + types.ModuleName, zeroLaunchTimestampRoute, + "LaunchTimestamp is not set while LaunchTriggered is set", + ), true + } + } + return "", false + } +} +``` + +Now you register the keeper invariants into the `x/earth/module.go`: + +```go +// RegisterInvariants registers the capability module's invariants. +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { + keeper.RegisterInvariants(ir, am.keeper) +} +``` \ No newline at end of file diff --git a/docs/kb/types.md b/docs/kb/types.md new file mode 100644 index 0000000000..90423d3c9c --- /dev/null +++ b/docs/kb/types.md @@ -0,0 +1,58 @@ +--- +order: 6 +description: Reference list of supported types. +--- + +# Starport Supported Types + +Types with CRUD operations are scaffolded with the `starport scaffold` command. + +## Built-in Types + +| Type | Alias | Index | Code Type | Description | +| ------------ | -------- | ----- | ----------- | ------------------------------- | +| string | - | yes | string | Text type | +| array.string | strings | no | []string | List of text type | +| bool | - | yes | bool | Boolean type | +| int | - | yes | int32 | Integer type | +| array.int | ints | no | []int32 | List of integers types | +| uint | - | yes | uint64 | Unsigned integer type | +| array.uint | uints | no | []uint64 | List of unsigned integers types | +| coin | - | no | sdk.Coin | Cosmos SDK coin type | +| array.coin | coins | no | sdk.Coins | List of Cosmos SDK coin types | + +You cannot use some types as an index, like the map and list indexes and module params. + +## Custom Types + +You can create custom types and then use the custom type later. + +For example, you can create a `list` type called `user` and then use the `user` type in a subsequent `starport scaffold` command. + +Here's an example of how to scaffold a new `CoordinatorDescription` type that is reusable in the future: + +```shell +starport scaffold list coordinator-description description:string --no-message +``` + +Now you can scaffold a message using the `CoordinatorDescription` type: + +```shell +starport scaffold message add-coordinator address:string description:CoordinatorDescription +``` + +Run the chain and then send the message using the CLI. + +To pass the custom type in JSON format: + +```shell +starport chain serve +marsd tx mars add-coordinator cosmos1t4jkut0yfnsmqle9vxk3adfwwm9vj9gsj98vqf '{"description":"coordinator description"}' true --from alice --chain-id mars +``` + +If you try to use a type that is not created yet, the follow error occurs: + +```shell +starport scaffold message validator validator:ValidatorDescription address:string +-> the field type ValidatorDescription doesn't exist +``` diff --git a/docs/migration/index.md b/docs/migration/index.md index 07f60500bb..0a8a35ad89 100644 --- a/docs/migration/index.md +++ b/docs/migration/index.md @@ -42,7 +42,7 @@ require ( github.com/spf13/cast v1.3.1 github.com/spf13/cobra v1.1.3 github.com/stretchr/testify v1.7.0 - github.com/tendermint/spm v0.1.5 + github.com/tendermint/spm v0.1.6 github.com/tendermint/tendermint v0.34.13 github.com/tendermint/tm-db v0.6.4 google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 diff --git a/go.mod b/go.mod index 48362f3677..225381ee40 100644 --- a/go.mod +++ b/go.mod @@ -5,53 +5,54 @@ go 1.16 require ( github.com/99designs/keyring v1.1.6 github.com/AlecAivazis/survey/v2 v2.1.1 - github.com/Microsoft/hcsshim v0.9.1 // indirect github.com/blang/semver v3.5.1+incompatible github.com/briandowns/spinner v1.11.1 github.com/cenkalti/backoff v2.2.1+incompatible - github.com/cenkalti/backoff/v4 v4.1.2 // indirect github.com/charmbracelet/glow v1.4.0 github.com/containerd/containerd v1.5.8 // indirect - github.com/cosmos/cosmos-sdk v0.44.3 + github.com/cosmos/cosmos-sdk v0.44.5 github.com/cosmos/go-bip39 v1.0.0 github.com/docker/docker v20.10.7+incompatible github.com/emicklei/proto v1.9.0 github.com/fatih/color v1.12.0 github.com/ghodss/yaml v1.0.0 github.com/go-git/go-git/v5 v5.1.0 + github.com/gobuffalo/envy v1.9.0 // indirect github.com/gobuffalo/genny v0.6.0 github.com/gobuffalo/logger v1.0.3 - github.com/gobuffalo/packd v1.0.0 + github.com/gobuffalo/packd v0.3.0 github.com/gobuffalo/plush v3.8.3+incompatible github.com/gobuffalo/plushgen v0.1.2 - github.com/goccy/go-yaml v1.9.2 + github.com/goccy/go-yaml v1.9.4 github.com/gogo/protobuf v1.3.3 github.com/google/go-github/v37 v37.0.0 - github.com/gookit/color v1.4.2 + github.com/gookit/color v1.5.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/rpc v1.2.0 - github.com/iancoleman/strcase v0.1.3 + github.com/iancoleman/strcase v0.2.0 github.com/imdario/mergo v0.3.12 - github.com/jpillora/chisel v1.7.3 - github.com/kr/pretty v0.3.0 // indirect + github.com/jpillora/chisel v1.7.6 + github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-zglob v0.0.3 github.com/moby/sys/mount v0.3.0 // indirect github.com/otiai10/copy v1.6.0 github.com/pelletier/go-toml v1.9.3 github.com/pkg/errors v0.9.1 github.com/radovskyb/watcher v1.0.7 + github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40 github.com/rs/cors v1.7.0 github.com/spf13/cobra v1.2.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 - github.com/tendermint/flutter v1.0.2 - github.com/tendermint/spm v0.1.8 + github.com/tendermint/flutter/v2 v2.0.1 + github.com/tendermint/spm v0.1.9 + github.com/tendermint/spn v0.1.1-0.20211210094128-4ca78a240c57 github.com/tendermint/tendermint v0.34.14 github.com/tendermint/vue v0.1.55 golang.org/x/mod v0.4.2 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d - google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 // indirect + golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf + google.golang.org/grpc v1.42.0 ) replace ( diff --git a/go.sum b/go.sum index 540993c1cc..e3e593f1a5 100644 --- a/go.sum +++ b/go.sum @@ -111,10 +111,8 @@ github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg3 github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.8.23 h1:47MSwtKGXet80aIn+7h4YI6fwPmwIghAnsx2aOUrG2M= github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= -github.com/Microsoft/hcsshim v0.9.1 h1:VfDCj+QnY19ktX5TsH22JHcjaZ05RWQiwDbOyEg5ziM= -github.com/Microsoft/hcsshim v0.9.1/go.mod h1:Y/0uV2jUab5kBI7SQgl62at0AVX7uaruzADAVmxm3eM= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -124,9 +122,7 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8 github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -247,8 +243,6 @@ github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= @@ -273,8 +267,11 @@ github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOo github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU= github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= @@ -328,8 +325,6 @@ github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7 github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= -github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/containerd v1.5.8 h1:NmkCC1/QxyZFBny8JogwLpOy2f+VEbO/f6bV2Mqtwuw= github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -361,7 +356,6 @@ github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJ github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= @@ -403,17 +397,23 @@ github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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/btcutil v1.0.4 h1:n7C2ngKXo7UC9gNyMNLbzqz7Asuf+7Qv4gnX/rOdQ44= +github.com/cosmos/btcutil v1.0.4/go.mod h1:Ffqc8Hn6TJUdDgHBwIZLtrLQC1KdJ9jGJl/TvgUaxbU= github.com/cosmos/cosmos-sdk v0.44.2/go.mod h1:fwQJdw+aECatpTvQTo1tSfHEsxACdZYU80QCZUPnHr4= -github.com/cosmos/cosmos-sdk v0.44.3 h1:F71n1jCqPi4F0wXg8AU4AUdUF8llw0x3D3o6aLt/j2A= github.com/cosmos/cosmos-sdk v0.44.3/go.mod h1:bA3+VenaR/l/vDiYzaiwbWvRPWHMBX2jG0ygiFtiBp0= +github.com/cosmos/cosmos-sdk v0.44.4/go.mod h1:0QTCOkE8IWu5LZyfnbbjFjxYRIcV4pBOr7+zPpJwl58= +github.com/cosmos/cosmos-sdk v0.44.5 h1:t5h+KPzZb0Zsag1RP1DCMQlyJyIQqJcqSPJrbUCDGHY= +github.com/cosmos/cosmos-sdk v0.44.5/go.mod h1:maUA6m2TBxOJZkbwl0eRtEBgTX37kcaiOWU5t1HEGaY= 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/go.mod h1:OLjQiAQ4fGD2KDZooyJG9yz+p2ao2IAYSbke8mVvSA4= -github.com/cosmos/iavl v0.17.1 h1:b/Cl8h1PRMvsu24+TYNlKchIu7W6tmxIBGe6E9u2Ybw= github.com/cosmos/iavl v0.17.1/go.mod h1:7aisPZK8yCpQdy3PMvKeO+bhq1NwDjUwjzxwwROUxFk= +github.com/cosmos/iavl v0.17.2/go.mod h1:prJoErZFABYZGDHka1R6Oay4z9PrNeFFiMKHDAMOi4w= +github.com/cosmos/iavl v0.17.3 h1:s2N819a2olOmiauVa0WAhoIJq9EhSXE9HDBAoR9k+8Y= +github.com/cosmos/iavl v0.17.3/go.mod h1:prJoErZFABYZGDHka1R6Oay4z9PrNeFFiMKHDAMOi4w= github.com/cosmos/ibc-go v1.2.2/go.mod h1:XmYjsRFOs6Q9Cz+CSsX21icNoH27vQKb3squgnCOCbs= 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= @@ -463,15 +463,12 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ= github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= @@ -502,7 +499,6 @@ github.com/emicklei/proto v1.9.0 h1:l0QiNT6Qs7Yj0Mb4X6dnWBQer4ebei2BFcgQLbGqUDc= github.com/emicklei/proto v1.9.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/enigmampc/btcutil v1.0.3-0.20200723161021-e2fb6adb2a25 h1:2vLKys4RBU4pn2T/hjXMbvwTr1Cvy5THHrQkbeY9HRk= github.com/enigmampc/btcutil v1.0.3-0.20200723161021-e2fb6adb2a25/go.mod h1:hTr8+TLQmkUkgcuh3mcr5fjrT9c64ZzsBCdCEC6UppY= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -577,15 +573,11 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -616,8 +608,9 @@ github.com/go-toolsmith/typep v1.0.2/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2 github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= -github.com/gobuffalo/envy v1.8.1 h1:RUr68liRvs0TS1D5qdW3mQv2SjAsu1QWMCx1tG4kDjs= github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/envy v1.9.0 h1:eZR0DuEgVLfeIb1zIKt3bT4YovIMf9O9LXQeCZLXpqE= +github.com/gobuffalo/envy v1.9.0/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/flect v0.2.0 h1:EWCvMGGxOjsgwlWaP+f4+Hh6yrrte7JeFL2S6b+0hdM= github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/genny v0.6.0 h1:d7c6d66ZrTHHty01hDX1/TcTWvAJQxRZl885KWX5kHY= @@ -630,9 +623,8 @@ github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PL github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc= github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= -github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= -github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o= github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= github.com/gobuffalo/plush v3.8.3+incompatible h1:kzvUTnFPhwyfPEsx7U7LI05/IIslZVGnAlMA1heWub8= @@ -649,8 +641,8 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/goccy/go-yaml v1.9.2 h1:2Njwzw+0+pjU2gb805ZC1B/uBuAs2VcZ3K+ZgHwDs7w= -github.com/goccy/go-yaml v1.9.2/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/goccy/go-yaml v1.9.4 h1:S0GCYjwHKVI6IHqio7QWNKNThUl6NLzFd/g8Z65Axw8= +github.com/goccy/go-yaml v1.9.4/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -733,7 +725,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-github/v37 v37.0.0 h1:rCspN8/6kB1BAJWZfuafvHhyfIo5fkAulaP/3bOQ/tM= github.com/google/go-github/v37 v37.0.0/go.mod h1:LM7in3NmXDrX58GbEHy7FtNLbI2JijX93RnMKvWG3m4= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= @@ -773,8 +764,9 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ 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/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0 h1:1Opow3+BWDwqor78DcJkJCIwnkviFi+rrOANki9BUFw= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= 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/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= @@ -876,8 +868,8 @@ github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63 github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= -github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw= -github.com/iancoleman/strcase v0.1.3/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -914,7 +906,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -923,8 +914,8 @@ github.com/jpillora/ansi v1.0.2 h1:+Ei5HCAH0xsrQRCT2PDr4mq9r4Gm4tg+arNdXRkB22s= github.com/jpillora/ansi v1.0.2/go.mod h1:D2tT+6uzJvN1nBVQILYWkIdq7zG+b5gcFN5WI/VyjMY= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/jpillora/chisel v1.7.3 h1:6yaUb8/JaWhmkJSQBHsrVMawjgGNsxkkCUUrl07VIjE= -github.com/jpillora/chisel v1.7.3/go.mod h1:BC2zg11mTIoyGPUjc2EkTgfz3uUUV93+K9tNYCCU/fw= +github.com/jpillora/chisel v1.7.6 h1:ipVmcsJcz90+0u0rkn07wKwcXNiw1W/3E2w3rxIPpWM= +github.com/jpillora/chisel v1.7.6/go.mod h1:BC2zg11mTIoyGPUjc2EkTgfz3uUUV93+K9tNYCCU/fw= github.com/jpillora/requestlog v1.0.0 h1:bg++eJ74T7DYL3DlIpiwknrtfdUA9oP/M4fL+PpqnyA= github.com/jpillora/requestlog v1.0.0/go.mod h1:HTWQb7QfDc2jtHnWe2XEIEeJB7gJPnVdpNn52HXPvy8= github.com/jpillora/sizestr v1.0.0 h1:4tr0FLxs1Mtq3TnsLDV+GYUWG7Q26a6s+tV5Zfw2ygw= @@ -970,9 +961,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5 h1:hyz3dwM5QLc1Rfoz4FuWJQG5BN7tc6K1MndAUnGpQr4= @@ -1000,7 +990,6 @@ github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOS 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/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -1010,10 +999,11 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/matoous/godox v0.0.0-20210227103229-6504466cf951/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= @@ -1044,7 +1034,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To= github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= @@ -1052,7 +1041,6 @@ github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpe github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/meowgorithm/babyenv v1.3.0/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig= github.com/meowgorithm/babyenv v1.3.1 h1:18ZEYIgbzoFQfRLF9+lxjRfk/ui6w8U0FWl07CgWvvc= @@ -1107,7 +1095,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/moricho/tparallel v0.2.1/go.mod h1:fXEIZxG2vdfl0ZF8b42f5a78EhjjD5mX8qUplsoSU4k= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mozilla/scribe v0.0.0-20180711195314-fb71baf557c1/go.mod h1:FIczTrinKo8VaLxe6PWTPEXRXDIHz2QAwiaBaP5/4a8= github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= @@ -1170,11 +1157,9 @@ github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 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.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.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/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= @@ -1184,10 +1169,8 @@ github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGV github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 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.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/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/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= @@ -1333,6 +1316,8 @@ github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Ung 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/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40 h1:31Y7UZ1yTYBU4E79CE52I/1IRi3TqiuwquXGNtZDXWs= +github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40/go.mod h1:j4c6zEU0eMG1oiZPUy+zD4ykX0NIpjZAEOEAviTWC18= github.com/regen-network/cosmos-proto v0.3.1 h1:rV7iM4SSFAagvy8RiyhiACbWEGotmqzywPxOvwMdxcg= github.com/regen-network/cosmos-proto v0.3.1/go.mod h1:jO0sVX6a1B36nmE8C9xBFXpNwWejXC7QqCOnH3O0+YM= github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= @@ -1347,7 +1332,6 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0= github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v0.0.0-20160617231935-a62a804a8a00/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= @@ -1375,7 +1359,6 @@ github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZ 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/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/securego/gosec/v2 v2.8.1/go.mod h1:pUmsq6+VyFEElJMUX+QB3p3LWNHXg1R3xh2ssVJPs8Q= @@ -1489,12 +1472,15 @@ 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/flutter v1.0.2 h1:c3ZAJ348lf+doq17ftT3by5K2TPoHBhsUmzh36rWqSY= -github.com/tendermint/flutter v1.0.2/go.mod h1:hW8URRpeA0KEaBWl9sOJUAlFLXeUrNWMvUoahMi1s3A= +github.com/tendermint/flutter/v2 v2.0.1 h1:PdgduMhrqHEtduQUFc1SukdguMHwScsCMwOoa4ntFMQ= +github.com/tendermint/flutter/v2 v2.0.1/go.mod h1:hnaVhWhzv2Od1LqZFWrRKwiOHeMonsB9EIWP0AGMPw0= 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/spm v0.1.8 h1:ya0o4Um6Hht2LC1X/y0+9Rz0Q8DyUhRIIXR9vgDUTGA= github.com/tendermint/spm v0.1.8/go.mod h1:iHgfQ5YOI6ONc9E7ugGQolVdfSMHpeXfZ/OpXuN/42Q= +github.com/tendermint/spm v0.1.9 h1:O1DJF4evS8wgk5SZqRcO29irNNtKQmTpvQ0xFzUiczI= +github.com/tendermint/spm v0.1.9/go.mod h1:iHgfQ5YOI6ONc9E7ugGQolVdfSMHpeXfZ/OpXuN/42Q= +github.com/tendermint/spn v0.1.1-0.20211210094128-4ca78a240c57 h1:+QwioCbnSZ+bwYDkSP6siMv1CLKLu4AHPFN/6JFtzwM= +github.com/tendermint/spn v0.1.1-0.20211210094128-4ca78a240c57/go.mod h1:p4BO8YC6kOKSKqMfySqaLHfwBmuPE/QcLwnnVhh7H9M= 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= @@ -1583,9 +1569,8 @@ github.com/zondax/hid v0.9.0/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWp 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/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= @@ -1650,8 +1635,9 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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= @@ -1749,7 +1735,6 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg= golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1857,7 +1842,6 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1883,6 +1867,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1890,9 +1875,8 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6Dg golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= @@ -1910,7 +1894,6 @@ golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/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-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/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-20190307163923-6a08e3108db3/go.mod h1:25r3+/G6/xytQM8iWZKq3Hn0kr0rgFKPUNVEL/dr3z4= @@ -1929,7 +1912,6 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/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-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1970,11 +1952,9 @@ golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200622203043-20e05c1c8ffa/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -1992,7 +1972,6 @@ golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200831203904-5a2aa26beb65/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -2089,7 +2068,6 @@ google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -2111,8 +2089,9 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 h1:3V2dxSZpz4zozWWUq36vUxXEKnSYitEH2LdsAx+RUmg= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12 h1:DN5b3HU13J4sMd/QjDx34U6afpaexKTDdop+26pdjdk= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -2201,7 +2180,6 @@ k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= -k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= @@ -2210,12 +2188,8 @@ k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= @@ -2230,7 +2204,6 @@ 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/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/integration/cmd_app_test.go b/integration/app/cmd_app_test.go similarity index 90% rename from integration/cmd_app_test.go rename to integration/app/cmd_app_test.go index a44c01fce9..c037a25a72 100644 --- a/integration/cmd_app_test.go +++ b/integration/app/cmd_app_test.go @@ -1,7 +1,7 @@ //go:build !relayer // +build !relayer -package integration_test +package app_test import ( "fmt" @@ -10,12 +10,13 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/tendermint/starport/integration" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" ) func TestGenerateAnApp(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog") ) @@ -27,7 +28,7 @@ func TestGenerateAnApp(t *testing.T) { func TestGenerateAnAppWithNoDefaultModule(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) appName = "blog" ) @@ -46,7 +47,7 @@ func TestGenerateAnAppWithNoDefaultModule(t *testing.T) { ) // Cleanup the home directory of the app - env.t.Cleanup(func() { + env.SetCleanup(func() { os.RemoveAll(filepath.Join(env.Home(), fmt.Sprintf(".%s", appName))) }) @@ -60,7 +61,7 @@ func TestGenerateAnAppWithNoDefaultModule(t *testing.T) { func TestGenerateAnAppWithNoDefaultModuleAndCreateAModule(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog", "--no-module") ) @@ -78,7 +79,7 @@ func TestGenerateAnAppWithWasm(t *testing.T) { t.Skip() var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog") ) @@ -94,7 +95,7 @@ func TestGenerateAnAppWithWasm(t *testing.T) { step.Exec("starport", "s", "wasm"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.EnsureAppIsSteady(path) @@ -102,7 +103,7 @@ func TestGenerateAnAppWithWasm(t *testing.T) { func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog") ) @@ -118,7 +119,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { step.Exec("starport", "s", "module", "example", "--require-registration"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a module with an invalid name", @@ -126,7 +127,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { step.Exec("starport", "s", "module", "example1", "--require-registration"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a module with a reserved name", @@ -134,7 +135,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { step.Exec("starport", "s", "module", "tx", "--require-registration"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a module with a forbidden prefix", @@ -142,7 +143,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { step.Exec("starport", "s", "module", "ibcfoo", "--require-registration"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a module prefixed with an existing module", @@ -150,7 +151,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { step.Exec("starport", "s", "module", "examplefoo", "--require-registration"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("create a module with dependencies", @@ -181,7 +182,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { ), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a module with a non registered dependency", @@ -197,7 +198,7 @@ func TestGenerateAStargateAppWithEmptyModule(t *testing.T) { ), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.EnsureAppIsSteady(path) diff --git a/integration/cmd_ibc_test.go b/integration/app/cmd_ibc_test.go similarity index 83% rename from integration/cmd_ibc_test.go rename to integration/app/cmd_ibc_test.go index 675c376dde..33557c5211 100644 --- a/integration/cmd_ibc_test.go +++ b/integration/app/cmd_ibc_test.go @@ -1,19 +1,20 @@ //go:build !relayer // +build !relayer -package integration_test +package app_test import ( "path/filepath" "testing" + "github.com/tendermint/starport/integration" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" ) func TestCreateModuleWithIBC(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blogibc") ) @@ -107,7 +108,7 @@ func TestCreateModuleWithIBC(t *testing.T) { func TestCreateIBCOracle(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("ibcoracle") ) @@ -118,6 +119,22 @@ func TestCreateIBCOracle(t *testing.T) { )), )) + env.Must(env.Exec("create an IBC module with params", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "module", + "paramsFoo", + "--ibc", + "--params", + "defaultName,isLaunched:bool,minLaunch:uint,maxLaunch:int", + "--require-registration", + ), + step.Workdir(path), + )), + )) + env.Must(env.Exec("create the first BandChain oracle integration", step.NewSteps(step.New( step.Exec("starport", "s", "band", "oracleone", "--module", "foo"), @@ -137,7 +154,7 @@ func TestCreateIBCOracle(t *testing.T) { step.Exec("starport", "s", "band", "invalidOracle"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a BandChain oracle in a non existent module", @@ -145,12 +162,12 @@ func TestCreateIBCOracle(t *testing.T) { step.Exec("starport", "s", "band", "invalidOracle", "--module", "nomodule"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("create a non-IBC module", step.NewSteps(step.New( - step.Exec("starport", "s", "module", "bar", "--require-registration"), + step.Exec("starport", "s", "module", "bar", "--params", "name,minLaunch:uint,maxLaunch:int", "--require-registration"), step.Workdir(path), )), )) @@ -160,7 +177,7 @@ func TestCreateIBCOracle(t *testing.T) { step.Exec("starport", "s", "band", "invalidOracle", "--module", "bar"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.EnsureAppIsSteady(path) @@ -169,7 +186,7 @@ func TestCreateIBCOracle(t *testing.T) { func TestCreateIBCPacket(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blogibc2") ) @@ -188,6 +205,7 @@ func TestCreateIBCPacket(t *testing.T) { "packet", "bar", "text", + "texts:strings", "--module", "foo", "--ack", @@ -202,7 +220,7 @@ func TestCreateIBCPacket(t *testing.T) { step.Exec("starport", "s", "packet", "bar", "text"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a packet in a non existent module", @@ -210,7 +228,7 @@ func TestCreateIBCPacket(t *testing.T) { step.Exec("starport", "s", "packet", "bar", "text", "--module", "nomodule"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating an existing packet", @@ -218,12 +236,30 @@ func TestCreateIBCPacket(t *testing.T) { step.Exec("starport", "s", "packet", "bar", "post", "--module", "foo"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("create a packet with custom type fields", step.NewSteps(step.New( - step.Exec("starport", "s", "packet", "ticket", "num:int", "victory:bool", "--module", "foo"), + step.Exec("starport", + "s", + "packet", + "ticket", + "numInt:int", + "numsInt:array.int", + "numsIntAlias:ints", + "numUint:uint", + "numsUint:array.uint", + "numsUintAlias:uints", + "textString:string", + "textStrings:array.string", + "textStringsAlias:strings", + "textCoin:coin", + "textCoins:array.coin", + "textCoinsAlias:coins", + "victory:bool", + "--module", + "foo"), step.Workdir(path), )), )) @@ -268,7 +304,7 @@ func TestCreateIBCPacket(t *testing.T) { step.Exec("starport", "s", "packet", "foo", "text", "--module", "bar"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.EnsureAppIsSteady(path) diff --git a/integration/cmd_serve_test.go b/integration/app/cmd_serve_test.go similarity index 78% rename from integration/cmd_serve_test.go rename to integration/app/cmd_serve_test.go index 16d7e37a72..6731e76be9 100644 --- a/integration/cmd_serve_test.go +++ b/integration/app/cmd_serve_test.go @@ -1,16 +1,16 @@ //go:build !relayer // +build !relayer -package integration_test +package app_test import ( "context" - "io/ioutil" "os" "path/filepath" "testing" "github.com/stretchr/testify/require" + "github.com/tendermint/starport/integration" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" ) @@ -18,7 +18,7 @@ func TestServeStargateWithWasm(t *testing.T) { t.Skip() var ( - env = newEnv(t) + env = envtest.New(t) apath = env.Scaffold("sgblog") servers = env.RandomizeServerPorts(apath, "") ) @@ -31,41 +31,41 @@ func TestServeStargateWithWasm(t *testing.T) { )) var ( - ctx, cancel = context.WithTimeout(env.Ctx(), serveTimeout) + ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) isBackendAliveErr error ) go func() { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", apath, "", "", ExecCtx(ctx))) + env.Must(env.Serve("should serve with Stargate version", apath, "", "", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } func TestServeStargateWithCustomHome(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) apath = env.Scaffold("sgblog2") servers = env.RandomizeServerPorts(apath, "") ) var ( - ctx, cancel = context.WithTimeout(env.Ctx(), serveTimeout) + ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) isBackendAliveErr error ) go func() { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", apath, "./home", "", ExecCtx(ctx))) + env.Must(env.Serve("should serve with Stargate version", apath, "./home", "", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } func TestServeStargateWithConfigHome(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) apath = env.Scaffold("sgblog3") servers = env.RandomizeServerPorts(apath, "") ) @@ -74,25 +74,25 @@ func TestServeStargateWithConfigHome(t *testing.T) { env.SetRandomHomeConfig(apath, "") var ( - ctx, cancel = context.WithTimeout(env.Ctx(), serveTimeout) + ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) isBackendAliveErr error ) go func() { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", apath, "", "", ExecCtx(ctx))) + env.Must(env.Serve("should serve with Stargate version", apath, "", "", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } func TestServeStargateWithCustomConfigFile(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "starporttest") + tmpDir, err := os.MkdirTemp("", "starporttest") require.NoError(t, err) defer os.RemoveAll(tmpDir) var ( - env = newEnv(t) + env = envtest.New(t) apath = env.Scaffold("sgblog4") ) // Move config @@ -107,14 +107,14 @@ func TestServeStargateWithCustomConfigFile(t *testing.T) { env.SetRandomHomeConfig(tmpDir, newConfig) var ( - ctx, cancel = context.WithTimeout(env.Ctx(), serveTimeout) + ctx, cancel = context.WithTimeout(env.Ctx(), envtest.ServeTimeout) isBackendAliveErr error ) go func() { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve with Stargate version", apath, "", newConfigPath, ExecCtx(ctx))) + env.Must(env.Serve("should serve with Stargate version", apath, "", newConfigPath, envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") } diff --git a/integration/config_test.go b/integration/app/config_test.go similarity index 86% rename from integration/config_test.go rename to integration/app/config_test.go index 913b14fcc7..3f49828446 100644 --- a/integration/config_test.go +++ b/integration/app/config_test.go @@ -1,7 +1,7 @@ //go:build !relayer // +build !relayer -package integration_test +package app_test import ( "context" @@ -9,14 +9,15 @@ import ( "testing" "github.com/stretchr/testify/require" - conf "github.com/tendermint/starport/starport/chainconf" + "github.com/tendermint/starport/integration" + "github.com/tendermint/starport/starport/chainconfig" "github.com/tendermint/starport/starport/pkg/confile" "github.com/tendermint/starport/starport/pkg/randstr" ) func TestOverwriteSDKConfigsAndChainID(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) appname = randstr.Runes(10) path = env.Scaffold(appname) servers = env.RandomizeServerPorts(path, "") @@ -24,7 +25,7 @@ func TestOverwriteSDKConfigsAndChainID(t *testing.T) { isBackendAliveErr error ) - var c conf.Config + var c chainconfig.Config cf := confile.New(confile.DefaultYAMLEncodingCreator, filepath.Join(path, "config.yml")) require.NoError(t, cf.Load(&c)) @@ -39,7 +40,7 @@ func TestOverwriteSDKConfigsAndChainID(t *testing.T) { defer cancel() isBackendAliveErr = env.IsAppServed(ctx, servers) }() - env.Must(env.Serve("should serve", path, "", "", ExecCtx(ctx))) + env.Must(env.Serve("should serve", path, "", "", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") configs := []struct { diff --git a/integration/tx_test.go b/integration/app/tx_test.go similarity index 94% rename from integration/tx_test.go rename to integration/app/tx_test.go index 50e0b6df22..08c14db722 100644 --- a/integration/tx_test.go +++ b/integration/app/tx_test.go @@ -1,13 +1,14 @@ //go:build !relayer // +build !relayer -package integration_test +package app_test import ( "bytes" "context" "encoding/json" "fmt" + "github.com/tendermint/starport/integration" "net/http" "testing" @@ -21,7 +22,7 @@ import ( func TestGetTxViaGRPCGateway(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) appname = randstr.Runes(10) path = env.Scaffold(appname) host = env.RandomizeServerPorts(path, "") @@ -74,10 +75,10 @@ func TestGetTxViaGRPCGateway(t *testing.T) { addresses := []string{} // collect addresses of alice and bob. - accounts := []struct { + var accounts []struct { Name string `json:"name"` Address string `json:"address"` - }{} + } if err := json.NewDecoder(output).Decode(&accounts); err != nil { return err } @@ -152,10 +153,10 @@ func TestGetTxViaGRPCGateway(t *testing.T) { go func() { defer cancel() - isTxBodyRetrieved = env.Exec("retrieve account addresses", steps, ExecRetry()) + isTxBodyRetrieved = env.Exec("retrieve account addresses", steps, envtest.ExecRetry()) }() - env.Must(env.Serve("should serve", path, "", "", ExecCtx(ctx))) + env.Must(env.Serve("should serve", path, "", "", envtest.ExecCtx(ctx))) if !isTxBodyRetrieved { t.FailNow() diff --git a/integration/env_test.go b/integration/env.go similarity index 74% rename from integration/env_test.go rename to integration/env.go index a51835dfb0..d7e9e3cdeb 100644 --- a/integration/env_test.go +++ b/integration/env.go @@ -1,4 +1,4 @@ -package integration_test +package envtest import ( "bytes" @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strconv" @@ -17,41 +16,54 @@ import ( "github.com/goccy/go-yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - starportconf "github.com/tendermint/starport/starport/chainconf" + "github.com/tendermint/starport/starport/chainconfig" "github.com/tendermint/starport/starport/pkg/availableport" "github.com/tendermint/starport/starport/pkg/cmdrunner" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" "github.com/tendermint/starport/starport/pkg/gocmd" "github.com/tendermint/starport/starport/pkg/httpstatuschecker" + "github.com/tendermint/starport/starport/pkg/xexec" "github.com/tendermint/starport/starport/pkg/xurl" ) const ( - serveTimeout = time.Minute * 15 + ServeTimeout = time.Minute * 15 + StarportApp = "starport" ) var isCI, _ = strconv.ParseBool(os.Getenv("CI")) -// env provides an isolated testing environment and what's needed to +// Env provides an isolated testing environment and what's needed to // make it possible. -type env struct { +type Env struct { t *testing.T ctx context.Context } -// env creates a new testing environment. -func newEnv(t *testing.T) env { +// New creates a new testing environment. +func New(t *testing.T) Env { ctx, cancel := context.WithCancel(context.Background()) - e := env{ + e := Env{ t: t, ctx: ctx, } t.Cleanup(cancel) + + if !xexec.IsCommandAvailable(StarportApp) { + t.Fatal("starport needs to be installed") + } + return e } +// SetCleanup registers a function to be called when the test (or subtest) and all its +// subtests complete. +func (e Env) SetCleanup(f func()) { + e.t.Cleanup(f) +} + // Ctx returns parent context for the test suite to use for cancelations. -func (e env) Ctx() context.Context { +func (e Env) Ctx() context.Context { return e.ctx } @@ -61,38 +73,38 @@ type execOptions struct { stdout, stderr io.Writer } -type execOption func(*execOptions) +type ExecOption func(*execOptions) // ExecShouldError sets the expectations of a command's execution to end with a failure. -func ExecShouldError() execOption { +func ExecShouldError() ExecOption { return func(o *execOptions) { o.shouldErr = true } } // ExecCtx sets cancelation context for the execution. -func ExecCtx(ctx context.Context) execOption { +func ExecCtx(ctx context.Context) ExecOption { return func(o *execOptions) { o.ctx = ctx } } // ExecStdout captures stdout of an execution. -func ExecStdout(w io.Writer) execOption { +func ExecStdout(w io.Writer) ExecOption { return func(o *execOptions) { o.stdout = w } } -// ExecSterr captures stderr of an execution. -func ExecStderr(w io.Writer) execOption { +// ExecStderr captures stderr of an execution. +func ExecStderr(w io.Writer) ExecOption { return func(o *execOptions) { o.stderr = w } } // ExecRetry retries command until it is successful before context is canceled. -func ExecRetry() execOption { +func ExecRetry() ExecOption { return func(o *execOptions) { o.shouldRetry = true } @@ -100,11 +112,11 @@ func ExecRetry() execOption { // Exec executes a command step with options where msg describes the expectation from the test. // unless calling with Must(), Exec() will not exit test runtime on failure. -func (e env) Exec(msg string, steps step.Steps, options ...execOption) (ok bool) { +func (e Env) Exec(msg string, steps step.Steps, options ...ExecOption) (ok bool) { opts := &execOptions{ ctx: e.ctx, - stdout: ioutil.Discard, - stderr: ioutil.Discard, + stdout: io.Discard, + stderr: io.Discard, } for _, o := range options { o(opts) @@ -150,12 +162,12 @@ const ( ) // Scaffold scaffolds an app to a unique appPath and returns it. -func (e env) Scaffold(appName string, flags ...string) (appPath string) { +func (e Env) Scaffold(appName string, flags ...string) (appPath string) { root := e.TmpDir() e.Exec("scaffold an app", step.NewSteps(step.New( step.Exec( - "starport", + StarportApp, append([]string{ "scaffold", "chain", @@ -175,9 +187,9 @@ func (e env) Scaffold(appName string, flags ...string) (appPath string) { } // Serve serves an application lives under path with options where msg describes the -// expection from the serving action. +// execution from the serving action. // unless calling with Must(), Serve() will not exit test runtime on failure. -func (e env) Serve(msg, path, home, configPath string, options ...execOption) (ok bool) { +func (e Env) Serve(msg, path, home, configPath string, options ...ExecOption) (ok bool) { serveCommand := []string{ "chain", "serve", @@ -193,16 +205,34 @@ func (e env) Serve(msg, path, home, configPath string, options ...execOption) (o return e.Exec(msg, step.NewSteps(step.New( - step.Exec("starport", serveCommand...), + step.Exec(StarportApp, serveCommand...), step.Workdir(path), )), options..., ) } +// Simulate runs the simulation test for the app +func (e Env) Simulate(appPath string, numBlocks, blockSize int) { + e.Exec("running the simulation tests", + step.NewSteps(step.New( + step.Exec( + StarportApp, + "chain", + "simulate", + "--numBlocks", + strconv.Itoa(numBlocks), + "--blockSize", + strconv.Itoa(blockSize), + ), + step.Workdir(appPath), + )), + ) +} + // EnsureAppIsSteady ensures that app living at the path can compile and its tests // are passing. -func (e env) EnsureAppIsSteady(appPath string) { +func (e Env) EnsureAppIsSteady(appPath string) { _, statErr := os.Stat(filepath.Join(appPath, "config.yml")) require.False(e.t, os.IsNotExist(statErr), "config.yml cannot be found") @@ -216,7 +246,7 @@ func (e env) EnsureAppIsSteady(appPath string) { // IsAppServed checks that app is served properly and servers are started to listening // before ctx canceled. -func (e env) IsAppServed(ctx context.Context, host starportconf.Host) error { +func (e Env) IsAppServed(ctx context.Context, host chainconfig.Host) error { checkAlive := func() error { ok, err := httpstatuschecker.Check(ctx, xurl.HTTP(host.API)+"/node_info") if err == nil && !ok { @@ -228,8 +258,8 @@ func (e env) IsAppServed(ctx context.Context, host starportconf.Host) error { } // TmpDir creates a new temporary directory. -func (e env) TmpDir() (path string) { - path, err := ioutil.TempDir("", "integration") +func (e Env) TmpDir() (path string) { + path, err := os.MkdirTemp("", "integration") require.NoError(e.t, err, "create a tmp dir") e.t.Cleanup(func() { os.RemoveAll(path) }) return path @@ -237,7 +267,7 @@ func (e env) TmpDir() (path string) { // RandomizeServerPorts randomizes server ports for the app at path, updates // its config.yml and returns new values. -func (e env) RandomizeServerPorts(path string, configFile string) starportconf.Host { +func (e Env) RandomizeServerPorts(path string, configFile string) chainconfig.Host { if configFile == "" { configFile = "config.yml" } @@ -250,7 +280,7 @@ func (e env) RandomizeServerPorts(path string, configFile string) starportconf.H return fmt.Sprintf("localhost:%d", port) } - servers := starportconf.Host{ + servers := chainconfig.Host{ RPC: genAddr(ports[0]), P2P: genAddr(ports[1]), Prof: genAddr(ports[2]), @@ -264,7 +294,7 @@ func (e env) RandomizeServerPorts(path string, configFile string) starportconf.H require.NoError(e.t, err) defer configyml.Close() - var conf starportconf.Config + var conf chainconfig.Config require.NoError(e.t, yaml.NewDecoder(configyml).Decode(&conf)) conf.Host = servers @@ -277,7 +307,7 @@ func (e env) RandomizeServerPorts(path string, configFile string) starportconf.H } // SetRandomHomeConfig sets in the blockchain config files generated temporary directories for home directories -func (e env) SetRandomHomeConfig(path string, configFile string) { +func (e Env) SetRandomHomeConfig(path string, configFile string) { if configFile == "" { configFile = "config.yml" } @@ -287,7 +317,7 @@ func (e env) SetRandomHomeConfig(path string, configFile string) { require.NoError(e.t, err) defer configyml.Close() - var conf starportconf.Config + var conf chainconfig.Config require.NoError(e.t, yaml.NewDecoder(configyml).Decode(&conf)) conf.Init.Home = e.TmpDir() @@ -299,20 +329,20 @@ func (e env) SetRandomHomeConfig(path string, configFile string) { // Must fails the immediately if not ok. // t.Fail() needs to be called for the failing tests before running Must(). -func (e env) Must(ok bool) { +func (e Env) Must(ok bool) { if !ok { e.t.FailNow() } } // Home returns user's home dir. -func (e env) Home() string { +func (e Env) Home() string { home, err := os.UserHomeDir() require.NoError(e.t, err) return home } -// AppHome returns appd's home dir. -func (e env) AppdHome(name string) string { +// AppdHome returns appd's home dir. +func (e Env) AppdHome(name string) string { return filepath.Join(e.Home(), fmt.Sprintf(".%s", name)) } diff --git a/integration/integration_test.go b/integration/integration_test.go deleted file mode 100644 index ab2cd2c26d..0000000000 --- a/integration/integration_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// Package integration_test integration test Starport and scaffolded apps. -package integration_test - -import ( - "errors" - "flag" - "fmt" - "os" - "testing" - - "github.com/tendermint/starport/starport/pkg/xexec" -) - -func TestMain(m *testing.M) { - flag.Parse() - if err := checkSystemRequirements(); err != nil { - fmt.Println(err) - os.Exit(1) - } - os.Exit(m.Run()) -} - -func checkSystemRequirements() error { - if !xexec.IsCommandAvailable("starport") { - return errors.New("starport needs to be installed") - } - return nil -} diff --git a/integration/cmd_list_test.go b/integration/list/cmd_list_test.go similarity index 81% rename from integration/cmd_list_test.go rename to integration/list/cmd_list_test.go index 4d829d173f..52720f9078 100644 --- a/integration/cmd_list_test.go +++ b/integration/list/cmd_list_test.go @@ -1,18 +1,19 @@ //go:build !relayer // +build !relayer -package integration_test +package list_test import ( "path/filepath" "testing" + "github.com/tendermint/starport/integration" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" ) func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog") ) @@ -30,9 +31,25 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { )), )) - env.Must(env.Exec("create a list with int", - step.NewSteps(step.New( - step.Exec("starport", "s", "list", "employee", "name:string", "level:int"), + env.Must(env.Exec("create a custom type fields", + step.NewSteps(step.New( + step.Exec("starport", + "s", + "list", + "employee", + "numInt:int", + "numsInt:array.int", + "numsIntAlias:ints", + "numUint:uint", + "numsUint:array.uint", + "numsUintAlias:uints", + "textString:string", + "textStrings:array.string", + "textStringsAlias:strings", + "textCoin:coin", + "textCoins:array.coin", + "textCoinsAlias:coins", + ), step.Workdir(path), )), )) @@ -56,7 +73,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { step.Exec("starport", "s", "list", "company", "name", "name"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a list with unrecognized field type", @@ -64,7 +81,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { step.Exec("starport", "s", "list", "employee", "level:itn"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating an existing list", @@ -72,7 +89,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { step.Exec("starport", "s", "list", "user", "email"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a list whose name is a reserved word", @@ -80,7 +97,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { step.Exec("starport", "s", "list", "map", "size:int"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a list containing a field with a reserved word", @@ -88,7 +105,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { step.Exec("starport", "s", "list", "document", "type:int"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("create a list with no interaction message", @@ -103,7 +120,7 @@ func TestGenerateAnAppWithStargateWithListAndVerify(t *testing.T) { func TestCreateListInCustomModuleWithStargate(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog") ) @@ -133,7 +150,7 @@ func TestCreateListInCustomModuleWithStargate(t *testing.T) { step.Exec("starport", "s", "list", "user", "email", "--module", "idontexist"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating an existing list", @@ -141,7 +158,7 @@ func TestCreateListInCustomModuleWithStargate(t *testing.T) { step.Exec("starport", "s", "list", "user", "email", "--module", "example"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.EnsureAppIsSteady(path) diff --git a/integration/cmd_map_test.go b/integration/map/cmd_map_test.go similarity index 62% rename from integration/cmd_map_test.go rename to integration/map/cmd_map_test.go index 54f67fda0c..4deb544d37 100644 --- a/integration/cmd_map_test.go +++ b/integration/map/cmd_map_test.go @@ -1,24 +1,25 @@ //go:build !relayer // +build !relayer -package integration_test +package map_test import ( "path/filepath" "testing" + "github.com/tendermint/starport/integration" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" ) func TestCreateMapWithStargate(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog") ) env.Must(env.Exec("create a map", step.NewSteps(step.New( - step.Exec("starport", "s", "map", "user", "email"), + step.Exec("starport", "s", "map", "user", "user-id", "email"), step.Workdir(path), )), )) @@ -56,30 +57,84 @@ func TestCreateMapWithStargate(t *testing.T) { step.Exec("starport", "s", "map", "user", "email", "--module", "example"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("create a map in a custom module", step.NewSteps(step.New( - step.Exec("starport", "s", "map", "mapuser", "email", "--module", "example"), + step.Exec("starport", "s", "map", "mapUser", "email", "--module", "example"), step.Workdir(path), )), )) env.Must(env.Exec("create a map with a custom field type", step.NewSteps(step.New( - step.Exec("starport", "s", "map", "mapdetail", "user:Mapuser", "--module", "example"), + step.Exec("starport", "s", "map", "mapDetail", "user:MapUser", "--module", "example"), step.Workdir(path), )), )) - env.Must(env.Exec("create a map with custom indexes", + env.Must(env.Exec("create a map with Coin and []Coin", step.NewSteps(step.New( - step.Exec("starport", "s", "map", "map_with_index", "email", "--index", "foo:string,bar:int,foobar:uint,barFoo:bool"), + step.Exec("starport", + "s", + "map", + "salary", + "numInt:int", + "numsInt:array.int", + "numsIntAlias:ints", + "numUint:uint", + "numsUint:array.uint", + "numsUintAlias:uints", + "textString:string", + "textStrings:array.string", + "textStringsAlias:strings", + "textCoin:coin", + "textCoins:array.coin", + "textCoinsAlias:coins", + "--module", + "example", + ), step.Workdir(path), )), )) + env.Must(env.Exec("create a map with index", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "map", + "map_with_index", + "email", + "emailIds:ints", + "--index", + "foo:string,bar:int,foobar:uint,barFoo:bool", + "--module", + "example", + ), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("create a map with invalid index", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "map", + "map_with_invalid_index", + "email", + "--index", + "foo:strings,bar:ints", + "--module", + "example", + ), + step.Workdir(path), + )), + envtest.ExecShouldError(), + )) + env.Must(env.Exec("create a message and a map with no-message flag to check conflicts", step.NewSteps(step.New( step.Exec("starport", "s", "message", "create-scavenge", "description"), @@ -93,7 +148,7 @@ func TestCreateMapWithStargate(t *testing.T) { step.Exec("starport", "s", "map", "map_with_duplicated_index", "email", "--index", "foo,foo"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("should prevent creating a map with an index present in fields", @@ -101,7 +156,7 @@ func TestCreateMapWithStargate(t *testing.T) { step.Exec("starport", "s", "map", "map_with_invalid_index", "email", "--index", "email"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.EnsureAppIsSteady(path) diff --git a/integration/cmd_message_test.go b/integration/other_components/cmd_message_test.go similarity index 79% rename from integration/cmd_message_test.go rename to integration/other_components/cmd_message_test.go index b959abf9f6..286389eb76 100644 --- a/integration/cmd_message_test.go +++ b/integration/other_components/cmd_message_test.go @@ -1,18 +1,19 @@ //go:build !relayer // +build !relayer -package integration_test +package other_components_test import ( "path/filepath" "testing" + "github.com/tendermint/starport/integration" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" ) func TestGenerateAnAppWithMessage(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog") ) @@ -57,7 +58,7 @@ func TestGenerateAnAppWithMessage(t *testing.T) { step.Exec("starport", "s", "message", "do-foo", "bar"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("create a message with a custom signer name", @@ -69,7 +70,23 @@ func TestGenerateAnAppWithMessage(t *testing.T) { env.Must(env.Exec("create a custom field type", step.NewSteps(step.New( - step.Exec("starport", "s", "type", "custom-type", "customField:uint"), + step.Exec("starport", + "s", + "type", + "custom-type", + "numInt:int", + "numsInt:array.int", + "numsIntAlias:ints", + "numUint:uint", + "numsUint:array.uint", + "numsUintAlias:uints", + "textString:string", + "textStrings:array.string", + "textStringsAlias:strings", + "textCoin:coin", + "textCoins:array.coin", + "textCoinsAlias:coins", + ), step.Workdir(path), )), )) @@ -96,6 +113,7 @@ func TestGenerateAnAppWithMessage(t *testing.T) { "message", "do-foo", "text", + "userIds:array.uint", "--module", "foo", "--desc", diff --git a/integration/cmd_query_test.go b/integration/other_components/cmd_query_test.go similarity index 68% rename from integration/cmd_query_test.go rename to integration/other_components/cmd_query_test.go index dd33b7dfee..0d0f346275 100644 --- a/integration/cmd_query_test.go +++ b/integration/other_components/cmd_query_test.go @@ -1,15 +1,19 @@ -package integration_test +//go:build !relayer +// +build !relayer + +package other_components_test import ( "path/filepath" "testing" + "github.com/tendermint/starport/integration" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" ) func TestGenerateAnAppWithQuery(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog") ) @@ -69,16 +73,40 @@ func TestGenerateAnAppWithQuery(t *testing.T) { env.Must(env.Exec("create a custom field type", step.NewSteps(step.New( - step.Exec("starport", "s", "type", "custom-type", "customField:uint"), + step.Exec("starport", + "s", + "type", + "custom-type", + "numInt:int", + "numsInt:array.int", + "numsIntAlias:ints", + "numUint:uint", + "numsUint:array.uint", + "numsUintAlias:uints", + "textString:string", + "textStrings:array.string", + "textStringsAlias:strings", + "textCoin:coin", + "textCoins:array.coin", + "textCoinsAlias:coins", + ), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("create a query with the custom field type as a response", + step.NewSteps(step.New( + step.Exec("starport", "s", "query", "foobaz", "-r", "bar:CustomType"), step.Workdir(path), )), )) - env.Must(env.Exec("create a query with the custom field type", + env.Must(env.Exec("should prevent using custom type in request params", step.NewSteps(step.New( - step.Exec("starport", "s", "query", "foobaz", "bar:CustomType"), + step.Exec("starport", "s", "query", "bur", "bar:CustomType"), step.Workdir(path), )), + envtest.ExecShouldError(), )) env.Must(env.Exec("create an empty query", @@ -93,7 +121,7 @@ func TestGenerateAnAppWithQuery(t *testing.T) { step.Exec("starport", "s", "query", "foo", "bar"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("create a module", diff --git a/integration/readme.md b/integration/readme.md new file mode 100644 index 0000000000..63a8c67024 --- /dev/null +++ b/integration/readme.md @@ -0,0 +1,46 @@ +# Starport Integration Tests + +The Starport integration tests build a new application and run all Starport commands to check the Starport code integrity. The runners and helper methods are located in this current folder. The test commands are split into folders, for better concurrency, each folder is a parallel job into the CI workflow. To create a new one, we only need to create a new folder. This will be automatically detected and added into the PR CI checks, or we can only create new tests into an existing folder or file. + +Running synchronously all integration tests can be very slow. The command below can run everything: +```shell +go test -v -timeout 120m ./integration +``` + +Or you can just run a specific test folder, like the `list` types test +```shell +go test -v -timeout 120m ./integration/list +``` + +# Usage + +- Create a new env and scaffold an empty chain: +```go +var ( + env = envtest.New(t) + path = env.Scaffold("blog") +) +``` + +- Now, you can use the env to run the starport commands and check the success status: +```go +env.Must(env.Exec("create a list with bool", + step.NewSteps(step.New( + step.Exec("starport", "s", "list", "document", "signed:bool"), + step.Workdir(path), + )), +)) +env.EnsureAppIsSteady(path) +``` + +- To check if the command returns an error, you can add the `envtest.ExecShouldError()` step: +```go +env.Must(env.Exec("should prevent creating a list with duplicated fields", + step.NewSteps(step.New( + step.Exec("starport", "s", "list", "company", "name", "name"), + step.Workdir(path), + )), + envtest.ExecShouldError(), +)) +env.EnsureAppIsSteady(path) +``` \ No newline at end of file diff --git a/integration/simulation/simapp_test.go b/integration/simulation/simapp_test.go new file mode 100644 index 0000000000..071bc3664d --- /dev/null +++ b/integration/simulation/simapp_test.go @@ -0,0 +1,63 @@ +//go:build !relayer +// +build !relayer + +package simulation_test + +import ( + "testing" + + "github.com/tendermint/starport/integration" + "github.com/tendermint/starport/starport/pkg/cmdrunner/step" +) + +func TestGenerateAnAppAndSimulate(t *testing.T) { + var ( + env = envtest.New(t) + path = env.Scaffold("blog") + ) + + env.Must(env.Exec("create a list", + step.NewSteps(step.New( + step.Exec("starport", "s", "list", "foo", "foobar"), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("create an singleton type", + step.NewSteps(step.New( + step.Exec("starport", "s", "single", "baz", "foobar"), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("create a message", + step.NewSteps(step.New( + step.Exec("starport", "s", "msgFoo", "foobar"), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("scaffold a new module", + step.NewSteps(step.New( + step.Exec("starport", "s", "module", "new_module"), + step.Workdir(path), + )), + )) + + env.Must(env.Exec("create a map", + step.NewSteps(step.New( + step.Exec( + "starport", + "s", + "map", + "bar", + "foobar", + "--module", + "new_module", + ), + step.Workdir(path), + )), + )) + + env.Simulate(path, 100, 50) +} diff --git a/integration/cmd_singleton_test.go b/integration/single/cmd_singleton_test.go similarity index 94% rename from integration/cmd_singleton_test.go rename to integration/single/cmd_singleton_test.go index 5a4c1dc27a..a7d318fb18 100644 --- a/integration/cmd_singleton_test.go +++ b/integration/single/cmd_singleton_test.go @@ -1,18 +1,19 @@ //go:build !relayer // +build !relayer -package integration_test +package single_test import ( "path/filepath" "testing" + "github.com/tendermint/starport/integration" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" ) func TestCreateSingletonWithStargate(t *testing.T) { var ( - env = newEnv(t) + env = envtest.New(t) path = env.Scaffold("blog") ) @@ -63,7 +64,7 @@ func TestCreateSingletonWithStargate(t *testing.T) { step.Exec("starport", "s", "single", "user", "email", "--module", "example"), step.Workdir(path), )), - ExecShouldError(), + envtest.ExecShouldError(), )) env.Must(env.Exec("create an singleton type in a custom module", diff --git a/readme.md b/readme.md index c817b5da26..e952f2d412 100644 --- a/readme.md +++ b/readme.md @@ -1,9 +1,11 @@ -# ![Starport](./assets/starport.jpg) +# Starport -Starport is the all-in-one platform to build, launch and maintain any crypto application on a sovereign and secured blockchain. It is a developer-friendly interface to the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk), the world's most widely-used blockchain application framework. Starport generates boilerplate code for you, so you can focus on writing business logic. +![Starport](./assets/starport.png) + +[Starport](https://starport.com) is the all-in-one platform to build, launch, and maintain any crypto application on a sovereign and secured blockchain. It is a developer-friendly interface to the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk), the world's most widely-used blockchain application framework. Starport generates boilerplate code for you, so you can focus on writing business logic. * [**Build a blockchain with Starport in a web-based IDE** (stable)](https://gitpod.io/#https://github.com/tendermint/starport/tree/master) or use [nightly version](https://gitpod.io/#https://github.com/tendermint/starport/) -* [Check out the latest features in v0.17](https://medium.com/tendermint/starport-v0-17-streamlined-cli-improvements-to-scaffolding-a5332e5fb4ed) +* [Check out the latest features in v0.18](https://medium.com/tendermint/starport-v0-18-cosmos-sdk-updates-and-scaffolding-enhancements-5ea5654bcd0c) ## Quick start @@ -19,22 +21,43 @@ starport chain serve ## Documentation -To learn how to use Starport, check out the [Starport Documentation](https://docs.starport.network). To learn more about how to customize your blockchain see `config.yml` [reference](https://docs.starport.network/kb/config.html). To install Starport locally on GNU/Linux or macOS, follow [these steps](https://docs.starport.network/guide/install.html). +To learn how to use Starport, check out the [Starport Documentation](https://docs.starport.com). To learn more about how to build blockchain apps with Starport, see the [Starport Developer Tutorials](https://docs.starport.com/guide/). + +To install Starport locally on GNU, Linux, or macOS, see [Install Starport](https://docs.starport.com/guide/install.html). -To learn more about building a JavaScript frontend for your Cosmos SDK blockchain, see [`tendermint/vue`](https://github.com/tendermint/vue). +To learn more about building a JavaScript frontend for your Cosmos SDK blockchain, see [tendermint/vue](https://github.com/tendermint/vue). ## Questions -For questions and support please join the `cosmos-sdk-starport` channel in the [Cosmos Community Discord](https://discord.com/invite/W8trcGV). The issue list of this repo is exclusively for bug reports and feature requests. +For questions and support, join the official [Starport Discord](https://discord.gg/starport) server. The issue list in this repo is exclusively for bug reports and feature requests. + +## Cosmos SDK Compatibility + +Blockchains created with Starport use the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk/) framework. To ensure the best possible experience, use the version of Starport that corresponds to the version of Cosmos SDK that your blockchain is built with. Unless noted otherwise, a row refers to a minor version and all associated patch versions. + +| Starport | Cosmos SDK | Notes | +| -------- | ---------- | ------------------------------------------------ | +| v0.18 | v0.44 | `starport chain serve` works with v0.44.x chains | | | +| v0.17 | v0.42 | | + +To upgrade your blockchain to the newer version of Cosmos SDK, see the [Migration guide](https://docs.starport.com/migration/). ## Contributing -We welcome contributions from everyone. The `develop` branch contains the development version of the code. You can branch of from `develop` and create a pull request, or maintain your own fork and submit a cross-repository pull request. If you're not sure where to start check out [contributing.md](contributing.md) for our guidelines & policies for how we develop Starport. Thank you to all those who have contributed to Starport! +We welcome contributions from everyone. The `develop` branch contains the development version of the code. You can create a branch from `develop` and create a pull request, or maintain your own fork and submit a cross-repository pull request. + +**Important** Before you start implementing a new Starport feature, the first step is to create an issue on Github that describes the proposed changes. + +If you're not sure where to start, check out [contributing.md](contributing.md) for our guidelines and policies for how we develop Starport. Thank you to everyone who has contributed to Starport! -## Stay in touch +## Community -Starport is a free and open source product maintained by [Tendermint](https://tendermint.com). Follow us to get the latest updates! +Starport is a free and open source product maintained by [Tendermint](https://tendermint.com). Here's where you can find us. Stay in touch. -- [Twitter](https://twitter.com/starportHQ) -- [Blog](https://medium.com/tendermint) -- [Jobs](https://tendermint.com/careers) +- [Starport.com website](https://starport.com) +- [@StarportHQ on Twitter](https://twitter.com/StarportHQ) +- [Starport.com/blog](https://starport.com/blog/) +- [Starport Discord](https://discord.com/starport) +- [Starport YouTube](https://www.youtube.com/channel/UCXMndYLK7OuvjvElSeSWJ1Q) +- [Starport docs](https://docs.starport.com/) +- [Tendermint jobs](https://tendermint.com/careers) diff --git a/scripts/gen-protoc-gen-dart b/scripts/gen-protoc-gen-dart index 528ca04982..a0d6ed52c7 100755 --- a/scripts/gen-protoc-gen-dart +++ b/scripts/gen-protoc-gen-dart @@ -9,7 +9,7 @@ [[ ${dep_check} = "false" ]] && { echo "Some dependencie(s) isn't installed yet. Please install that dependencie(s)" ; exit 1 ;} ## Variables -protobuf_src="https://github.com/google/protobuf.dart/archive/refs/tags/protoc_plugin-19.2.0+1.tar.gz" +protobuf_src="https://github.com/google/protobuf.dart/archive/refs/tags/protoc_plugin-v20.0.0.tar.gz" setdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd)" # this line powered by stackoverflow kernelname="$(uname -s | tr '[:upper:]' '[:lower:]' || { echo 'kernel name can not definied' ; exit 1 ;})" diff --git a/starport/chainconf/config.go b/starport/chainconfig/config.go similarity index 81% rename from starport/chainconf/config.go rename to starport/chainconfig/config.go index d2da47f17f..5b45d88eae 100644 --- a/starport/chainconf/config.go +++ b/starport/chainconfig/config.go @@ -1,4 +1,4 @@ -package conf +package chainconfig import ( "errors" @@ -9,40 +9,48 @@ import ( "github.com/goccy/go-yaml" "github.com/imdario/mergo" + "github.com/tendermint/starport/starport/pkg/xfilepath" +) + +var ( + // ConfigDirPath returns the path of configuration directory of Starport. + ConfigDirPath = xfilepath.JoinFromHome(xfilepath.Path(".starport")) + + // ConfigFileNames is a list of recognized names as for Starport's config file. + ConfigFileNames = []string{"config.yml", "config.yaml"} ) var ( // ErrCouldntLocateConfig returned when config.yml cannot be found in the source code. - ErrCouldntLocateConfig = errors.New("could not locate a config.yml in your chain. please follow the link for how-to: https://github.com/tendermint/starport/blob/develop/docs/configure/index.md") - - // FileNames holds a list of appropriate names for the config file. - FileNames = []string{"config.yml", "config.yaml"} - - // DefaultConf holds default configuration. - DefaultConf = Config{ - Host: Host{ - // when in Docker on MacOS, it only works with 0.0.0.0. - RPC: "0.0.0.0:26657", - P2P: "0.0.0.0:26656", - Prof: "0.0.0.0:6060", - GRPC: "0.0.0.0:9090", - GRPCWeb: "0.0.0.0:9091", - API: "0.0.0.0:1317", - }, - Build: Build{ - Proto: Proto{ - Path: "proto", - ThirdPartyPaths: []string{ - "third_party/proto", - "proto_vendor", - }, + ErrCouldntLocateConfig = errors.New( + "could not locate a config.yml in your chain. please follow the link for" + + "how-to: https://github.com/tendermint/starport/blob/develop/docs/configure/index.md") +) + +// DefaultConf holds default configuration. +var DefaultConf = Config{ + Host: Host{ + // when in Docker on MacOS, it only works with 0.0.0.0. + RPC: "0.0.0.0:26657", + P2P: "0.0.0.0:26656", + Prof: "0.0.0.0:6060", + GRPC: "0.0.0.0:9090", + GRPCWeb: "0.0.0.0:9091", + API: "0.0.0.0:1317", + }, + Build: Build{ + Proto: Proto{ + Path: "proto", + ThirdPartyPaths: []string{ + "third_party/proto", + "proto_vendor", }, }, - Faucet: Faucet{ - Host: "0.0.0.0:4500", - }, - } -) + }, + Faucet: Faucet{ + Host: "0.0.0.0:4500", + }, +} // Config is the user given configuration to do additional setup // during serve. @@ -73,6 +81,7 @@ type Account struct { Coins []string `yaml:"coins,omitempty"` Mnemonic string `yaml:"mnemonic,omitempty"` Address string `yaml:"address,omitempty"` + CoinType string `yaml:"cointype,omitempty"` // The RPCAddress off the chain that account is issued at. RPCAddress string `yaml:"rpc_address,omitempty"` @@ -224,7 +233,7 @@ func (e *ValidationError) Error() string { // LocateDefault locates the default path for the config file, if no file found returns ErrCouldntLocateConfig. func LocateDefault(root string) (path string, err error) { - for _, name := range FileNames { + for _, name := range ConfigFileNames { path = filepath.Join(root, name) if _, err := os.Stat(path); err == nil { return path, nil @@ -246,3 +255,13 @@ func FaucetHost(conf Config) string { return host } + +// CreateConfigDir creates config directory if it is not created yet. +func CreateConfigDir() error { + confPath, err := ConfigDirPath() + if err != nil { + return err + } + + return os.MkdirAll(confPath, 0755) +} diff --git a/starport/chainconf/config_test.go b/starport/chainconfig/config_test.go similarity index 65% rename from starport/chainconf/config_test.go rename to starport/chainconfig/config_test.go index 57bdf7a888..f6c19b0903 100644 --- a/starport/chainconf/config_test.go +++ b/starport/chainconfig/config_test.go @@ -1,4 +1,4 @@ -package conf +package chainconfig import ( "strings" @@ -20,6 +20,7 @@ validator: ` conf, err := Parse(strings.NewReader(confyml)) + require.NoError(t, err) require.Equal(t, []Account{ { @@ -37,6 +38,43 @@ validator: }, conf.Validator) } +func TestCoinTypeParse(t *testing.T) { + confyml := ` +accounts: + - name: me + coins: ["1000token", "100000000stake"] + mnemonic: ozone unfold device pave lemon potato omit insect column wise cover hint narrow large provide kidney episode clay notable milk mention dizzy muffin crazy + cointype: 7777777 + - name: you + coins: ["5000token"] + cointype: 123456 +validator: + name: user1 + staked: "100000000stake" +` + + conf, err := Parse(strings.NewReader(confyml)) + + require.NoError(t, err) + require.Equal(t, []Account{ + { + Name: "me", + Coins: []string{"1000token", "100000000stake"}, + Mnemonic: "ozone unfold device pave lemon potato omit insect column wise cover hint narrow large provide kidney episode clay notable milk mention dizzy muffin crazy", + CoinType: "7777777", + }, + { + Name: "you", + Coins: []string{"5000token"}, + CoinType: "123456", + }, + }, conf.Accounts) + require.Equal(t, Validator{ + Name: "user1", + Staked: "100000000stake", + }, conf.Validator) +} + func TestParseInvalid(t *testing.T) { confyml := ` accounts: diff --git a/starport/cmd/account.go b/starport/cmd/account.go index eba6f56463..b8bebe1987 100644 --- a/starport/cmd/account.go +++ b/starport/cmd/account.go @@ -1,14 +1,13 @@ package starportcmd import ( - "fmt" "os" - "text/tabwriter" "github.com/spf13/cobra" flag "github.com/spf13/pflag" "github.com/tendermint/starport/starport/pkg/cliquiz" "github.com/tendermint/starport/starport/pkg/cosmosaccount" + "github.com/tendermint/starport/starport/pkg/entrywriter" ) const ( @@ -16,6 +15,7 @@ const ( flagPassphrase = "passphrase" flagNonInteractive = "non-interactive" flagKeyringBackend = "keyring-backend" + flagFrom = "from" ) func NewAccount() *cobra.Command { @@ -38,26 +38,12 @@ Starport uses accounts to interact with the Starport Network blockchain, use an return c } -func printAccounts(cmd *cobra.Command, accounts ...cosmosaccount.Account) { - w := &tabwriter.Writer{} - w.Init(os.Stdout, 0, 8, 0, '\t', 0) - - if len(accounts) == 0 { - return - } - - fmt.Fprintln(w, "name\taddress\tpublic key") - +func printAccounts(cmd *cobra.Command, accounts ...cosmosaccount.Account) error { + var accEntries [][]string for _, acc := range accounts { - fmt.Fprintf(w, "%s\t%s\t%s\n", - acc.Name, - acc.Address(getAddressPrefix(cmd)), - acc.PubKey(), - ) + accEntries = append(accEntries, []string{acc.Name, acc.Address(getAddressPrefix(cmd)), acc.PubKey()}) } - - fmt.Fprintln(w) - w.Flush() + return entrywriter.MustWrite(os.Stdout, []string{"name", "address", "public key"}, accEntries...) } func flagSetKeyringBackend() *flag.FlagSet { @@ -82,6 +68,11 @@ func getAddressPrefix(cmd *cobra.Command) string { return prefix } +func getFrom(cmd *cobra.Command) string { + prefix, _ := cmd.Flags().GetString(flagFrom) + return prefix +} + func flagSetAccountImportExport() *flag.FlagSet { fs := flag.NewFlagSet("", flag.ContinueOnError) fs.Bool(flagNonInteractive, false, "Do not enter into interactive mode") diff --git a/starport/cmd/account_list.go b/starport/cmd/account_list.go index 93479abc83..2ced4c3b65 100644 --- a/starport/cmd/account_list.go +++ b/starport/cmd/account_list.go @@ -31,6 +31,5 @@ func accountListHandler(cmd *cobra.Command, args []string) error { return err } - printAccounts(cmd, accounts...) - return nil + return printAccounts(cmd, accounts...) } diff --git a/starport/cmd/account_show.go b/starport/cmd/account_show.go index 301fc47094..14d2af42ac 100644 --- a/starport/cmd/account_show.go +++ b/starport/cmd/account_show.go @@ -34,6 +34,5 @@ func accountShowHandler(cmd *cobra.Command, args []string) error { return err } - printAccounts(cmd, acc) - return nil + return printAccounts(cmd, acc) } diff --git a/starport/cmd/chain.go b/starport/cmd/chain.go index 8995c4b41c..b72ddada6e 100644 --- a/starport/cmd/chain.go +++ b/starport/cmd/chain.go @@ -13,11 +13,13 @@ func NewChain() *cobra.Command { Args: cobra.ExactArgs(1), } - flagSetPath(c) - c.AddCommand(NewChainServe()) - c.AddCommand(NewChainBuild()) - c.AddCommand(NewChainInit()) - c.AddCommand(NewChainFaucet()) + c.AddCommand( + NewChainServe(), + NewChainBuild(), + NewChainInit(), + NewChainFaucet(), + NewChainSimulate(), + ) return c } diff --git a/starport/cmd/chain_build.go b/starport/cmd/chain_build.go index 632ba15df8..b4314897bf 100644 --- a/starport/cmd/chain_build.go +++ b/starport/cmd/chain_build.go @@ -30,12 +30,13 @@ source. Specify the release targets with GOOS:GOARCH build tags. If the optional --release.targets is not specified, a binary is created for your current environment. Sample usages: - - starport build - - starport build --release -t linux:amd64 -t darwin:amd64 -t darwin:arm64`, - Args: cobra.ExactArgs(0), + - starport chain build + - starport chain build --release -t linux:amd64 -t darwin:amd64 -t darwin:arm64`, + Args: cobra.NoArgs, RunE: chainBuildHandler, } + flagSetPath(c) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetProto3rdParty("Available only without the --release flag")) c.Flags().Bool(flagRelease, false, "build for a release") diff --git a/starport/cmd/chain_faucet.go b/starport/cmd/chain_faucet.go index 07bca20f26..ad1db5efe4 100644 --- a/starport/cmd/chain_faucet.go +++ b/starport/cmd/chain_faucet.go @@ -19,6 +19,7 @@ func NewChainFaucet() *cobra.Command { RunE: chainFaucetHandler, } + flagSetPath(c) c.Flags().AddFlagSet(flagSetHome()) c.Flags().BoolP("verbose", "v", false, "Verbose output") diff --git a/starport/cmd/chain_init.go b/starport/cmd/chain_init.go index 0acdb4a5dc..78cd31b7dc 100644 --- a/starport/cmd/chain_init.go +++ b/starport/cmd/chain_init.go @@ -12,10 +12,11 @@ func NewChainInit() *cobra.Command { c := &cobra.Command{ Use: "init", Short: "Initialize your chain", - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, RunE: chainInitHandler, } + flagSetPath(c) c.Flags().AddFlagSet(flagSetHome()) return c diff --git a/starport/cmd/chain_serve.go b/starport/cmd/chain_serve.go index f685c4b14c..e6ea94a04a 100644 --- a/starport/cmd/chain_serve.go +++ b/starport/cmd/chain_serve.go @@ -17,10 +17,11 @@ func NewChainServe() *cobra.Command { Use: "serve", Short: "Start a blockchain node in development", Long: "Start a blockchain node with automatic reloading", - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, RunE: chainServeHandler, } + flagSetPath(c) c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetProto3rdParty("")) c.Flags().BoolP("verbose", "v", false, "Verbose output") diff --git a/starport/cmd/chain_simulate.go b/starport/cmd/chain_simulate.go new file mode 100644 index 0000000000..b74a986deb --- /dev/null +++ b/starport/cmd/chain_simulate.go @@ -0,0 +1,129 @@ +package starportcmd + +import ( + "path/filepath" + + "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/services/chain" +) + +const ( + flagSimappGenesis = "genesis" + flagSimappParams = "params" + flagSimappExportParamsPath = "exportParamsPath" + flagSimappExportParamsHeight = "exportParamsHeight" + flagSimappExportStatePath = "exportStatePath" + flagSimappExportStatsPath = "exportStatsPath" + flagSimappSeed = "seed" + flagSimappInitialBlockHeight = "initialBlockHeight" + flagSimappNumBlocks = "numBlocks" + flagSimappBlockSize = "blockSize" + flagSimappLean = "lean" + flagSimappSimulateEveryOperation = "simulateEveryOperation" + flagSimappPrintAllInvariants = "printAllInvariants" + flagSimappVerbose = "verbose" + flagSimappPeriod = "period" + flagSimappGenesisTime = "genesisTime" +) + +// NewChainSimulate creates a new simulation command to run the blockchain simulation. +func NewChainSimulate() *cobra.Command { + c := &cobra.Command{ + Use: "simulate", + Short: "Run simulation testing for the blockchain", + Long: "Run simulation testing for the blockchain. It sends many randomized-input messages of each module to a simulated node and checks if invariants break", + Args: cobra.NoArgs, + RunE: chainSimulationHandler, + } + simappFlags(c) + return c +} + +func chainSimulationHandler(cmd *cobra.Command, args []string) error { + var ( + verbose, _ = cmd.Flags().GetBool(flagSimappVerbose) + period, _ = cmd.Flags().GetUint(flagSimappPeriod) + genesisTime, _ = cmd.Flags().GetInt64(flagSimappGenesisTime) + config = newConfigFromFlags(cmd) + appPath = flagGetPath(cmd) + ) + // create the chain with path + absPath, err := filepath.Abs(appPath) + if err != nil { + return err + } + c, err := chain.New(absPath) + if err != nil { + return err + } + + config.ChainID, err = c.ID() + if err != nil { + return err + } + + return c.Simulate(cmd.Context(), + chain.SimappWithVerbose(verbose), + chain.SimappWithPeriod(period), + chain.SimappWithGenesisTime(genesisTime), + chain.SimappWithConfig(config), + ) +} + +// newConfigFromFlags creates a simulation from the retrieved values of the flags. +func newConfigFromFlags(cmd *cobra.Command) simulation.Config { + var ( + genesis, _ = cmd.Flags().GetString(flagSimappGenesis) + params, _ = cmd.Flags().GetString(flagSimappParams) + exportParamsPath, _ = cmd.Flags().GetString(flagSimappExportParamsPath) + exportParamsHeight, _ = cmd.Flags().GetInt(flagSimappExportParamsHeight) + exportStatePath, _ = cmd.Flags().GetString(flagSimappExportStatePath) + exportStatsPath, _ = cmd.Flags().GetString(flagSimappExportStatsPath) + seed, _ = cmd.Flags().GetInt64(flagSimappSeed) + initialBlockHeight, _ = cmd.Flags().GetInt(flagSimappInitialBlockHeight) + numBlocks, _ = cmd.Flags().GetInt(flagSimappNumBlocks) + blockSize, _ = cmd.Flags().GetInt(flagSimappBlockSize) + lean, _ = cmd.Flags().GetBool(flagSimappLean) + simulateEveryOperation, _ = cmd.Flags().GetBool(flagSimappSimulateEveryOperation) + printAllInvariants, _ = cmd.Flags().GetBool(flagSimappPrintAllInvariants) + ) + return simulation.Config{ + Commit: true, + GenesisFile: genesis, + ParamsFile: params, + ExportParamsPath: exportParamsPath, + ExportParamsHeight: exportParamsHeight, + ExportStatePath: exportStatePath, + ExportStatsPath: exportStatsPath, + Seed: seed, + InitialBlockHeight: initialBlockHeight, + NumBlocks: numBlocks, + BlockSize: blockSize, + Lean: lean, + OnOperation: simulateEveryOperation, + AllInvariants: printAllInvariants, + } +} + +func simappFlags(c *cobra.Command) { + // config fields + c.Flags().String(flagSimappGenesis, "", "custom simulation genesis file; cannot be used with params file") + c.Flags().String(flagSimappParams, "", "custom simulation params file which overrides any random params; cannot be used with genesis") + c.Flags().String(flagSimappExportParamsPath, "", "custom file path to save the exported params JSON") + c.Flags().Int(flagSimappExportParamsHeight, 0, "height to which export the randomly generated params") + c.Flags().String(flagSimappExportStatePath, "", "custom file path to save the exported app state JSON") + c.Flags().String(flagSimappExportStatsPath, "", "custom file path to save the exported simulation statistics JSON") + c.Flags().Int64(flagSimappSeed, 42, "simulation random seed") + c.Flags().Int(flagSimappInitialBlockHeight, 1, "initial block to start the simulation") + c.Flags().Int(flagSimappNumBlocks, 200, "number of new blocks to simulate from the initial block height") + c.Flags().Int(flagSimappBlockSize, 30, "operations per block") + c.Flags().Bool(flagSimappLean, false, "lean simulation log output") + c.Flags().Bool(flagSimappSimulateEveryOperation, false, "run slow invariants every operation") + c.Flags().Bool(flagSimappPrintAllInvariants, false, "print all invariants if a broken invariant is found") + + // simulation flags + c.Flags().BoolP(flagSimappVerbose, "v", false, "verbose log output") + c.Flags().Uint(flagSimappPeriod, 0, "run slow invariants only once every period assertions") + c.Flags().Int64(flagSimappGenesisTime, 0, "override genesis UNIX time instead of using a random UNIX time") +} diff --git a/starport/cmd/cmd.go b/starport/cmd/cmd.go index 4c5278a919..ccfabf1082 100644 --- a/starport/cmd/cmd.go +++ b/starport/cmd/cmd.go @@ -7,13 +7,17 @@ import ( "path/filepath" "sort" "strings" + "sync" "time" "github.com/fatih/color" "github.com/spf13/cobra" flag "github.com/spf13/pflag" "github.com/tendermint/starport/starport/internal/version" + "github.com/tendermint/starport/starport/pkg/clispinner" + "github.com/tendermint/starport/starport/pkg/cosmosaccount" "github.com/tendermint/starport/starport/pkg/cosmosver" + "github.com/tendermint/starport/starport/pkg/events" "github.com/tendermint/starport/starport/pkg/gitpod" "github.com/tendermint/starport/starport/pkg/goenv" "github.com/tendermint/starport/starport/pkg/xgenny" @@ -25,6 +29,7 @@ const ( flagPath = "path" flagHome = "home" flagProto3rdParty = "proto-all-modules" + flagYes = "yes" checkVersionTimeout = time.Millisecond * 600 ) @@ -57,6 +62,7 @@ starport scaffold chain github.com/cosmonaut/mars`, c.AddCommand(NewScaffold()) c.AddCommand(NewChain()) c.AddCommand(NewGenerate()) + c.AddCommand(NewNetwork()) c.AddCommand(NewAccount()) c.AddCommand(NewRelayer()) c.AddCommand(NewTools()) @@ -75,6 +81,20 @@ func logLevel(cmd *cobra.Command) chain.LogLvl { return chain.LogRegular } +func printEvents(wg *sync.WaitGroup, bus events.Bus, s *clispinner.Spinner) { + defer wg.Done() + + for event := range bus { + if event.IsOngoing() { + s.SetText(event.Text()) + s.Start() + } else { + s.Stop() + fmt.Printf("%s %s\n", clispinner.OK, event.Description) + } + } +} + func flagSetPath(cmd *cobra.Command) { cmd.PersistentFlags().StringP(flagPath, "p", ".", "path of the app") } @@ -90,17 +110,34 @@ func flagSetHome() *flag.FlagSet { return fs } -func getHomeFlag(cmd *cobra.Command) (home string) { +func flagNetworkFrom() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.String(flagFrom, cosmosaccount.DefaultAccount, "Account name to use for sending transactions to SPN") + return fs +} + +func getHome(cmd *cobra.Command) (home string) { home, _ = cmd.Flags().GetString(flagHome) return } -func flagSetProto3rdParty(additonalInfo string) *flag.FlagSet { +func flagSetYes() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.Bool(flagYes, false, "Answers interactive yes/no questions with yes") + return fs +} + +func getYes(cmd *cobra.Command) (ok bool) { + ok, _ = cmd.Flags().GetBool(flagYes) + return +} + +func flagSetProto3rdParty(additionalInfo string) *flag.FlagSet { fs := flag.NewFlagSet("", flag.ContinueOnError) info := "Enables proto code generation for 3rd party modules used in your chain" - if additonalInfo != "" { - info += ". " + additonalInfo + if additionalInfo != "" { + info += ". " + additionalInfo } fs.Bool(flagProto3rdParty, false, info) @@ -114,7 +151,7 @@ func flagGetProto3rdParty(cmd *cobra.Command) bool { func newChainWithHomeFlags(cmd *cobra.Command, chainOption ...chain.Option) (*chain.Chain, error) { // Check if custom home is provided - if home := getHomeFlag(cmd); home != "" { + if home := getHome(cmd); home != "" { chainOption = append(chainOption, chain.HomePath(home)) } @@ -214,7 +251,7 @@ func checkNewVersion(ctx context.Context) { } fmt.Printf(`· -· 🛸 Starport %q is available! +· 🛸 Starport %s is available! · · If you're looking to upgrade check out the instructions: https://docs.starport.network/guide/install.html#upgrading-your-starport-installation · @@ -240,3 +277,7 @@ https://docs.starport.network/migration`, sc.Version.String(), } return sc, nil } + +func printSection(title string) { + fmt.Printf("------\n%s\n------\n\n", title) +} diff --git a/starport/cmd/docs.go b/starport/cmd/docs.go index 47e7796771..d7117d64d2 100644 --- a/starport/cmd/docs.go +++ b/starport/cmd/docs.go @@ -11,7 +11,7 @@ func NewDocs() *cobra.Command { c := &cobra.Command{ Use: "docs", Short: "Show Starport docs", - Args: cobra.ExactArgs(0), + Args: cobra.NoArgs, RunE: docsHandler, } return c diff --git a/starport/cmd/generate_dart.go b/starport/cmd/generate_dart.go index ff6f6f7b3f..22c8ba1a6d 100644 --- a/starport/cmd/generate_dart.go +++ b/starport/cmd/generate_dart.go @@ -10,10 +10,9 @@ import ( func NewGenerateDart() *cobra.Command { c := &cobra.Command{ - Hidden: true, - Use: "dart", - Short: "Generate a Dart client", - RunE: generateDartHandler, + Use: "dart", + Short: "Generate a Dart client", + RunE: generateDartHandler, } return c } diff --git a/starport/cmd/network.go b/starport/cmd/network.go new file mode 100644 index 0000000000..d42786dc18 --- /dev/null +++ b/starport/cmd/network.go @@ -0,0 +1,176 @@ +package starportcmd + +import ( + "sync" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/clispinner" + "github.com/tendermint/starport/starport/pkg/cosmosaccount" + "github.com/tendermint/starport/starport/pkg/cosmosclient" + "github.com/tendermint/starport/starport/pkg/events" + "github.com/tendermint/starport/starport/pkg/gitpod" + "github.com/tendermint/starport/starport/services/network" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +var ( + nightly bool + local bool + + spnNodeAddress string + spnFaucetAddress string +) + +const ( + flagNightly = "nightly" + flagLocal = "local" + + flagSPNNodeAddress = "spn-node-address" + flagSPNFaucetAddress = "spn-faucet-address" + + spnNodeAddressAlpha = "https://rpc.alpha.starport.network:443" + spnFaucetAddressAlpha = "https://faucet.alpha.starport.network" + + spnNodeAddressNightly = "https://rpc.nightly.starport.network:443" + spnFaucetAddressNightly = "https://faucet.nightly.starport.network" + + spnNodeAddressLocal = "http://0.0.0.0:26657" + spnFaucetAddressLocal = "http://0.0.0.0:4500" +) + +// NewNetwork creates a new network command that holds some other sub commands +// related to creating a new network collaboratively. +func NewNetwork() *cobra.Command { + c := &cobra.Command{ + Use: "network [command]", + Aliases: []string{"n"}, + Short: "Launch a blockchain network in production", + Args: cobra.ExactArgs(1), + Hidden: true, + } + + // configure flags. + c.PersistentFlags().BoolVar(&local, flagLocal, false, "Use local SPN network") + c.PersistentFlags().BoolVar(&nightly, flagNightly, false, "Use nightly SPN network") + c.PersistentFlags().StringVar(&spnNodeAddress, flagSPNNodeAddress, spnNodeAddressAlpha, "SPN node address") + c.PersistentFlags().StringVar(&spnFaucetAddress, flagSPNFaucetAddress, spnFaucetAddressAlpha, "SPN Faucet address") + + // add sub commands. + c.AddCommand( + NewNetworkChain(), + NewNetworkRequest(), + ) + + return c +} + +var cosmos *cosmosclient.Client + +type NetworkBuilder struct { + AccountRegistry cosmosaccount.Registry + Spinner *clispinner.Spinner + + ev events.Bus + wg *sync.WaitGroup + cmd *cobra.Command + cc cosmosclient.Client +} + +func newNetworkBuilder(cmd *cobra.Command) (NetworkBuilder, error) { + var err error + + n := NetworkBuilder{ + Spinner: clispinner.New(), + ev: events.NewBus(), + wg: &sync.WaitGroup{}, + cmd: cmd, + } + + n.wg.Add(1) + go printEvents(n.wg, n.ev, n.Spinner) + + if n.cc, err = getNetworkCosmosClient(cmd); err != nil { + n.Cleanup() + return NetworkBuilder{}, err + } + + n.AccountRegistry = n.cc.AccountRegistry + + return n, nil +} + +func (n NetworkBuilder) Chain(source networkchain.SourceOption, options ...networkchain.Option) (*networkchain.Chain, error) { + options = append(options, networkchain.CollectEvents(n.ev)) + + if home := getHome(n.cmd); home != "" { + options = append(options, networkchain.WithHome(home)) + } + + return networkchain.New(n.cmd.Context(), n.AccountRegistry, source, options...) +} + +func (n NetworkBuilder) Network(options ...network.Option) (network.Network, error) { + options = append(options, network.CollectEvents(n.ev)) + + account, err := cosmos.AccountRegistry.GetByName(getFrom(n.cmd)) + if err != nil { + return network.Network{}, errors.Wrap(err, "make sure that this account exists, use 'starport account -h' to manage accounts") + } + + return network.New(*cosmos, account, options...) +} + +func (n NetworkBuilder) Cleanup() { + n.Spinner.Stop() + n.ev.Shutdown() + n.wg.Wait() +} + +func getNetworkCosmosClient(cmd *cobra.Command) (cosmosclient.Client, error) { + // check preconfigured networks + if nightly && local { + return cosmosclient.Client{}, errors.New("local and nightly networks can't be specified in the same command") + } + if local { + spnNodeAddress = spnNodeAddressLocal + spnFaucetAddress = spnFaucetAddressLocal + } else if nightly { + spnNodeAddress = spnNodeAddressNightly + spnFaucetAddress = spnFaucetAddressNightly + } + + cosmosOptions := []cosmosclient.Option{ + cosmosclient.WithHome(cosmosaccount.KeyringHome), + cosmosclient.WithNodeAddress(spnNodeAddress), + cosmosclient.WithAddressPrefix(networkchain.SPN), + cosmosclient.WithUseFaucet(spnFaucetAddress, networkchain.SPNDenom, 5), + cosmosclient.WithKeyringServiceName(cosmosaccount.KeyringServiceName), + } + + keyringBackend := getKeyringBackend(cmd) + // use test keyring backend on Gitpod in order to prevent prompting for keyring + // password. This happens because Gitpod uses containers. + // + // when not on Gitpod, OS keyring backend is used which only asks password once. + if gitpod.IsOnGitpod() { + keyringBackend = cosmosaccount.KeyringTest + } + cosmosOptions = append(cosmosOptions, cosmosclient.WithKeyringBackend(keyringBackend)) + + // init cosmos client only once on start in order to spnclient to + // reuse unlocked keyring in the following steps. + if cosmos == nil { + client, err := cosmosclient.New(cmd.Context(), cosmosOptions...) + if err != nil { + return cosmosclient.Client{}, err + } + cosmos = &client + } + + if err := cosmos.AccountRegistry.EnsureDefaultAccount(); err != nil { + return cosmosclient.Client{}, err + } + + return *cosmos, nil +} diff --git a/starport/cmd/network_chain.go b/starport/cmd/network_chain.go new file mode 100644 index 0000000000..ffa42d18ee --- /dev/null +++ b/starport/cmd/network_chain.go @@ -0,0 +1,26 @@ +package starportcmd + +import ( + "github.com/spf13/cobra" +) + +// NewNetworkChain creates a new chain command that holds some other +// sub commands related to launching a network for a chain. +func NewNetworkChain() *cobra.Command { + c := &cobra.Command{ + Use: "chain", + Short: "Build networks", + } + + c.AddCommand( + NewNetworkChainList(), + NewNetworkChainPublish(), + NewNetworkChainInit(), + NewNetworkChainJoin(), + NewNetworkChainPrepare(), + NewNetworkChainShow(), + NewNetworkChainLaunch(), + ) + + return c +} diff --git a/starport/cmd/network_chain_init.go b/starport/cmd/network_chain_init.go new file mode 100644 index 0000000000..95412ddd9a --- /dev/null +++ b/starport/cmd/network_chain_init.go @@ -0,0 +1,168 @@ +package starportcmd + +import ( + "fmt" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/cliquiz" + "github.com/tendermint/starport/starport/pkg/clispinner" + "github.com/tendermint/starport/starport/pkg/cosmosaccount" + "github.com/tendermint/starport/starport/services/chain" + "github.com/tendermint/starport/starport/services/network" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +const ( + flagValidatorAccount = "validator-account" + flagValidatorWebsite = "validator-website" + flagValidatorDetails = "validator-details" + flagValidatorSecurityContact = "validator-security-contact" + flagValidatorMoniker = "validator-moniker" + flagValidatorIdentity = "validator-identity" +) + +// NewNetworkChainInit returns a new command to initialize a chain from a published chain ID +func NewNetworkChainInit() *cobra.Command { + c := &cobra.Command{ + Use: "init [launch-id]", + Short: "Initialize a chain from a published chain ID", + Args: cobra.ExactArgs(1), + RunE: networkChainInitHandler, + } + + c.Flags().String(flagValidatorAccount, cosmosaccount.DefaultAccount, "Account for the chain validator") + c.Flags().String(flagValidatorWebsite, "", "Associate a website to the validator") + c.Flags().String(flagValidatorDetails, "", "Provide details about the validator") + c.Flags().String(flagValidatorSecurityContact, "", "Provide a validator security contact email") + c.Flags().String(flagValidatorMoniker, "", "Provide a custom validator moniker") + c.Flags().String(flagValidatorIdentity, "", "Provide a validator identity signature (ex. UPort or Keybase)") + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetHome()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetYes()) + + return c +} + +func networkChainInitHandler(cmd *cobra.Command, args []string) error { + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + // parse launch ID + launchID, err := network.ParseLaunchID(args[0]) + if err != nil { + return err + } + + // check if the provided account for the validator exists. + validatorAccount, _ := cmd.Flags().GetString(flagValidatorAccount) + if _, err = nb.AccountRegistry.GetByName(validatorAccount); err != nil { + return err + } + + // if a chain has already been initialized with this launch ID, we ask for confirmation + // before erasing the directory. + chainHome, exist, err := networkchain.IsChainHomeExist(launchID) + if err != nil { + return err + } + + if !getYes(cmd) && exist { + prompt := promptui.Prompt{ + Label: fmt.Sprintf("The chain has already been initialized under: %s. Would you like to overwrite the home directory", + chainHome, + ), + IsConfirm: true, + } + nb.Spinner.Stop() + if _, err := prompt.Run(); err != nil { + fmt.Println("said no") + return nil + } + nb.Spinner.Start() + } + + n, err := nb.Network() + if err != nil { + return err + } + + chainLaunch, err := n.ChainLaunch(cmd.Context(), launchID) + if err != nil { + return err + } + + c, err := nb.Chain(networkchain.SourceLaunch(chainLaunch)) + if err != nil { + return err + } + + if err := c.Init(cmd.Context()); err != nil { + return err + } + + // ask validator information. + v, err := askValidatorInfo(cmd) + if err != nil { + return err + } + + gentxPath, err := c.InitAccount(cmd.Context(), v, validatorAccount) + if err != nil { + return err + } + fmt.Printf("%s Gentx generated: %s\n", clispinner.Bullet, gentxPath) + + return nil +} + +// askValidatorInfo prompts to the user questions to query validator information +func askValidatorInfo(cmd *cobra.Command) (chain.Validator, error) { + var ( + account, _ = cmd.Flags().GetString(flagValidatorAccount) + website, _ = cmd.Flags().GetString(flagValidatorWebsite) + details, _ = cmd.Flags().GetString(flagValidatorDetails) + securityContact, _ = cmd.Flags().GetString(flagValidatorSecurityContact) + moniker, _ = cmd.Flags().GetString(flagValidatorMoniker) + identity, _ = cmd.Flags().GetString(flagValidatorIdentity) + ) + + v := chain.Validator{ + Name: account, + Website: website, + Details: details, + Moniker: moniker, + Identity: identity, + SecurityContact: securityContact, + GasPrices: "0stake", + MinSelfDelegation: "1", + } + + questions := append([]cliquiz.Question{}, + cliquiz.NewQuestion("Staking amount", + &v.StakingAmount, + cliquiz.DefaultAnswer("95000000stake"), + cliquiz.Required(), + ), + cliquiz.NewQuestion("Commission rate", + &v.CommissionRate, + cliquiz.DefaultAnswer("0.10"), + cliquiz.Required(), + ), + cliquiz.NewQuestion("Commission max rate", + &v.CommissionMaxRate, + cliquiz.DefaultAnswer("0.20"), + cliquiz.Required(), + ), + cliquiz.NewQuestion("Commission max change rate", + &v.CommissionMaxChangeRate, + cliquiz.DefaultAnswer("0.01"), + cliquiz.Required(), + ), + ) + return v, cliquiz.Ask(questions...) +} diff --git a/starport/cmd/network_chain_join.go b/starport/cmd/network_chain_join.go new file mode 100644 index 0000000000..d4f4d8a80e --- /dev/null +++ b/starport/cmd/network_chain_join.go @@ -0,0 +1,103 @@ +package starportcmd + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/pkg/errors" + "github.com/rdegges/go-ipify" + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/cliquiz" + "github.com/tendermint/starport/starport/pkg/clispinner" + "github.com/tendermint/starport/starport/pkg/xchisel" + "github.com/tendermint/starport/starport/services/network" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +const ( + flagGentx = "gentx" +) + +// NewNetworkChainJoin creates a new chain join command to join +// to a network as a validator. +func NewNetworkChainJoin() *cobra.Command { + c := &cobra.Command{ + Use: "join [launch-id] [amount]", + Short: "Request to join a network as a validator", + Args: cobra.ExactArgs(2), + RunE: networkChainJoinHandler, + } + c.Flags().String(flagGentx, "", "Path to a gentx json file") + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) + return c +} + +func networkChainJoinHandler(cmd *cobra.Command, args []string) error { + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + // parse launch ID. + launchID, err := network.ParseLaunchID(args[0]) + if err != nil { + return err + } + + // parse the amount. + amount, err := sdk.ParseCoinNormalized(args[1]) + if err != nil { + return errors.Wrap(err, "error parsing amount") + } + + gentxPath, _ := cmd.Flags().GetString(flagGentx) + + // get the peer public address for the validator. + publicAddr, err := askPublicAddress(nb.Spinner) + if err != nil { + return err + } + + n, err := nb.Network() + if err != nil { + return err + } + + chainLaunch, err := n.ChainLaunch(cmd.Context(), launchID) + if err != nil { + return err + } + + c, err := nb.Chain(networkchain.SourceLaunch(chainLaunch)) + if err != nil { + return err + } + + // create the message to add the validator. + return n.Join(cmd.Context(), c, launchID, amount, publicAddr, gentxPath) +} + +// askPublicAddress prepare questions to interactively ask for a publicAddress +// when peer isn't provided and not running through chisel proxy. +func askPublicAddress(s *clispinner.Spinner) (publicAddress string, err error) { + s.Stop() + defer s.Start() + + options := []cliquiz.Option{ + cliquiz.Required(), + } + if !xchisel.IsEnabled() { + ip, _ := ipify.GetIp() + if err == nil { + options = append(options, cliquiz.DefaultAnswer(fmt.Sprintf("%s:26656", ip))) + } + } + questions := []cliquiz.Question{cliquiz.NewQuestion( + "Peer's address", + &publicAddress, + options..., + )} + return publicAddress, cliquiz.Ask(questions...) +} diff --git a/starport/cmd/network_chain_launch.go b/starport/cmd/network_chain_launch.go new file mode 100644 index 0000000000..dd8017cb0e --- /dev/null +++ b/starport/cmd/network_chain_launch.go @@ -0,0 +1,50 @@ +package starportcmd + +import ( + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/services/network" +) + +const ( + flagRemainingTime = "remaining-time" +) + +// NewNetworkChainLaunch creates a new chain launch command to launch +// the network as a coordinator. +func NewNetworkChainLaunch() *cobra.Command { + c := &cobra.Command{ + Use: "launch [launch-id]", + Short: "Launch a network as a coordinator", + Args: cobra.ExactArgs(1), + RunE: networkChainLaunchHandler, + } + + c.Flags().Duration(flagRemainingTime, 0, "The remaining time for validator preparation before the chain is effectively launched") + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) + + return c +} + +func networkChainLaunchHandler(cmd *cobra.Command, args []string) error { + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + // parse launch ID + launchID, err := network.ParseLaunchID(args[0]) + if err != nil { + return err + } + + remainingTime, _ := cmd.Flags().GetDuration(flagRemainingTime) + + n, err := nb.Network() + if err != nil { + return err + } + + return n.TriggerLaunch(cmd.Context(), launchID, remainingTime) +} diff --git a/starport/cmd/network_chain_list.go b/starport/cmd/network_chain_list.go new file mode 100644 index 0000000000..bcfb7196cf --- /dev/null +++ b/starport/cmd/network_chain_list.go @@ -0,0 +1,78 @@ +package starportcmd + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/cosmosaccount" + "github.com/tendermint/starport/starport/pkg/entrywriter" + "github.com/tendermint/starport/starport/services/network/networktypes" +) + +var LaunchSummaryHeader = []string{"launch ID", "chain ID", "source", "campaign ID"} + +// LaunchSummary holds summarized information about a chain launch +type LaunchSummary struct { + LaunchID string + ChainID string + Source string + CampaignID string +} + +// NewNetworkChainList returns a new command to list all published chains on Starport Network +func NewNetworkChainList() *cobra.Command { + c := &cobra.Command{ + Use: "list", + Short: "List published chains", + Args: cobra.NoArgs, + RunE: networkChainListHandler, + } + c.Flags().String(flagFrom, cosmosaccount.DefaultAccount, "Account name to use for sending transactions to SPN") + c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetHome()) + + return c +} + +func networkChainListHandler(cmd *cobra.Command, args []string) error { + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + nb.Spinner.Stop() + + n, err := nb.Network() + if err != nil { + return err + } + chainLaunches, err := n.ChainLaunches(cmd.Context()) + if err != nil { + return err + } + return renderLaunchSummaries(chainLaunches, os.Stdout) +} + +// renderLaunchSummaries writes into the provided out, the list of summarized launches +func renderLaunchSummaries(chainLaunches []networktypes.ChainLaunch, out io.Writer) error { + var launchEntries [][]string + + for _, c := range chainLaunches { + campaign := "no campaign" + if c.CampaignID > 0 { + campaign = fmt.Sprintf("%d", c.CampaignID) + } + + launchEntries = append(launchEntries, []string{ + fmt.Sprintf("%d", c.ID), + c.ChainID, + c.SourceURL, + campaign, + }) + } + + return entrywriter.MustWrite(out, LaunchSummaryHeader, launchEntries...) +} diff --git a/starport/cmd/network_chain_prepare.go b/starport/cmd/network_chain_prepare.go new file mode 100644 index 0000000000..a95ea7570c --- /dev/null +++ b/starport/cmd/network_chain_prepare.go @@ -0,0 +1,61 @@ +package starportcmd + +import ( + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/services/network" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +// NewNetworkChainPrepare returns a new command to prepare the chain for launch +func NewNetworkChainPrepare() *cobra.Command { + c := &cobra.Command{ + Use: "prepare [launch-id]", + Short: "Prepare the chain for launch", + Args: cobra.ExactArgs(1), + RunE: networkChainPrepareHandler, + } + + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetHome()) + + return c +} + +func networkChainPrepareHandler(cmd *cobra.Command, args []string) error { + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + // parse launch ID + launchID, err := network.ParseLaunchID(args[0]) + if err != nil { + return err + } + + n, err := nb.Network() + if err != nil { + return err + } + + // fetch chain information + chainLaunch, err := n.ChainLaunch(cmd.Context(), launchID) + if err != nil { + return err + } + + c, err := nb.Chain(networkchain.SourceLaunch(chainLaunch)) + if err != nil { + return err + } + + // fetch the information to construct genesis + genesisInformation, err := n.GenesisInformation(cmd.Context(), launchID) + if err != nil { + return err + } + + return c.Prepare(cmd.Context(), genesisInformation) +} diff --git a/starport/cmd/network_chain_publish.go b/starport/cmd/network_chain_publish.go new file mode 100644 index 0000000000..ad45086fef --- /dev/null +++ b/starport/cmd/network_chain_publish.go @@ -0,0 +1,139 @@ +package starportcmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/clispinner" + "github.com/tendermint/starport/starport/services/network" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +const ( + flagTag = "tag" + flagBranch = "branch" + flagHash = "hash" + flagGenesis = "genesis" + flagCampaign = "campaign" + flagNoCheck = "no-check" + flagChainID = "chain-id" +) + +// NewNetworkChainPublish returns a new command to publish a new chain to start a new network. +func NewNetworkChainPublish() *cobra.Command { + c := &cobra.Command{ + Use: "publish [source-url]", + Short: "Publish a new chain to start a new network", + Args: cobra.ExactArgs(1), + RunE: networkChainPublishHandler, + } + + c.Flags().String(flagBranch, "", "Git branch to use for the repo") + c.Flags().String(flagTag, "", "Git tag to use for the repo") + c.Flags().String(flagHash, "", "Git hash to use for the repo") + c.Flags().String(flagGenesis, "", "URL to a custom Genesis") + c.Flags().String(flagChainID, "", "Chain ID to use for this network") + c.Flags().Uint64(flagCampaign, 0, "Campaign ID to use for this network") + c.Flags().Bool(flagNoCheck, false, "Skip verifying chain's integrity") + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagSetHome()) + c.Flags().AddFlagSet(flagSetYes()) + + return c +} + +func networkChainPublishHandler(cmd *cobra.Command, args []string) error { + var ( + source = args[0] + tag, _ = cmd.Flags().GetString(flagTag) + branch, _ = cmd.Flags().GetString(flagBranch) + hash, _ = cmd.Flags().GetString(flagHash) + genesisURL, _ = cmd.Flags().GetString(flagGenesis) + chainID, _ = cmd.Flags().GetString(flagChainID) + campaign, _ = cmd.Flags().GetUint64(flagCampaign) + noCheck, _ = cmd.Flags().GetBool(flagNoCheck) + ) + + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + // use source from chosen target. + var sourceOption networkchain.SourceOption + + switch { + case tag != "": + sourceOption = networkchain.SourceRemoteTag(source, tag) + case branch != "": + sourceOption = networkchain.SourceRemoteBranch(source, branch) + case hash != "": + sourceOption = networkchain.SourceRemoteHash(source, hash) + default: + sourceOption = networkchain.SourceRemote(source) + } + + var initOptions []networkchain.Option + + // use custom genesis from url if given. + if genesisURL != "" { + initOptions = append(initOptions, networkchain.WithGenesisFromURL(genesisURL)) + } + + // init in a temp dir. + homeDir, err := os.MkdirTemp("", "") + if err != nil { + return err + } + defer os.RemoveAll(homeDir) + + initOptions = append(initOptions, networkchain.WithHome(homeDir)) + + // init the chain. + c, err := nb.Chain(sourceOption, initOptions...) + if err != nil { + return err + } + + var publishOptions []network.PublishOption + + if genesisURL != "" { + publishOptions = append(publishOptions, network.WithCustomGenesis(genesisURL)) + } + + if campaign != 0 { + publishOptions = append(publishOptions, network.WithCampaign(campaign)) + } + + // use custom chain id if given. + if chainID != "" { + publishOptions = append(publishOptions, network.WithChainID(chainID)) + } + + if noCheck { + publishOptions = append(publishOptions, network.WithNoCheck()) + } else if err := c.Init(cmd.Context()); err != nil { // initialize the chain for checking. + return err + } + + n, err := nb.Network() + if err != nil { + return err + } + + launchID, campaignID, err := n.Publish(cmd.Context(), c, publishOptions...) + if err != nil { + return err + } + + nb.Spinner.Stop() + + fmt.Printf("%s Network published \n", clispinner.OK) + fmt.Printf("%s Launch ID: %d \n", clispinner.Bullet, launchID) + fmt.Printf("%s Campaign ID: %d \n", clispinner.Bullet, campaignID) + + return nil +} diff --git a/starport/cmd/network_chain_show.go b/starport/cmd/network_chain_show.go new file mode 100644 index 0000000000..5fb781943b --- /dev/null +++ b/starport/cmd/network_chain_show.go @@ -0,0 +1,328 @@ +package starportcmd + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/cosmosutil" + "github.com/tendermint/starport/starport/pkg/entrywriter" + "github.com/tendermint/starport/starport/pkg/yaml" + "github.com/tendermint/starport/starport/services/network" + "github.com/tendermint/starport/starport/services/network/networkchain" + "github.com/tendermint/starport/starport/services/network/networktypes" +) + +var ( + chainGenesisValSummaryHeader = []string{"Genesis Validator", "Self Delegation", "Peer"} + chainGenesisAccSummaryHeader = []string{"Genesis Account", "Coins"} + chainVestingAccSummaryHeader = []string{"Vesting Account", "Total Balance", "Vesting", "EndTime"} +) + +// NewNetworkChainShow creates a new chain show +// command to show a chain details on SPN. +func NewNetworkChainShow() *cobra.Command { + c := &cobra.Command{ + Use: "show", + Short: "Show details of a chain", + } + c.AddCommand( + newNetworkChainShowInfo(), + newNetworkChainShowGenesis(), + newNetworkChainShowAccounts(), + newNetworkChainShowValidators(), + newNetworkChainShowPeers(), + ) + c.PersistentFlags().AddFlagSet(flagNetworkFrom()) + c.PersistentFlags().AddFlagSet(flagSetKeyringBackend()) + return c +} + +func networkChainLaunch(cmd *cobra.Command, args []string) (NetworkBuilder, uint64, error) { + nb, err := newNetworkBuilder(cmd) + if err != nil { + return nb, 0, err + } + // parse launch ID. + launchID, err := network.ParseLaunchID(args[0]) + if err != nil { + return nb, launchID, err + } + return nb, launchID, err +} + +func newNetworkChainShowInfo() *cobra.Command { + c := &cobra.Command{ + Use: "info [launch-id]", + Short: "Show info details of the chain", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + nb, launchID, err := networkChainLaunch(cmd, args) + if err != nil { + return err + } + defer nb.Cleanup() + n, err := nb.Network() + if err != nil { + return err + } + + chainLaunch, err := n.ChainLaunch(cmd.Context(), launchID) + if err != nil { + return err + } + + var genesis []byte + if chainLaunch.GenesisURL != "" { + genesis, _, err = cosmosutil.GenesisAndHashFromURL(cmd.Context(), chainLaunch.GenesisURL) + if err != nil { + return err + } + } + chainInfo := struct { + Chain networktypes.ChainLaunch `json:"Chain"` + Genesis []byte `json:"Genesis"` + }{ + Chain: chainLaunch, + Genesis: genesis, + } + info, err := yaml.Marshal(cmd.Context(), chainInfo, "$.Genesis") + if err != nil { + return err + } + nb.Spinner.Stop() + fmt.Print(info) + return nil + }, + } + return c +} + +func newNetworkChainShowGenesis() *cobra.Command { + c := &cobra.Command{ + Use: "genesis [launch-id]", + Short: "Show the chain genesis file", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + nb, launchID, err := networkChainLaunch(cmd, args) + if err != nil { + return err + } + defer nb.Cleanup() + n, err := nb.Network() + if err != nil { + return err + } + + chainLaunch, err := n.ChainLaunch(cmd.Context(), launchID) + if err != nil { + return err + } + + c, err := nb.Chain(networkchain.SourceLaunch(chainLaunch)) + if err != nil { + return err + } + genesisPath, err := c.GenesisPath() + if err != nil { + return err + } + + // check if the genesis already exist + if _, err = os.Stat(genesisPath); os.IsNotExist(err) { + // fetch the information to construct genesis + genesisInformation, err := n.GenesisInformation(cmd.Context(), launchID) + if err != nil { + return err + } + + // create the chain into a temp dir + home := filepath.Join(os.TempDir(), "spn/temp", chainLaunch.ChainID) + c.SetHome(home) + defer os.RemoveAll(home) + + err = c.Prepare(cmd.Context(), genesisInformation) + if err != nil { + return err + } + + // get the new genesis path + genesisPath, err = c.GenesisPath() + if err != nil { + return err + } + } + genesisFile, err := os.ReadFile(genesisPath) + if err != nil { + return err + } + + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, genesisFile, "", " "); err != nil { + return err + } + nb.Spinner.Stop() + fmt.Printf("Genesis: \n%s", prettyJSON.String()) + return nil + }, + } + return c +} + +func newNetworkChainShowAccounts() *cobra.Command { + c := &cobra.Command{ + Use: "accounts [launch-id]", + Short: "Show all vesting and genesis accounts of the chain", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + nb, launchID, err := networkChainLaunch(cmd, args) + if err != nil { + return err + } + defer nb.Cleanup() + n, err := nb.Network() + if err != nil { + return err + } + + accountSummary := bytes.NewBufferString("") + + // get all chain genesis accounts + genesisAccs, err := n.GenesisAccounts(cmd.Context(), launchID) + if err != nil { + return err + } + genesisAccEntries := make([][]string, 0) + for _, acc := range genesisAccs { + genesisAccEntries = append(genesisAccEntries, []string{ + acc.Address, + acc.Coins, + }) + } + if len(genesisAccEntries) > 0 { + if err = entrywriter.MustWrite( + accountSummary, + chainGenesisAccSummaryHeader, + genesisAccEntries..., + ); err != nil { + return err + } + } + + // get all chain vesting accounts + vestingAccs, err := n.VestingAccounts(cmd.Context(), launchID) + if err != nil { + return err + } + genesisVestingAccEntries := make([][]string, 0) + for _, acc := range vestingAccs { + genesisVestingAccEntries = append(genesisVestingAccEntries, []string{ + acc.Address, + acc.TotalBalance, + acc.Vesting, + strconv.FormatInt(acc.EndTime, 10), + }) + } + if len(genesisVestingAccEntries) > 0 { + if err = entrywriter.MustWrite( + accountSummary, + chainVestingAccSummaryHeader, + genesisVestingAccEntries..., + ); err != nil { + return err + } + } + nb.Spinner.Stop() + fmt.Print(accountSummary.String()) + return nil + }, + } + return c +} + +func newNetworkChainShowValidators() *cobra.Command { + c := &cobra.Command{ + Use: "validators [launch-id]", + Short: "Show all validators of the chain", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + nb, launchID, err := networkChainLaunch(cmd, args) + if err != nil { + return err + } + defer nb.Cleanup() + n, err := nb.Network() + if err != nil { + return err + } + + validatorSummary := bytes.NewBufferString("") + validators, err := n.GenesisValidators(cmd.Context(), launchID) + if err != nil { + return err + } + validatorEntries := make([][]string, 0) + for _, acc := range validators { + validatorEntries = append(validatorEntries, []string{ + acc.Address, + acc.SelfDelegation.String(), + acc.Peer, + }) + } + if len(validatorEntries) > 0 { + if err = entrywriter.MustWrite( + validatorSummary, + chainGenesisValSummaryHeader, + validatorEntries..., + ); err != nil { + return err + } + } + nb.Spinner.Stop() + fmt.Print(validatorSummary.String()) + return nil + }, + } + return c +} + +func newNetworkChainShowPeers() *cobra.Command { + c := &cobra.Command{ + Use: "peers [launch-id]", + Short: "Show peers list of the chain", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + nb, launchID, err := networkChainLaunch(cmd, args) + if err != nil { + return err + } + defer nb.Cleanup() + n, err := nb.Network() + if err != nil { + return err + } + genVals, err := n.GenesisValidators(cmd.Context(), launchID) + if err != nil { + return err + } + + peers := make([]string, 0) + for _, acc := range genVals { + peers = append(peers, acc.Peer) + } + nb.Spinner.Stop() + if len(peers) > 0 { + fmt.Printf("Peers: %s\n", strings.Join(peers, ",")) + } else { + fmt.Print("empty peer list") + } + return nil + }, + } + return c +} diff --git a/starport/cmd/network_request.go b/starport/cmd/network_request.go new file mode 100644 index 0000000000..04c950d826 --- /dev/null +++ b/starport/cmd/network_request.go @@ -0,0 +1,21 @@ +package starportcmd + +import "github.com/spf13/cobra" + +// NewNetworkRequest creates a new approval request command that holds some other +// sub commands related to handle request for a chain. +func NewNetworkRequest() *cobra.Command { + c := &cobra.Command{ + Use: "request", + Short: "Handle requests", + } + + c.AddCommand( + NewNetworkRequestShow(), + NewNetworkRequestList(), + NewNetworkRequestApprove(), + NewNetworkRequestReject(), + ) + + return c +} diff --git a/starport/cmd/network_request_approve.go b/starport/cmd/network_request_approve.go new file mode 100644 index 0000000000..6f465be8c8 --- /dev/null +++ b/starport/cmd/network_request_approve.go @@ -0,0 +1,82 @@ +package starportcmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/clispinner" + "github.com/tendermint/starport/starport/pkg/numbers" + "github.com/tendermint/starport/starport/services/network" +) + +const ( + flagNoVerification = "no-verification" +) + +// NewNetworkRequestApprove creates a new request approve +// command to approve requests for a chain. +func NewNetworkRequestApprove() *cobra.Command { + c := &cobra.Command{ + Use: "approve [launch-id] [number<,...>]", + Aliases: []string{"accept"}, + Short: "Approve requests", + RunE: networkRequestApproveHandler, + Args: cobra.ExactArgs(2), + } + c.Flags().Bool(flagNoVerification, false, "approve the requests without verifying them") + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetHome()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) + return c +} + +func networkRequestApproveHandler(cmd *cobra.Command, args []string) error { + // initialize network common methods + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + // parse launch ID + launchID, err := network.ParseLaunchID(args[0]) + if err != nil { + return err + } + + // Get the list of request ids + ids, err := numbers.ParseList(args[1]) + if err != nil { + return err + } + + // Verify the requests are valid + noVerification, err := cmd.Flags().GetBool(flagNoVerification) + if err != nil { + return err + } + + n, err := nb.Network() + if err != nil { + return err + } + + if !noVerification { + err := n.VerifyRequests(cmd.Context(), launchID, ids...) + if err != nil { + return err + } + } + // Submit the approved requests + reviewals := make([]network.Reviewal, 0) + for _, id := range ids { + reviewals = append(reviewals, network.ApproveRequest(id)) + } + if err := n.SubmitRequest(launchID, reviewals...); err != nil { + return err + } + + nb.Spinner.Stop() + fmt.Printf("%s Request(s) %s approved\n", clispinner.OK, numbers.List(ids, "#")) + return nil +} diff --git a/starport/cmd/network_request_list.go b/starport/cmd/network_request_list.go new file mode 100644 index 0000000000..d33109eca0 --- /dev/null +++ b/starport/cmd/network_request_list.go @@ -0,0 +1,109 @@ +package starportcmd + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + launchtypes "github.com/tendermint/spn/x/launch/types" + "github.com/tendermint/starport/starport/pkg/entrywriter" + "github.com/tendermint/starport/starport/services/network" +) + +var requestSummaryHeader = []string{"ID", "Type", "Content"} + +// NewNetworkRequestList creates a new request list command to list +// requests for a chain +func NewNetworkRequestList() *cobra.Command { + c := &cobra.Command{ + Use: "list [launch-id]", + Short: "List all pending requests", + RunE: networkRequestListHandler, + Args: cobra.ExactArgs(1), + } + c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetHome()) + return c +} + +func networkRequestListHandler(cmd *cobra.Command, args []string) error { + // initialize network common methods + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + // parse launch ID + launchID, err := network.ParseLaunchID(args[0]) + if err != nil { + return err + } + + n, err := nb.Network() + if err != nil { + return err + } + + requests, err := n.Requests(cmd.Context(), launchID) + if err != nil { + return err + } + + nb.Spinner.Stop() + return renderRequestSummaries(requests, os.Stdout) +} + +// renderRequestSummaries writes into the provided out, the list of summarized requests +func renderRequestSummaries(requests []launchtypes.Request, out io.Writer) error { + requestEntries := make([][]string, 0) + for _, request := range requests { + id := fmt.Sprintf("%d", request.RequestID) + requestType := "Unknown" + content := "" + + switch req := request.Content.Content.(type) { + case *launchtypes.RequestContent_GenesisAccount: + requestType = "Add Genesis Account" + content = fmt.Sprintf("%s, %s", + req.GenesisAccount.Address, + req.GenesisAccount.Coins.String()) + case *launchtypes.RequestContent_GenesisValidator: + requestType = "Add Genesis Validator" + content = fmt.Sprintf("%s, %s, %s", + req.GenesisValidator.Peer, + req.GenesisValidator.Address, + req.GenesisValidator.SelfDelegation.String()) + case *launchtypes.RequestContent_VestingAccount: + requestType = "Add Vesting Account" + + // parse vesting options + var vestingCoins string + dv := req.VestingAccount.VestingOptions.GetDelayedVesting() + if dv == nil { + vestingCoins = "unrecognized vesting option" + } else { + vestingCoins = fmt.Sprintf("%s (vesting: %s)", dv.TotalBalance, dv.Vesting) + } + content = fmt.Sprintf("%s, %s", + req.VestingAccount.Address, + vestingCoins, + ) + case *launchtypes.RequestContent_ValidatorRemoval: + requestType = "Remove Validator" + content = req.ValidatorRemoval.ValAddress + case *launchtypes.RequestContent_AccountRemoval: + requestType = "Remove Account" + content = req.AccountRemoval.Address + } + + requestEntries = append(requestEntries, []string{ + id, + requestType, + content, + }) + } + return entrywriter.MustWrite(out, requestSummaryHeader, requestEntries...) +} diff --git a/starport/cmd/network_request_reject.go b/starport/cmd/network_request_reject.go new file mode 100644 index 0000000000..45ef536c9f --- /dev/null +++ b/starport/cmd/network_request_reject.go @@ -0,0 +1,65 @@ +package starportcmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/clispinner" + "github.com/tendermint/starport/starport/pkg/numbers" + "github.com/tendermint/starport/starport/services/network" +) + +// NewNetworkRequestReject creates a new request reject +// command to reject requests for a chain. +func NewNetworkRequestReject() *cobra.Command { + c := &cobra.Command{ + Use: "reject [launch-id] [number<,...>]", + Aliases: []string{"accept"}, + Short: "Reject requests", + RunE: networkRequestRejectHandler, + Args: cobra.ExactArgs(2), + } + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetHome()) + c.Flags().AddFlagSet(flagSetKeyringBackend()) + return c +} + +func networkRequestRejectHandler(cmd *cobra.Command, args []string) error { + // initialize network common methods + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + // parse launch ID + launchID, err := network.ParseLaunchID(args[0]) + if err != nil { + return err + } + + // Get the list of request ids + ids, err := numbers.ParseList(args[1]) + if err != nil { + return err + } + + n, err := nb.Network() + if err != nil { + return err + } + + // Submit the rejected requests + reviewals := make([]network.Reviewal, 0) + for _, id := range ids { + reviewals = append(reviewals, network.RejectRequest(id)) + } + if err := n.SubmitRequest(launchID, reviewals...); err != nil { + return err + } + + nb.Spinner.Stop() + fmt.Printf("%s Request(s) %s rejected\n", clispinner.OK, numbers.List(ids, "#")) + return nil +} diff --git a/starport/cmd/network_request_show.go b/starport/cmd/network_request_show.go new file mode 100644 index 0000000000..004e31a247 --- /dev/null +++ b/starport/cmd/network_request_show.go @@ -0,0 +1,71 @@ +package starportcmd + +import ( + "fmt" + "strconv" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/tendermint/starport/starport/pkg/yaml" + "github.com/tendermint/starport/starport/services/network" +) + +// NewNetworkRequestShow creates a new request show command to show +// requests details for a chain +func NewNetworkRequestShow() *cobra.Command { + c := &cobra.Command{ + Use: "show [launch-id] [request-id]", + Short: "Show pending requests details", + RunE: networkRequestShowHandler, + Args: cobra.ExactArgs(2), + } + c.Flags().AddFlagSet(flagSetKeyringBackend()) + c.Flags().AddFlagSet(flagNetworkFrom()) + c.Flags().AddFlagSet(flagSetHome()) + return c +} + +func networkRequestShowHandler(cmd *cobra.Command, args []string) error { + // initialize network common methods + nb, err := newNetworkBuilder(cmd) + if err != nil { + return err + } + defer nb.Cleanup() + + // parse launch ID + launchID, err := network.ParseLaunchID(args[0]) + if err != nil { + return err + } + + // parse request ID + requestID, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return errors.Wrap(err, "error parsing requestID") + } + + n, err := nb.Network() + if err != nil { + return err + } + + request, err := n.Request(cmd.Context(), launchID, requestID) + if err != nil { + return err + } + + // convert the request object to YAML to be more readable + // and convert the byte array fields to string. + requestYaml, err := yaml.Marshal(cmd.Context(), request, + "$.content.content.genesisValidator.genTx", + "$.content.content.genesisValidator.consPubKey", + ) + if err != nil { + return err + } + + nb.Spinner.Stop() + fmt.Println(requestYaml) + return nil +} diff --git a/starport/cmd/relayer_configure.go b/starport/cmd/relayer_configure.go index 48546220fb..816408a7c2 100644 --- a/starport/cmd/relayer_configure.go +++ b/starport/cmd/relayer_configure.go @@ -483,7 +483,3 @@ func initChain( return c, nil } - -func printSection(title string) { - fmt.Printf("---------------------------------------------\n%s\n---------------------------------------------\n\n", title) -} diff --git a/starport/cmd/scaffold.go b/starport/cmd/scaffold.go index e50d8c7c0e..0d6af85caa 100644 --- a/starport/cmd/scaffold.go +++ b/starport/cmd/scaffold.go @@ -71,8 +71,7 @@ func scaffoldType( } if withoutMessage { options = append(options, scaffolder.TypeWithoutMessage()) - } - if signer != "" { + } else if signer != "" { options = append(options, scaffolder.TypeWithSigner(signer)) } diff --git a/starport/cmd/scaffold_module.go b/starport/cmd/scaffold_module.go index ee33928da8..c2ba5d29a0 100644 --- a/starport/cmd/scaffold_module.go +++ b/starport/cmd/scaffold_module.go @@ -18,6 +18,7 @@ import ( const ( flagDep = "dep" flagIBC = "ibc" + flagParams = "params" flagIBCOrdering = "ordering" flagRequireRegistration = "require-registration" ) @@ -37,14 +38,13 @@ func NewScaffoldModule() *cobra.Command { c.Flags().Bool(flagIBC, false, "scaffold an IBC module") c.Flags().String(flagIBCOrdering, "none", "channel ordering of the IBC module [none|ordered|unordered]") c.Flags().Bool(flagRequireRegistration, false, "if true command will fail if module can't be registered") + c.Flags().StringSlice(flagParams, []string{}, "scaffold module params") return c } func scaffoldModuleHandler(cmd *cobra.Command, args []string) error { var ( - options []scaffolder.ModuleCreationOption - name = args[0] appPath = flagGetPath(cmd) ) @@ -65,6 +65,15 @@ func scaffoldModuleHandler(cmd *cobra.Command, args []string) error { return err } + params, err := cmd.Flags().GetStringSlice(flagParams) + if err != nil { + return err + } + + options := []scaffolder.ModuleCreationOption{ + scaffolder.WithParams(params), + } + // Check if the module must be an IBC module if ibcModule { options = append(options, scaffolder.WithIBCChannelOrdering(ibcOrdering), scaffolder.WithIBC()) diff --git a/starport/cmd/scaffold_package.go b/starport/cmd/scaffold_package.go index 94edeac14d..8e66e1501c 100644 --- a/starport/cmd/scaffold_package.go +++ b/starport/cmd/scaffold_package.go @@ -65,8 +65,7 @@ func createPacketHandler(cmd *cobra.Command, args []string) error { var options []scaffolder.PacketOption if noMessage { options = append(options, scaffolder.PacketWithoutMessage()) - } - if signer != "" { + } else if signer != "" { options = append(options, scaffolder.PacketWithSigner(signer)) } diff --git a/starport/internal/tools/gen-cli-docs/main.go b/starport/internal/tools/gen-cli-docs/main.go index f05c23d1be..1dfc228c96 100644 --- a/starport/internal/tools/gen-cli-docs/main.go +++ b/starport/internal/tools/gen-cli-docs/main.go @@ -27,7 +27,7 @@ parent: title: CLI Reference --- -# Reference +# CLI Reference Documentation for Starport CLI. ` diff --git a/starport/pkg/chaincmd/chaincmd.go b/starport/pkg/chaincmd/chaincmd.go index 3af50ea000..bb6f26e0c5 100644 --- a/starport/pkg/chaincmd/chaincmd.go +++ b/starport/pkg/chaincmd/chaincmd.go @@ -36,8 +36,15 @@ const ( optionValidatorCommissionMaxChangeRate = "--commission-max-change-rate" optionValidatorMinSelfDelegation = "--min-self-delegation" optionValidatorGasPrices = "--gas-prices" + optionValidatorDetails = "--details" + optionValidatorIdentity = "--identity" + optionValidatorWebsite = "--website" + optionValidatorSecurityContact = "--security-contact" optionYes = "--yes" optionHomeClient = "--home-client" + optionCoinType = "--coin-type" + optionVestingAmount = "--vesting-amount" + optionVestingEndTime = "--vesting-end-time" optionBroadcastMode = "--broadcast-mode" constTendermint = "tendermint" @@ -194,7 +201,7 @@ func (c ChainCmd) InitCommand(moniker string) step.Option { } // AddKeyCommand returns the command to add a new key in the chain keyring -func (c ChainCmd) AddKeyCommand(accountName string) step.Option { +func (c ChainCmd) AddKeyCommand(accountName, coinType string) step.Option { command := []string{ commandKeys, "add", @@ -202,19 +209,38 @@ func (c ChainCmd) AddKeyCommand(accountName string) step.Option { optionOutput, constJSON, } + if coinType != "" { + command = append(command, optionCoinType, coinType) + } command = c.attachKeyringBackend(command) return c.cliCommand(command) } -// ImportKeyCommand returns the command to import a key into the chain keyring from a mnemonic -func (c ChainCmd) ImportKeyCommand(accountName string) step.Option { +// RecoverKeyCommand returns the command to recover a key into the chain keyring from a mnemonic +func (c ChainCmd) RecoverKeyCommand(accountName, coinType string) step.Option { command := []string{ commandKeys, "add", accountName, optionRecover, } + if coinType != "" { + command = append(command, optionCoinType, coinType) + } + command = c.attachKeyringBackend(command) + + return c.cliCommand(command) +} + +// ImportKeyCommand returns the command to import a key into the chain keyring from a key file +func (c ChainCmd) ImportKeyCommand(accountName, keyFile string) step.Option { + command := []string{ + commandKeys, + "import", + accountName, + keyFile, + } command = c.attachKeyringBackend(command) return c.cliCommand(command) @@ -247,7 +273,7 @@ func (c ChainCmd) ListKeysCommand() step.Option { } // AddGenesisAccountCommand returns the command to add a new account in the genesis file of the chain -func (c ChainCmd) AddGenesisAccountCommand(address string, coins string) step.Option { +func (c ChainCmd) AddGenesisAccountCommand(address, coins string) step.Option { command := []string{ commandAddGenesisAccount, address, @@ -257,6 +283,21 @@ func (c ChainCmd) AddGenesisAccountCommand(address string, coins string) step.Op return c.daemonCommand(command) } +// AddVestingAccountCommand returns the command to add a delayed vesting account in the genesis file of the chain +func (c ChainCmd) AddVestingAccountCommand(address, originalCoins, vestingCoins string, vestingEndTime int64) step.Option { + command := []string{ + commandAddGenesisAccount, + address, + originalCoins, + optionVestingAmount, + vestingCoins, + optionVestingEndTime, + fmt.Sprintf("%d", vestingEndTime), + } + + return c.daemonCommand(command) +} + // GentxOption for the GentxCommand type GentxOption func([]string) []string @@ -320,6 +361,46 @@ func GentxWithGasPrices(gasPrices string) GentxOption { } } +// GentxWithDetails provides validator details option for the gentx command +func GentxWithDetails(details string) GentxOption { + return func(command []string) []string { + if len(details) > 0 { + return append(command, optionValidatorDetails, details) + } + return command + } +} + +// GentxWithIdentity provides validator identity option for the gentx command +func GentxWithIdentity(identity string) GentxOption { + return func(command []string) []string { + if len(identity) > 0 { + return append(command, optionValidatorIdentity, identity) + } + return command + } +} + +// GentxWithWebsite provides validator website option for the gentx command +func GentxWithWebsite(website string) GentxOption { + return func(command []string) []string { + if len(website) > 0 { + return append(command, optionValidatorWebsite, website) + } + return command + } +} + +// GentxWithSecurityContact provides validator security contact option for the gentx command +func GentxWithSecurityContact(securityContact string) GentxOption { + return func(command []string) []string { + if len(securityContact) > 0 { + return append(command, optionValidatorSecurityContact, securityContact) + } + return command + } +} + func (c ChainCmd) IsAutoChainIDDetectionEnabled() bool { return c.isAutoChainIDDetectionEnabled } @@ -475,7 +556,7 @@ func (c ChainCmd) QueryTxEventsCommand(query string) step.Option { } // LaunchpadSetConfigCommand returns the command to set config value -func (c ChainCmd) LaunchpadSetConfigCommand(name string, value string) step.Option { +func (c ChainCmd) LaunchpadSetConfigCommand(name, value string) step.Option { // Check version if c.isStargate() { panic("config command doesn't exist for Stargate") @@ -484,7 +565,7 @@ func (c ChainCmd) LaunchpadSetConfigCommand(name string, value string) step.Opti } // LaunchpadRestServerCommand returns the command to start the CLI REST server -func (c ChainCmd) LaunchpadRestServerCommand(apiAddress string, rpcAddress string) step.Option { +func (c ChainCmd) LaunchpadRestServerCommand(apiAddress, rpcAddress string) step.Option { // Check version if c.isStargate() { panic("rest-server command doesn't exist for Stargate") diff --git a/starport/pkg/chaincmd/runner/account.go b/starport/pkg/chaincmd/runner/account.go index b458fe8f8c..703c6e6920 100644 --- a/starport/pkg/chaincmd/runner/account.go +++ b/starport/pkg/chaincmd/runner/account.go @@ -20,32 +20,21 @@ var ( ErrAccountDoesNotExist = errors.New("account does not exit") ) +// Account represents a user account. +type Account struct { + Name string `json:"name"` + Address string `json:"address"` + Mnemonic string `json:"mnemonic,omitempty"` +} + // AddAccount creates a new account or imports an account when mnemonic is provided. // returns with an error if the operation went unsuccessful or an account with the provided name // already exists. -func (r Runner) AddAccount(ctx context.Context, name, mnemonic string) (Account, error) { - b := newBuffer() - - // check if account already exists. - var accounts []Account - if err := r.run(ctx, runOptions{stdout: b}, r.chainCmd.ListKeysCommand()); err != nil { - return Account{}, err - } - - data, err := b.JSONEnsuredBytes() - if err != nil { +func (r Runner) AddAccount(ctx context.Context, name, mnemonic, coinType string) (Account, error) { + if err := r.CheckAccountExist(ctx, name); err != nil { return Account{}, err } - if err := json.Unmarshal(data, &accounts); err != nil { - return Account{}, err - } - - for _, account := range accounts { - if account.Name == name { - return Account{}, ErrAccountAlreadyExists - } - } - b.Reset() + b := newBuffer() account := Account{ Name: name, @@ -65,7 +54,7 @@ func (r Runner) AddAccount(ctx context.Context, name, mnemonic string) (Account, if err := r.run( ctx, runOptions{}, - r.chainCmd.ImportKeyCommand(name), + r.chainCmd.RecoverKeyCommand(name, coinType), step.Write(input.Bytes()), ); err != nil { return Account{}, err @@ -75,7 +64,7 @@ func (r Runner) AddAccount(ctx context.Context, name, mnemonic string) (Account, stdout: b, stderr: b, stdin: os.Stdin, - }, r.chainCmd.AddKeyCommand(name)); err != nil { + }, r.chainCmd.AddKeyCommand(name, coinType)); err != nil { return Account{}, err } @@ -86,43 +75,66 @@ func (r Runner) AddAccount(ctx context.Context, name, mnemonic string) (Account, if err := json.Unmarshal(data, &account); err != nil { return Account{}, err } - - b.Reset() } - // get full details of the account. - runOptions := runOptions{ - stdout: b, - stderr: os.Stderr, + // get the address of the account. + retrieved, err := r.ShowAccount(ctx, name) + if err != nil { + return Account{}, err } + account.Address = retrieved.Address - stepOptions := []step.Option{ - r.chainCmd.ShowKeyAddressCommand(name), - } + return account, nil +} - if r.chainCmd.KeyringPassword() != "" { - // If keyring password is defined, we write it into the command input - input := &bytes.Buffer{} - fmt.Fprintln(input, r.chainCmd.KeyringPassword()) - stepOptions = append(stepOptions, step.Write(input.Bytes())) - } else { - // Otherwise we provide os stdin to the command - runOptions.stdin = os.Stdin +// ImportAccount import an account from a key file +func (r Runner) ImportAccount(ctx context.Context, name, keyFile, passphrase string) (Account, error) { + if err := r.CheckAccountExist(ctx, name); err != nil { + return Account{}, err } - if err := r.run(ctx, runOptions, stepOptions...); err != nil { + // write the passphrase as input + // TODO: manage keyring backend other than test + input := &bytes.Buffer{} + fmt.Fprintln(input, passphrase) + + if err := r.run( + ctx, + runOptions{}, + r.chainCmd.ImportKeyCommand(name, keyFile), + step.Write(input.Bytes()), + ); err != nil { return Account{}, err } - account.Address = strings.TrimSpace(b.String()) - return account, nil + return r.ShowAccount(ctx, name) } -// Account represents a user account. -type Account struct { - Name string `json:"name"` - Address string `json:"address"` - Mnemonic string `json:"mnemonic,omitempty"` +// CheckAccountExist returns an error if the account already exists in the chain keyring +func (r Runner) CheckAccountExist(ctx context.Context, name string) error { + b := newBuffer() + + // get and decodes all accounts of the chains + var accounts []Account + if err := r.run(ctx, runOptions{stdout: b}, r.chainCmd.ListKeysCommand()); err != nil { + return err + } + + data, err := b.JSONEnsuredBytes() + if err != nil { + return err + } + if err := json.Unmarshal(data, &accounts); err != nil { + return err + } + + // search for the account name + for _, account := range accounts { + if account.Name == name { + return ErrAccountAlreadyExists + } + } + return nil } // ShowAccount shows details of an account. @@ -157,3 +169,14 @@ func (r Runner) ShowAccount(ctx context.Context, name string) (Account, error) { func (r Runner) AddGenesisAccount(ctx context.Context, address, coins string) error { return r.run(ctx, runOptions{}, r.chainCmd.AddGenesisAccountCommand(address, coins)) } + +// AddVestingAccount adds vesting account to genesis by its address. +func (r Runner) AddVestingAccount( + ctx context.Context, + address, + originalCoins, + vestingCoins string, + vestingEndTime int64, +) error { + return r.run(ctx, runOptions{}, r.chainCmd.AddVestingAccountCommand(address, originalCoins, vestingCoins, vestingEndTime)) +} diff --git a/starport/pkg/chaincmd/runner/chain.go b/starport/pkg/chaincmd/runner/chain.go index 4cb4c348c0..fdc837d736 100644 --- a/starport/pkg/chaincmd/runner/chain.go +++ b/starport/pkg/chaincmd/runner/chain.go @@ -6,8 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "io" - "io/ioutil" "os" "regexp" "strings" @@ -79,7 +77,7 @@ func (r Runner) Gentx( if err := r.run(ctx, runOptions{ stdout: b, - stderr: io.MultiWriter(b, os.Stderr), + stderr: b, stdin: os.Stdin, }, r.chainCmd.GentxCommand(validatorName, selfDelegation, options...)); err != nil { return "", err @@ -225,7 +223,7 @@ func (r Runner) Export(ctx context.Context, exportedFile string) error { } // Save the new state - return ioutil.WriteFile(exportedFile, exportedState, 0644) + return os.WriteFile(exportedFile, exportedState, 0644) } // EventSelector is used to query events. diff --git a/starport/pkg/chaincmd/runner/runner.go b/starport/pkg/chaincmd/runner/runner.go index 50810aedcc..0efe4960fc 100644 --- a/starport/pkg/chaincmd/runner/runner.go +++ b/starport/pkg/chaincmd/runner/runner.go @@ -5,7 +5,6 @@ import ( "bytes" "context" "io" - "io/ioutil" "github.com/ghodss/yaml" "github.com/pkg/errors" @@ -58,8 +57,8 @@ func Stderr(w io.Writer) Option { func New(ctx context.Context, chainCmd chaincmd.ChainCmd, options ...Option) (Runner, error) { runner := Runner{ chainCmd: chainCmd, - stdout: ioutil.Discard, - stderr: ioutil.Discard, + stdout: io.Discard, + stderr: io.Discard, } applyOptions(&runner, options) diff --git a/starport/pkg/chaincmd/runner/simulate.go b/starport/pkg/chaincmd/runner/simulate.go new file mode 100644 index 0000000000..c4495106e4 --- /dev/null +++ b/starport/pkg/chaincmd/runner/simulate.go @@ -0,0 +1,43 @@ +package chaincmdrunner + +import ( + "context" + "os" + + "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/tendermint/starport/starport/pkg/chaincmd" +) + +// Simulation run the chain simulation. +func (r Runner) Simulation( + ctx context.Context, + appPath string, + enabled bool, + verbose bool, + config simulation.Config, + period uint, + genesisTime int64, +) error { + return r.run(ctx, runOptions{stdout: os.Stdout}, + chaincmd.SimulationCommand( + appPath, + chaincmd.SimappWithGenesis(config.GenesisFile), + chaincmd.SimappWithParams(config.ParamsFile), + chaincmd.SimappWithExportParamsPath(config.ExportParamsPath), + chaincmd.SimappWithExportParamsHeight(config.ExportParamsHeight), + chaincmd.SimappWithExportStatePath(config.ExportStatePath), + chaincmd.SimappWithExportStatsPath(config.ExportStatsPath), + chaincmd.SimappWithSeed(config.Seed), + chaincmd.SimappWithInitialBlockHeight(config.InitialBlockHeight), + chaincmd.SimappWithNumBlocks(config.NumBlocks), + chaincmd.SimappWithBlockSize(config.BlockSize), + chaincmd.SimappWithLean(config.Lean), + chaincmd.SimappWithCommit(config.Commit), + chaincmd.SimappWithSimulateEveryOperation(config.OnOperation), + chaincmd.SimappWithPrintAllInvariants(config.AllInvariants), + chaincmd.SimappWithEnable(enabled), + chaincmd.SimappWithVerbose(verbose), + chaincmd.SimappWithPeriod(period), + chaincmd.SimappWithGenesisTime(genesisTime), + )) +} diff --git a/starport/pkg/chaincmd/simulate.go b/starport/pkg/chaincmd/simulate.go new file mode 100644 index 0000000000..4ee96cdff6 --- /dev/null +++ b/starport/pkg/chaincmd/simulate.go @@ -0,0 +1,221 @@ +package chaincmd + +import ( + "path/filepath" + "strconv" + + "github.com/tendermint/starport/starport/pkg/cmdrunner/step" + "github.com/tendermint/starport/starport/pkg/gocmd" +) + +const ( + optionSimappGenesis = "-Genesis" + optionSimappParams = "-Params" + optionSimappExportParamsPath = "-ExportParamsPath" + optionSimappExportParamsHeight = "-ExportParamsHeight" + optionSimappExportStatePath = "-ExportStatePath" + optionSimappExportStatsPath = "-ExportStatsPath" + optionSimappSeed = "-Seed" + optionSimappInitialBlockHeight = "-InitialBlockHeight" + optionSimappNumBlocks = "-NumBlocks" + optionSimappBlockSize = "-BlockSize" + optionSimappLean = "-Lean" + optionSimappCommit = "-Commit" + optionSimappSimulateEveryOperation = "-SimulateEveryOperation" + optionSimappPrintAllInvariants = "-PrintAllInvariants" + optionSimappEnabled = "-Enabled" + optionSimappVerbose = "-Verbose" + optionSimappPeriod = "-Period" + optionSimappGenesisTime = "-GenesisTime" + + commandGoTest = "test" + optionGoBenchmem = "-benchmem" + optionGoSimappRun = "-run=^$" + optionGoSimappBench = "-bench=^BenchmarkSimulation" +) + +// SimappOption for the SimulateCommand +type SimappOption func([]string) []string + +// SimappWithGenesis provides genesis option for the simapp command +func SimappWithGenesis(genesis string) SimappOption { + return func(command []string) []string { + if len(genesis) > 0 { + return append(command, optionSimappGenesis, genesis) + } + return command + } +} + +// SimappWithParams provides params option for the simapp command +func SimappWithParams(params string) SimappOption { + return func(command []string) []string { + if len(params) > 0 { + return append(command, optionSimappParams, params) + } + return command + } +} + +// SimappWithExportParamsPath provides exportParamsPath option for the simapp command +func SimappWithExportParamsPath(exportParamsPath string) SimappOption { + return func(command []string) []string { + if len(exportParamsPath) > 0 { + return append(command, optionSimappExportParamsPath, exportParamsPath) + } + return command + } +} + +// SimappWithExportParamsHeight provides exportParamsHeight option for the simapp command +func SimappWithExportParamsHeight(exportParamsHeight int) SimappOption { + return func(command []string) []string { + if exportParamsHeight > 0 { + return append( + command, + optionSimappExportParamsHeight, + strconv.Itoa(exportParamsHeight), + ) + } + return command + } +} + +// SimappWithExportStatePath provides exportStatePath option for the simapp command +func SimappWithExportStatePath(exportStatePath string) SimappOption { + return func(command []string) []string { + if len(exportStatePath) > 0 { + return append(command, optionSimappExportStatePath, exportStatePath) + } + return command + } +} + +// SimappWithExportStatsPath provides exportStatsPath option for the simapp command +func SimappWithExportStatsPath(exportStatsPath string) SimappOption { + return func(command []string) []string { + if len(exportStatsPath) > 0 { + return append(command, optionSimappExportStatsPath, exportStatsPath) + } + return command + } +} + +// SimappWithSeed provides seed option for the simapp command +func SimappWithSeed(seed int64) SimappOption { + return func(command []string) []string { + return append(command, optionSimappSeed, strconv.FormatInt(seed, 10)) + } +} + +// SimappWithInitialBlockHeight provides initialBlockHeight option for the simapp command +func SimappWithInitialBlockHeight(initialBlockHeight int) SimappOption { + return func(command []string) []string { + return append(command, optionSimappInitialBlockHeight, strconv.Itoa(initialBlockHeight)) + } +} + +// SimappWithNumBlocks provides numBlocks option for the simapp command +func SimappWithNumBlocks(numBlocks int) SimappOption { + return func(command []string) []string { + return append(command, optionSimappNumBlocks, strconv.Itoa(numBlocks)) + } +} + +// SimappWithBlockSize provides blockSize option for the simapp command +func SimappWithBlockSize(blockSize int) SimappOption { + return func(command []string) []string { + return append(command, optionSimappBlockSize, strconv.Itoa(blockSize)) + } +} + +// SimappWithLean provides lean option for the simapp command +func SimappWithLean(lean bool) SimappOption { + return func(command []string) []string { + if lean { + return append(command, optionSimappLean) + } + return command + } +} + +// SimappWithCommit provides commit option for the simapp command +func SimappWithCommit(commit bool) SimappOption { + return func(command []string) []string { + if commit { + return append(command, optionSimappCommit) + } + return command + } +} + +// SimappWithSimulateEveryOperation provides simulateEveryOperation option for the simapp command +func SimappWithSimulateEveryOperation(simulateEveryOperation bool) SimappOption { + return func(command []string) []string { + if simulateEveryOperation { + return append(command, optionSimappSimulateEveryOperation) + } + return command + } +} + +// SimappWithPrintAllInvariants provides printAllInvariants option for the simapp command +func SimappWithPrintAllInvariants(printAllInvariants bool) SimappOption { + return func(command []string) []string { + if printAllInvariants { + return append(command, optionSimappPrintAllInvariants) + } + return command + } +} + +// SimappWithEnable provides enable option for the simapp command +func SimappWithEnable(enable bool) SimappOption { + return func(command []string) []string { + if enable { + return append(command, optionSimappEnabled) + } + return command + } +} + +// SimappWithVerbose provides verbose option for the simapp command +func SimappWithVerbose(verbose bool) SimappOption { + return func(command []string) []string { + if verbose { + return append(command, optionSimappVerbose) + } + return command + } +} + +// SimappWithPeriod provides period option for the simapp command +func SimappWithPeriod(period uint) SimappOption { + return func(command []string) []string { + return append(command, optionSimappPeriod, strconv.Itoa(int(period))) + } +} + +// SimappWithGenesisTime provides genesisTime option for the simapp command +func SimappWithGenesisTime(genesisTime int64) SimappOption { + return func(command []string) []string { + return append(command, optionSimappGenesisTime, strconv.Itoa(int(genesisTime))) + } +} + +// SimulationCommand returns the cli command for simapp tests +func SimulationCommand(appPath string, options ...SimappOption) step.Option { + command := []string{ + commandGoTest, + optionGoBenchmem, + optionGoSimappRun, + optionGoSimappBench, + filepath.Join(appPath, "app"), + } + + // Apply the options provided by the user + for _, applyOption := range options { + command = applyOption(command) + } + return step.Exec(gocmd.Name(), command...) +} diff --git a/starport/pkg/clispinner/icon.go b/starport/pkg/clispinner/icon.go new file mode 100644 index 0000000000..e7ddc59c0c --- /dev/null +++ b/starport/pkg/clispinner/icon.go @@ -0,0 +1,9 @@ +package clispinner + +import "github.com/fatih/color" + +var ( + // OK is an OK mark. + OK = color.New(color.FgGreen).SprintFunc()("âś”") + Bullet = color.New(color.FgYellow).SprintFunc()("⋆") +) diff --git a/starport/pkg/confile/confile_test.go b/starport/pkg/confile/confile_test.go index 37837fa219..3059f29902 100644 --- a/starport/pkg/confile/confile_test.go +++ b/starport/pkg/confile/confile_test.go @@ -2,7 +2,6 @@ package confile import ( "io" - "io/ioutil" "os" "strings" "testing" @@ -28,7 +27,7 @@ func TestAll(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - file, err := ioutil.TempFile("", "") + file, err := os.CreateTemp("", "") require.NoError(t, err) defer func() { file.Close() diff --git a/starport/pkg/cosmosclient/cosmosclient.go b/starport/pkg/cosmosclient/cosmosclient.go index 1a538f05b5..16f6ed51df 100644 --- a/starport/pkg/cosmosclient/cosmosclient.go +++ b/starport/pkg/cosmosclient/cosmosclient.go @@ -12,7 +12,7 @@ import ( "sync" "time" - "github.com/cenkalti/backoff/v4" + "github.com/cenkalti/backoff" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -35,7 +35,9 @@ import ( // FaucetTransferEnsureDuration is the duration that BroadcastTx will wait when a faucet transfer // is triggered prior to broadcasting but transfer's tx is not committed in the state yet. -var FaucetTransferEnsureDuration = time.Minute * 2 +var FaucetTransferEnsureDuration = time.Second * 40 + +var errCannotRetrieveFundsFromFaucet = errors.New("cannot retrieve funds from faucet") const ( defaultNodeAddress = "http://localhost:26657" @@ -266,16 +268,16 @@ func (c Client) BroadcastTxWithProvision(accountName string, msgs ...sdktypes.Ms return 0, nil, err } - context := c.Context. + ctx := c.Context. WithFromName(accountName). WithFromAddress(accountAddress) - txf, err := prepareFactory(context, c.Factory) + txf, err := prepareFactory(ctx, c.Factory) if err != nil { return 0, nil, err } - _, gas, err = tx.CalculateGas(context, txf, msgs...) + _, gas, err = tx.CalculateGas(ctx, txf, msgs...) if err != nil { return 0, nil, err } @@ -294,14 +296,14 @@ func (c Client) BroadcastTxWithProvision(accountName string, msgs ...sdktypes.Ms return Response{}, err } - txBytes, err := context.TxConfig.TxEncoder()(txUnsigned.GetTx()) + txBytes, err := ctx.TxConfig.TxEncoder()(txUnsigned.GetTx()) if err != nil { return Response{}, err } - resp, err := context.BroadcastTx(txBytes) + resp, err := ctx.BroadcastTx(txBytes) return Response{ - codec: context.Codec, + codec: ctx.Codec, TxResponse: resp, }, handleBroadcastResult(resp, err) }, nil @@ -343,14 +345,14 @@ func (c *Client) makeSureAccountHasTokens(ctx context.Context, address string) e fc := cosmosfaucet.NewClient(c.faucetAddress) faucetResp, err := fc.Transfer(ctx, cosmosfaucet.TransferRequest{AccountAddress: address}) if err != nil { - return errors.Wrap(err, "faucet server request failed") + return errors.Wrap(errCannotRetrieveFundsFromFaucet, err.Error()) } if faucetResp.Error != "" { - return fmt.Errorf("cannot retrieve tokens from faucet: %s", faucetResp.Error) + return errors.Wrap(errCannotRetrieveFundsFromFaucet, faucetResp.Error) } for _, transfer := range faucetResp.Transfers { if transfer.Error != "" { - return fmt.Errorf("cannot retrieve tokens from faucet: %s", transfer.Error) + return errors.Wrap(errCannotRetrieveFundsFromFaucet, transfer.Error) } } @@ -363,24 +365,20 @@ func (c *Client) makeSureAccountHasTokens(ctx context.Context, address string) e }, backoff.WithContext(backoff.NewConstantBackOff(time.Second), ctx)) } -func (c *Client) checkAccountBalance(ctx context.Context, address string) (err error) { - balancesResp, err := banktypes.NewQueryClient(c.Context).AllBalances(ctx, &banktypes.QueryAllBalancesRequest{ +func (c *Client) checkAccountBalance(ctx context.Context, address string) error { + resp, err := banktypes.NewQueryClient(c.Context).Balance(ctx, &banktypes.QueryBalanceRequest{ Address: address, + Denom: c.faucetDenom, }) if err != nil { return err } - // if the balance is enough do nothing. - if len(balancesResp.Balances) > 0 { - for _, coin := range balancesResp.Balances { - if coin.Denom == c.faucetDenom && coin.Amount.Uint64() >= c.faucetMinAmount { - return nil - } - } + if resp.Balance.Amount.Uint64() >= c.faucetMinAmount { + return nil } - return errors.New("account has not enough balance") + return fmt.Errorf("account has not enough %q balance, min. required amount: %d", c.faucetDenom, c.faucetMinAmount) } // handleBroadcastResult handles the result of broadcast messages result and checks if an error occurred diff --git a/starport/pkg/cosmoserror/error.go b/starport/pkg/cosmoserror/error.go new file mode 100644 index 0000000000..8c85eb4e74 --- /dev/null +++ b/starport/pkg/cosmoserror/error.go @@ -0,0 +1,30 @@ +package cosmoserror + +import ( + "errors" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + ErrInternal = errors.New("some invariants expected by the underlying system has been broken") + ErrInvalidRequest = errors.New("invalid GRPC request argument or object not found") +) + +func Unwrap(err error) error { + s, ok := status.FromError(err) + if ok { + switch s.Code() { + case codes.InvalidArgument: + return ErrInvalidRequest + case codes.Internal: + return ErrInternal + } + } + unwrapped := errors.Unwrap(err) + if unwrapped != nil { + return unwrapped + } + return err +} diff --git a/starport/pkg/cosmosfaucet/cosmosfaucet.go b/starport/pkg/cosmosfaucet/cosmosfaucet.go index ed56a9d33c..65b1cae7c3 100644 --- a/starport/pkg/cosmosfaucet/cosmosfaucet.go +++ b/starport/pkg/cosmosfaucet/cosmosfaucet.go @@ -43,6 +43,9 @@ type Faucet struct { // accountMnemonic is the mnemonic of the account. accountMnemonic string + // coinType registered coin type number for HD derivation (BIP-0044). + coinType string + // coins keeps a list of coins that can be distributed by the faucet. coins []coin @@ -73,10 +76,11 @@ type Option func(*Faucet) // Account provides the account information to transfer tokens from. // when mnemonic isn't provided, account assumed to be exists in the keyring. -func Account(name, mnemonic string) Option { +func Account(name, mnemonic string, coinType string) Option { return func(f *Faucet) { f.accountName = name f.accountMnemonic = mnemonic + f.coinType = coinType } } @@ -137,7 +141,7 @@ func New(ctx context.Context, ccr chaincmdrunner.Runner, options ...Option) (Fau // import the account if mnemonic is provided. if f.accountMnemonic != "" { - _, err := f.runner.AddAccount(ctx, f.accountName, f.accountMnemonic) + _, err := f.runner.AddAccount(ctx, f.accountName, f.accountMnemonic, f.coinType) if err != nil && err != chaincmdrunner.ErrAccountAlreadyExists { return Faucet{}, err } diff --git a/starport/pkg/cosmosgen/generate_go.go b/starport/pkg/cosmosgen/generate_go.go index 55b4e55368..8a7e584958 100644 --- a/starport/pkg/cosmosgen/generate_go.go +++ b/starport/pkg/cosmosgen/generate_go.go @@ -1,7 +1,6 @@ package cosmosgen import ( - "io/ioutil" "os" "path/filepath" @@ -27,7 +26,7 @@ func (g *generator) generateGo() error { // created a temporary dir to locate generated code under which later only some of them will be moved to the // app's source code. this also prevents having leftover files in the app's source code or its parent dir -when // command executed directly there- in case of an interrupt. - tmp, err := ioutil.TempDir("", "") + tmp, err := os.MkdirTemp("", "") if err != nil { return err } @@ -53,10 +52,12 @@ func (g *generator) generateGo() error { _, err = os.Stat(generatedPath) if err == nil { err = copy.Copy(generatedPath, g.appPath) - return errors.Wrap(err, "cannot copy path") - } - if !os.IsNotExist(err) { + if err != nil { + return errors.Wrap(err, "cannot copy path") + } + } else if !os.IsNotExist(err) { return err } + return nil } diff --git a/starport/pkg/cosmosgen/generate_javascript.go b/starport/pkg/cosmosgen/generate_javascript.go index 313f22ba12..2520b6d308 100644 --- a/starport/pkg/cosmosgen/generate_javascript.go +++ b/starport/pkg/cosmosgen/generate_javascript.go @@ -2,7 +2,6 @@ package cosmosgen import ( "context" - "io/ioutil" "os" "path/filepath" "strings" @@ -114,7 +113,7 @@ func (g *jsGenerator) generateModule(ctx context.Context, tsprotoPluginPath, app } // generate OpenAPI spec. - oaitemp, err := ioutil.TempDir("", "gen-js-openapi-module-spec") + oaitemp, err := os.MkdirTemp("", "gen-js-openapi-module-spec") if err != nil { return err } diff --git a/starport/pkg/cosmosgen/generate_openapi.go b/starport/pkg/cosmosgen/generate_openapi.go index 32ce5a1b83..77c663ad79 100644 --- a/starport/pkg/cosmosgen/generate_openapi.go +++ b/starport/pkg/cosmosgen/generate_openapi.go @@ -1,7 +1,6 @@ package cosmosgen import ( - "io/ioutil" "os" "path/filepath" "sort" @@ -43,7 +42,7 @@ func generateOpenAPISpec(g *generator) error { return err } - dir, err := ioutil.TempDir("", "gen-openapi-module-spec") + dir, err := os.MkdirTemp("", "gen-openapi-module-spec") if err != nil { return err } diff --git a/starport/pkg/cosmosgen/templates/js/index.ts.tpl b/starport/pkg/cosmosgen/templates/js/index.ts.tpl index eef97b9fc6..2b757fc7ca 100644 --- a/starport/pkg/cosmosgen/templates/js/index.ts.tpl +++ b/starport/pkg/cosmosgen/templates/js/index.ts.tpl @@ -13,7 +13,7 @@ const types = [ ]; export const MissingWalletError = new Error("wallet is required"); -const registry = new Registry(types); +export const registry = new Registry(types); const defaultFee = { amount: [], @@ -31,13 +31,17 @@ interface SignAndBroadcastOptions { const txClient = async (wallet: OfflineSigner, { addr: addr }: TxClientOptions = { addr: "http://localhost:26657" }) => { if (!wallet) throw MissingWalletError; - - const client = await SigningStargateClient.connectWithSigner(addr, wallet, { registry }); + let client; + if (addr) { + client = await SigningStargateClient.connectWithSigner(addr, wallet, { registry }); + }else{ + client = await SigningStargateClient.offline( wallet, { registry }); + } const { address } = (await wallet.getAccounts())[0]; return { signAndBroadcast: (msgs: EncodeObject[], { fee, memo }: SignAndBroadcastOptions = {fee: defaultFee, memo: ""}) => client.signAndBroadcast(address, msgs, fee,memo), - {{ range .Module.Msgs }}{{ camelCase .Name }}: (data: {{ .Name }}): EncodeObject => ({ typeUrl: "/{{ .URI }}", value: data }), + {{ range .Module.Msgs }}{{ camelCase .Name }}: (data: {{ .Name }}): EncodeObject => ({ typeUrl: "/{{ .URI }}", value: {{ .Name }}.fromPartial( data ) }), {{ end }} }; }; diff --git a/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl b/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl index b748c82d42..6ddd8004f7 100644 --- a/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl +++ b/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl @@ -1,4 +1,4 @@ -import { txClient, queryClient, MissingWalletError } from './module' +import { txClient, queryClient, MissingWalletError , registry} from './module' // @ts-ignore import { SpVuexError } from '@starport/vuex' @@ -49,6 +49,7 @@ const getDefaultState = () => { {{ range .Module.Types }}{{ .Name }}: getStructure({{ .Name }}.fromPartial({})), {{ end }} }, + _Registry: registry, _Subscriptions: new Set(), } } @@ -67,10 +68,10 @@ export default { state[query][JSON.stringify(key)] = value }, SUBSCRIBE(state, subscription) { - state._Subscriptions.add(subscription) + state._Subscriptions.add(JSON.stringify(subscription)) }, UNSUBSCRIBE(state, subscription) { - state._Subscriptions.delete(subscription) + state._Subscriptions.delete(JSON.stringify(subscription)) } }, getters: { @@ -83,6 +84,9 @@ export default { {{ end }} getTypeStructure: (state) => (type) => { return state._Structure[type].fields + }, + getRegistry: (state) => { + return state._Registry } }, actions: { @@ -103,7 +107,8 @@ export default { async StoreUpdate({ state, dispatch }) { state._Subscriptions.forEach(async (subscription) => { try { - await dispatch(subscription.action, subscription.payload) + const sub=JSON.parse(subscription) + await dispatch(sub.action, sub.payload) }catch(e) { throw new SpVuexError('Subscriptions: ' + e.message) } @@ -117,8 +122,9 @@ export default { {{ if (gt $i 0) }} {{ $n = inc $i }} {{ end}} - async {{ $FullName }}{{ $n }}({ commit, rootGetters, getters }, { options: { subscribe, all} = { subscribe:false, all:false}, params: {...key}, query=null }) { + async {{ $FullName }}{{ $n }}({ commit, rootGetters, getters }, { options: { subscribe, all} = { subscribe:false, all:false}, params, query=null }) { try { + const key = params ?? {}; const queryClient=await initQueryClient(rootGetters) let value= (await queryClient.{{ camelCase $FullName -}} {{- $n -}}( @@ -136,10 +142,10 @@ export default { )).data {{ if $rule.HasQuery }} - while (all && ( value).pagination && ( value).pagination.nextKey!=null) { + while (all && ( value).pagination && ( value).pagination.next_key!=null) { let next_values=(await queryClient.{{ camelCase $FullName -}} {{- $n -}}( - {{- range $j,$a :=$rule.Params }} key.{{$a}}, {{ end -}}{...query, 'pagination.key':( value).pagination.nextKey} + {{- range $j,$a :=$rule.Params }} key.{{$a}}, {{ end -}}{...query, 'pagination.key':( value).pagination.next_key} {{- if $rule.HasBody -}}, {...key} {{- end -}} )).data diff --git a/starport/pkg/cosmosutil/address.go b/starport/pkg/cosmosutil/address.go new file mode 100644 index 0000000000..1cceedeb51 --- /dev/null +++ b/starport/pkg/cosmosutil/address.go @@ -0,0 +1,25 @@ +package cosmosutil + +import ( + "errors" + + "github.com/cosmos/cosmos-sdk/types/bech32" +) + +// ChangeAddressPrefix returns the address with another prefix +func ChangeAddressPrefix(address, newPrefix string) (string, error) { + if newPrefix == "" { + return "", errors.New("empty prefix") + } + _, pubKey, err := bech32.DecodeAndConvert(address) + if err != nil { + return "", err + } + return bech32.ConvertAndEncode(newPrefix, pubKey) +} + +// GetAddressPrefix returns the bech 32 prefix used by the address +func GetAddressPrefix(address string) (string, error) { + prefix, _, err := bech32.DecodeAndConvert(address) + return prefix, err +} diff --git a/starport/pkg/cosmosutil/address_test.go b/starport/pkg/cosmosutil/address_test.go new file mode 100644 index 0000000000..4f699902fc --- /dev/null +++ b/starport/pkg/cosmosutil/address_test.go @@ -0,0 +1,80 @@ +package cosmosutil_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/starport/starport/pkg/cosmosutil" +) + +func TestChangePrefix(t *testing.T) { + tests := []struct { + name string + address string + prefix string + want string + wantErr bool + }{ + { + name: "cosmos address to spn address", + address: "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + prefix: "spn", + want: "spn1dd246yq6z5vzjz9gh8cff46pll75yyl8c5tt7g", + }, + { + name: "cosmos address to spn address 2", + address: "cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa", + prefix: "spn", + want: "spn1mmlqwyqk7neqegffp99q86eckpm4pjahdcne08", + }, + { + name: "cosmos validator address", + address: "cosmosvaloper1mmlqwyqk7neqegffp99q86eckpm4pjah5sl2dw", + prefix: "spn", + want: "spn1mmlqwyqk7neqegffp99q86eckpm4pjahdcne08", + }, + { + name: "mars address to earth address", + address: "mars1c6ac48k2ur8tl3tf0cpntlw5068kvp8xf4xq37", + prefix: "earth", + want: "earth1c6ac48k2ur8tl3tf0cpntlw5068kvp8x0xyl2v", + }, + { + name: "invalid bech32 address", + address: "mars1c6ac48k2ur9tl3tf0cpntlw5068kvp8xf4xq37", + prefix: "spn", + wantErr: true, + }, + { + name: "empty target prefix", + address: "mars1c6ac48k2ur8tl3tf0cpntlw5068kvp8xf4xq37", + prefix: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := cosmosutil.ChangeAddressPrefix(tt.address, tt.prefix) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func TestGetPrefix(t *testing.T) { + prefix, err := cosmosutil.GetAddressPrefix("cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj") + require.Equal(t, "cosmos", prefix) + require.NoError(t, err) + + prefix, err = cosmosutil.GetAddressPrefix("mars1c6ac48k2ur8tl3tf0cpntlw5068kvp8xf4xq37") + require.Equal(t, "mars", prefix) + require.NoError(t, err) + + // invalid bech32 address + _, err = cosmosutil.GetAddressPrefix("mars1c6ac48k2ur9tl3tf0cpntlw5068kvp8xf4xq37") + require.Error(t, err) +} diff --git a/starport/pkg/cosmosutil/genesis.go b/starport/pkg/cosmosutil/genesis.go new file mode 100644 index 0000000000..d2ac722d43 --- /dev/null +++ b/starport/pkg/cosmosutil/genesis.go @@ -0,0 +1,114 @@ +package cosmosutil + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "io" + "net/http" + "os" + "time" +) + +const genesisTimeField = "genesis_time" + +// ChainGenesis represents the stargate genesis file +type ChainGenesis struct { + AppState struct { + Auth struct { + Accounts []struct { + Address string `json:"address"` + } `json:"accounts"` + } `json:"auth"` + } `json:"app_state"` +} + +// HasAccount check if account exist into the genesis account +func (g ChainGenesis) HasAccount(address string) bool { + for _, account := range g.AppState.Auth.Accounts { + if account.Address == address { + return true + } + } + return false +} + +// ParseGenesis parse ChainGenesis object from a genesis file +func ParseGenesis(genesisPath string) (genesis ChainGenesis, err error) { + genesisFile, err := os.ReadFile(genesisPath) + if err != nil { + return genesis, errors.New("cannot open genesis file: " + err.Error()) + } + return genesis, json.Unmarshal(genesisFile, &genesis) +} + +// CheckGenesisContainsAddress returns true if the address exist into the genesis file +func CheckGenesisContainsAddress(genesisPath, addr string) (bool, error) { + _, err := os.Stat(genesisPath) + if os.IsNotExist(err) { + return false, nil + } else if err != nil { + return false, err + } + genesis, err := ParseGenesis(genesisPath) + if err != nil { + return false, err + } + return genesis.HasAccount(addr), nil +} + +// SetGenesisTime sets the genesis time inside a genesis file +func SetGenesisTime(genesisPath string, genesisTime int64) error { + // fetch and parse genesis + genesisBytes, err := os.ReadFile(genesisPath) + if err != nil { + return err + } + + var genesis map[string]interface{} + if err := json.Unmarshal(genesisBytes, &genesis); err != nil { + return err + } + + // check the genesis time with the RFC3339 standard format + formattedTime := time.Unix(genesisTime, 0).UTC().Format(time.RFC3339Nano) + + // modify and save the new genesis + genesis[genesisTimeField] = &formattedTime + genesisBytes, err = json.Marshal(genesis) + if err != nil { + return err + } + return os.WriteFile(genesisPath, genesisBytes, 0644) +} + +// GenesisAndHashFromURL fetches the genesis from the given url and returns its content along with the sha256 hash. +func GenesisAndHashFromURL(ctx context.Context, url string) (genesis []byte, hash string, err error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, "", err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, "", err + } + defer resp.Body.Close() + + genesis, err = io.ReadAll(resp.Body) + if err != nil { + return nil, "", err + } + + h := sha256.New() + if _, err := io.Copy(h, bytes.NewReader(genesis)); err != nil { + return nil, "", err + } + + hexHash := hex.EncodeToString(h.Sum(nil)) + + return genesis, hexHash, nil +} diff --git a/starport/pkg/cosmosutil/genesis_test.go b/starport/pkg/cosmosutil/genesis_test.go new file mode 100644 index 0000000000..8f3ddd6b85 --- /dev/null +++ b/starport/pkg/cosmosutil/genesis_test.go @@ -0,0 +1,46 @@ +package cosmosutil_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/starport/starport/pkg/cosmosutil" +) + +const ( + genesisSample = ` +{ + "foo": "bar", + "genesis_time": "foobar" +} +` + unixTime = 1600000000 + rfcTime = "2020-09-13T12:26:40Z" +) + +func TestSetGenesisTime(t *testing.T) { + tmp, err := os.MkdirTemp("", "") + t.Cleanup(func() { os.RemoveAll(tmp) }) + tmpGenesis := filepath.Join(tmp, "genesis.json") + + // fails with no file + require.NoError(t, err) + require.Error(t, cosmosutil.SetGenesisTime(tmpGenesis, 0)) + + require.NoError(t, os.WriteFile(tmpGenesis, []byte(genesisSample), 0644)) + require.NoError(t, cosmosutil.SetGenesisTime(tmpGenesis, unixTime)) + + // check genesis modified value + var actual struct { + Foo string `json:"foo"` + GenesisTime string `json:"genesis_time"` + } + actualBytes, err := os.ReadFile(tmpGenesis) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(actualBytes, &actual)) + require.Equal(t, "bar", actual.Foo) + require.Equal(t, rfcTime, actual.GenesisTime) +} diff --git a/starport/pkg/cosmosutil/gentx.go b/starport/pkg/cosmosutil/gentx.go new file mode 100644 index 0000000000..932e81b071 --- /dev/null +++ b/starport/pkg/cosmosutil/gentx.go @@ -0,0 +1,90 @@ +package cosmosutil + +import ( + "bytes" + "encoding/json" + "errors" + "os" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var GentxFilename = "gentx.json" + +type ( + // PubKey represents the public key in bytes array + PubKey []byte + // GentxInfo represents the basic info about gentx file + GentxInfo struct { + DelegatorAddress string + PubKey PubKey + SelfDelegation sdk.Coin + } + // StargateGentx represents the stargate gentx file + StargateGentx struct { + Body struct { + Messages []struct { + DelegatorAddress string `json:"delegator_address"` + ValidatorAddress string `json:"validator_address"` + PubKey struct { + Key string `json:"key"` + } `json:"pubkey"` + Value struct { + Denom string `json:"denom"` + Amount string `json:"amount"` + } `json:"value"` + } `json:"messages"` + } `json:"body"` + } +) + +// Equal returns true if the public keys are equal +func (pb PubKey) Equal(key []byte) bool { + res := bytes.Compare(pb, key) + return res == 0 +} + +// GentxFromPath returns GentxInfo from the json file +func GentxFromPath(path string) (info GentxInfo, gentx []byte, err error) { + if _, err := os.Stat(path); os.IsNotExist(err) { + return info, gentx, errors.New("chain home folder is not initialized yet: " + path) + } + + gentx, err = os.ReadFile(path) + if err != nil { + return info, gentx, err + } + return ParseGentx(gentx) +} + +// ParseGentx returns GentxInfo and the gentx file in bytes +func ParseGentx(gentx []byte) (info GentxInfo, file []byte, err error) { + // Try parsing Stargate gentx + var stargateGentx StargateGentx + if err := json.Unmarshal(gentx, &stargateGentx); err != nil { + return info, gentx, err + } + if stargateGentx.Body.Messages == nil { + return info, gentx, errors.New("the gentx cannot be parsed") + } + + // This is a stargate gentx + if len(stargateGentx.Body.Messages) != 1 { + return info, gentx, errors.New("add validator gentx must contain 1 message") + } + + info.DelegatorAddress = stargateGentx.Body.Messages[0].DelegatorAddress + info.PubKey = []byte(stargateGentx.Body.Messages[0].PubKey.Key) + + amount, ok := sdk.NewIntFromString(stargateGentx.Body.Messages[0].Value.Amount) + if !ok { + return info, gentx, errors.New("the self-delegation inside the gentx is invalid") + } + + info.SelfDelegation = sdk.NewCoin( + stargateGentx.Body.Messages[0].Value.Denom, + amount, + ) + + return info, gentx, nil +} diff --git a/starport/pkg/cosmosutil/gentx_test.go b/starport/pkg/cosmosutil/gentx_test.go new file mode 100644 index 0000000000..918af12186 --- /dev/null +++ b/starport/pkg/cosmosutil/gentx_test.go @@ -0,0 +1,174 @@ +package cosmosutil_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "github.com/tendermint/starport/starport/pkg/cosmosutil" +) + +func TestChainGenesis_HasAccount(t *testing.T) { + type account struct { + Address string `json:"address"` + } + tests := []struct { + name string + accounts []string + address string + want bool + }{ + { + name: "found account", + address: "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + accounts: []string{ + "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + "cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa", + }, + want: true, + }, { + name: "not found account", + address: "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8pu8cup", + accounts: []string{ + "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + "cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa", + }, + want: false, + }, { + name: "empty accounts", + address: "cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa", + accounts: []string{}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := cosmosutil.ChainGenesis{} + for _, acc := range tt.accounts { + g.AppState.Auth.Accounts = append(g.AppState.Auth.Accounts, account{Address: acc}) + } + got := g.HasAccount(tt.address) + require.Equal(t, tt.want, got) + }) + } +} + +func TestParseGenesis(t *testing.T) { + tests := []struct { + name string + genesisPath string + wantAccounts []string + wantErr bool + }{ + { + name: "parse genesis file 1", + genesisPath: "testdata/genesis1.json", + wantAccounts: []string{"cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj"}, + }, { + name: "parse genesis file 2", + genesisPath: "testdata/genesis2.json", + wantAccounts: []string{"cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa"}, + }, { + name: "parse not found file", + genesisPath: "testdata/genesis_not_found.json", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotGenesis, err := cosmosutil.ParseGenesis(tt.genesisPath) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + gotAddrs := make([]string, 0) + for _, acc := range gotGenesis.AppState.Auth.Accounts { + gotAddrs = append(gotAddrs, acc.Address) + } + require.ElementsMatch(t, tt.wantAccounts, gotAddrs) + }) + } +} + +func TestParseGentx(t *testing.T) { + tests := []struct { + name string + gentxPath string + wantInfo cosmosutil.GentxInfo + wantErr bool + }{ + { + name: "parse gentx file 1", + gentxPath: "testdata/gentx1.json", + wantInfo: cosmosutil.GentxInfo{ + DelegatorAddress: "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + PubKey: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + SelfDelegation: sdk.Coin{ + Denom: "stake", + Amount: sdk.NewInt(95000000), + }, + }, + }, { + name: "parse gentx file 2", + gentxPath: "testdata/gentx2.json", + wantInfo: cosmosutil.GentxInfo{ + DelegatorAddress: "cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa", + PubKey: []byte("OL+EIoo7DwyaBFDbPbgAhwS5rvgIqoUa0x8qWqzfQVQ="), + SelfDelegation: sdk.Coin{ + Denom: "stake", + Amount: sdk.NewInt(95000000), + }, + }, + }, { + name: "parse invalid file", + gentxPath: "testdata/gentx_invalid.json", + wantErr: true, + }, { + name: "not found file", + gentxPath: "testdata/gentx_not_found.json", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotInfo, _, err := cosmosutil.GentxFromPath(tt.gentxPath) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.wantInfo, gotInfo) + }) + } +} + +func TestPubKey_Equal(t *testing.T) { + tests := []struct { + name string + pb []byte + cmpKey []byte + want bool + }{ + { + name: "equal public keys", + pb: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + cmpKey: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + want: true, + }, + { + name: "not equal public keys", + pb: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + cmpKey: []byte("EIoo7DwyaBFDbPbgAhwS5rvgIqoUa0x8qWqzfQVQ="), + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pb := cosmosutil.PubKey(tt.pb) + got := pb.Equal(tt.cmpKey) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/starport/pkg/cosmosutil/peer.go b/starport/pkg/cosmosutil/peer.go new file mode 100644 index 0000000000..d61ac0d844 --- /dev/null +++ b/starport/pkg/cosmosutil/peer.go @@ -0,0 +1,17 @@ +package cosmosutil + +import ( + "strings" +) + +// VerifyPeerFormat checks if the peer address format is valid +func VerifyPeerFormat(peer string) bool { + // Check the format of the peer + nodeHost := strings.Split(peer, "@") + if len(nodeHost) != 2 || + len(nodeHost[0]) == 0 || + len(nodeHost[1]) == 0 { + return false + } + return true +} diff --git a/starport/pkg/cosmosutil/peer_test.go b/starport/pkg/cosmosutil/peer_test.go new file mode 100644 index 0000000000..7b17b0afd1 --- /dev/null +++ b/starport/pkg/cosmosutil/peer_test.go @@ -0,0 +1,41 @@ +package cosmosutil + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVerifyPeerFormat(t *testing.T) { + tests := []struct { + peer string + want bool + }{ + { + peer: "nodeid@peer", + want: true, + }, + { + peer: "@peer", + want: false, + }, + { + peer: "nodeid@", + want: false, + }, + { + peer: "nodeid", + want: false, + }, + { + peer: "@", + want: false, + }, + } + for _, tt := range tests { + t.Run("verifying "+tt.peer, func(t *testing.T) { + got := VerifyPeerFormat(tt.peer) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/starport/pkg/cosmosutil/testdata/genesis1.json b/starport/pkg/cosmosutil/testdata/genesis1.json new file mode 100644 index 0000000000..14cc2b9c81 --- /dev/null +++ b/starport/pkg/cosmosutil/testdata/genesis1.json @@ -0,0 +1,274 @@ +{ + "app_hash": "", + "app_state": { + "auth": { + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "account_number": "0", + "address": "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + "pub_key": null, + "sequence": "0" + } + ], + "params": { + "max_memo_characters": "256", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + }, + "bank": { + "balances": [ + { + "address": "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + "coins": [ + { + "amount": "95000000", + "denom": "stake" + } + ] + } + ], + "denom_metadata": [], + "params": { + "default_send_enabled": true, + "send_enabled": [] + }, + "supply": [] + }, + "capability": { + "index": "1", + "owners": [] + }, + "crisis": { + "constant_fee": { + "amount": "1000", + "denom": "stake" + } + }, + "distribution": { + "delegator_starting_infos": [], + "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, + "outstanding_rewards": [], + "params": { + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "community_tax": "0.020000000000000000", + "withdraw_addr_enabled": true + }, + "previous_proposer": "", + "validator_accumulated_commissions": [], + "validator_current_rewards": [], + "validator_historical_rewards": [], + "validator_slash_events": [] + }, + "earth": { + "params": {} + }, + "evidence": { + "evidence": [] + }, + "feegrant": { + "allowances": [] + }, + "genutil": { + "gen_txs": [ + { + "auth_info": { + "fee": { + "amount": [], + "gas_limit": "200000", + "granter": "", + "payer": "" + }, + "signer_infos": [ + { + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AhLlX8QQEymFlvdKrb0xfYGHt7GTK8KiExAThDHQKSe4" + }, + "sequence": "0" + } + ] + }, + "body": { + "extension_options": [], + "memo": "9b1f4adbfb0c0b513040d914bfb717303c0eaa71@192.168.0.148:26656", + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "commission": { + "max_change_rate": "0.010000000000000000", + "max_rate": "0.200000000000000000", + "rate": "0.100000000000000000" + }, + "delegator_address": "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + "description": { + "details": "", + "identity": "", + "moniker": "default", + "security_contact": "", + "website": "" + }, + "min_self_delegation": "1", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs=" + }, + "validator_address": "cosmosvaloper1dd246yq6z5vzjz9gh8cff46pll75yyl8pu8cup", + "value": { + "amount": "95000000", + "denom": "stake" + } + } + ], + "non_critical_extension_options": [], + "timeout_height": "0" + }, + "signatures": [ + "sz0uixBOHJoZbvVrz670vLBRQ5Z2wnhHeNRxKJPz5dADKfz34/sg7FQv6nCeEomODMrgjUD70YBeguKIqxjcLw==" + ] + } + ] + }, + "gov": { + "deposit_params": { + "max_deposit_period": "172800s", + "min_deposit": [ + { + "amount": "10000000", + "denom": "stake" + } + ] + }, + "deposits": [], + "proposals": [], + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto_threshold": "0.334000000000000000" + }, + "votes": [], + "voting_params": { + "voting_period": "172800s" + } + }, + "ibc": { + "channel_genesis": { + "ack_sequences": [], + "acknowledgements": [], + "channels": [], + "commitments": [], + "next_channel_sequence": "0", + "receipts": [], + "recv_sequences": [], + "send_sequences": [] + }, + "client_genesis": { + "clients": [], + "clients_consensus": [], + "clients_metadata": [], + "create_localhost": false, + "next_client_sequence": "0", + "params": { + "allowed_clients": [ + "06-solomachine", + "07-tendermint" + ] + } + }, + "connection_genesis": { + "client_connection_paths": [], + "connections": [], + "next_connection_sequence": "0", + "params": { + "max_expected_time_per_block": "30000000000" + } + } + }, + "mint": { + "minter": { + "annual_provisions": "0.000000000000000000", + "inflation": "0.130000000000000000" + }, + "params": { + "blocks_per_year": "6311520", + "goal_bonded": "0.670000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "inflation_rate_change": "0.130000000000000000", + "mint_denom": "stake" + } + }, + "params": null, + "slashing": { + "missed_blocks": [], + "params": { + "downtime_jail_duration": "600s", + "min_signed_per_window": "0.500000000000000000", + "signed_blocks_window": "100", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": [] + }, + "staking": { + "delegations": [], + "exported": false, + "last_total_power": "0", + "last_validator_powers": [], + "params": { + "bond_denom": "stake", + "historical_entries": 10000, + "max_entries": 7, + "max_validators": 100, + "unbonding_time": "1814400s" + }, + "redelegations": [], + "unbonding_delegations": [], + "validators": [] + }, + "transfer": { + "denom_traces": [], + "params": { + "receive_enabled": true, + "send_enabled": true + }, + "port_id": "transfer" + }, + "upgrade": {}, + "vesting": {} + }, + "chain_id": "earth-1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age_duration": "172800000000000", + "max_age_num_blocks": "100000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + }, + "version": {} + }, + "genesis_time": "2021-11-12T02:08:12.522572Z", + "initial_height": "1" +} \ No newline at end of file diff --git a/starport/pkg/cosmosutil/testdata/genesis2.json b/starport/pkg/cosmosutil/testdata/genesis2.json new file mode 100644 index 0000000000..50513e9a7b --- /dev/null +++ b/starport/pkg/cosmosutil/testdata/genesis2.json @@ -0,0 +1,279 @@ +{ + "app_hash": "", + "app_state": { + "auth": { + "accounts": [ + { + "@type": "/cosmos.auth.v1beta1.BaseAccount", + "account_number": "0", + "address": "cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa", + "pub_key": null, + "sequence": "0" + } + ], + "params": { + "max_memo_characters": "256", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + }, + "bank": { + "balances": [ + { + "address": "cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa", + "coins": [ + { + "amount": "95000000", + "denom": "stake" + } + ] + } + ], + "denom_metadata": [], + "params": { + "default_send_enabled": true, + "send_enabled": [] + }, + "supply": [] + }, + "capability": { + "index": "1", + "owners": [] + }, + "crisis": { + "constant_fee": { + "amount": "1000", + "denom": "stake" + } + }, + "distribution": { + "delegator_starting_infos": [], + "delegator_withdraw_infos": [], + "fee_pool": { + "community_pool": [] + }, + "outstanding_rewards": [], + "params": { + "base_proposer_reward": "0.010000000000000000", + "bonus_proposer_reward": "0.040000000000000000", + "community_tax": "0.020000000000000000", + "withdraw_addr_enabled": true + }, + "previous_proposer": "", + "validator_accumulated_commissions": [], + "validator_current_rewards": [], + "validator_historical_rewards": [], + "validator_slash_events": [] + }, + "earth": { + "params": {} + }, + "evidence": { + "evidence": [] + }, + "feegrant": { + "allowances": [] + }, + "genutil": { + "gen_txs": [ + { + "auth_info": { + "fee": { + "amount": [ + { + "amount": "5000", + "denom": "stake" + } + ], + "gas_limit": "200000", + "granter": "", + "payer": "" + }, + "signer_infos": [ + { + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AslH/zmmjEHI/jWup3tC/TfG4eRiD959tyE9z98xt/oO" + }, + "sequence": "0" + } + ] + }, + "body": { + "extension_options": [], + "memo": "a412c917cb29f73cc3ad0592bbd0152fe0e690bd@192.168.0.148:26656", + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "commission": { + "max_change_rate": "0.010000000000000000", + "max_rate": "0.200000000000000000", + "rate": "0.100000000000000000" + }, + "delegator_address": "cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa", + "description": { + "details": "", + "identity": "", + "moniker": "alice", + "security_contact": "", + "website": "" + }, + "min_self_delegation": "1", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "OL+EIoo7DwyaBFDbPbgAhwS5rvgIqoUa0x8qWqzfQVQ=" + }, + "validator_address": "cosmosvaloper1mmlqwyqk7neqegffp99q86eckpm4pjah5sl2dw", + "value": { + "amount": "95000000", + "denom": "stake" + } + } + ], + "non_critical_extension_options": [], + "timeout_height": "0" + }, + "signatures": [ + "XDwkcX6QNRDL7FYD/UCNXmwVZHR7tCVyTh+VKAC8KJESZyCEo4/Uo9HGRX2pWGX0nrn/v5h2HKzHxXc/41rDag==" + ] + } + ] + }, + "gov": { + "deposit_params": { + "max_deposit_period": "172800s", + "min_deposit": [ + { + "amount": "10000000", + "denom": "stake" + } + ] + }, + "deposits": [], + "proposals": [], + "starting_proposal_id": "1", + "tally_params": { + "quorum": "0.334000000000000000", + "threshold": "0.500000000000000000", + "veto_threshold": "0.334000000000000000" + }, + "votes": [], + "voting_params": { + "voting_period": "172800s" + } + }, + "ibc": { + "channel_genesis": { + "ack_sequences": [], + "acknowledgements": [], + "channels": [], + "commitments": [], + "next_channel_sequence": "0", + "receipts": [], + "recv_sequences": [], + "send_sequences": [] + }, + "client_genesis": { + "clients": [], + "clients_consensus": [], + "clients_metadata": [], + "create_localhost": false, + "next_client_sequence": "0", + "params": { + "allowed_clients": [ + "06-solomachine", + "07-tendermint" + ] + } + }, + "connection_genesis": { + "client_connection_paths": [], + "connections": [], + "next_connection_sequence": "0", + "params": { + "max_expected_time_per_block": "30000000000" + } + } + }, + "mint": { + "minter": { + "annual_provisions": "0.000000000000000000", + "inflation": "0.130000000000000000" + }, + "params": { + "blocks_per_year": "6311520", + "goal_bonded": "0.670000000000000000", + "inflation_max": "0.200000000000000000", + "inflation_min": "0.070000000000000000", + "inflation_rate_change": "0.130000000000000000", + "mint_denom": "stake" + } + }, + "params": null, + "slashing": { + "missed_blocks": [], + "params": { + "downtime_jail_duration": "600s", + "min_signed_per_window": "0.500000000000000000", + "signed_blocks_window": "100", + "slash_fraction_double_sign": "0.050000000000000000", + "slash_fraction_downtime": "0.010000000000000000" + }, + "signing_infos": [] + }, + "staking": { + "delegations": [], + "exported": false, + "last_total_power": "0", + "last_validator_powers": [], + "params": { + "bond_denom": "stake", + "historical_entries": 10000, + "max_entries": 7, + "max_validators": 100, + "unbonding_time": "1814400s" + }, + "redelegations": [], + "unbonding_delegations": [], + "validators": [] + }, + "transfer": { + "denom_traces": [], + "params": { + "receive_enabled": true, + "send_enabled": true + }, + "port_id": "transfer" + }, + "upgrade": {}, + "vesting": {} + }, + "chain_id": "earth-1", + "consensus_params": { + "block": { + "max_bytes": "22020096", + "max_gas": "-1", + "time_iota_ms": "1000" + }, + "evidence": { + "max_age_duration": "172800000000000", + "max_age_num_blocks": "100000", + "max_bytes": "1048576" + }, + "validator": { + "pub_key_types": [ + "ed25519" + ] + }, + "version": {} + }, + "genesis_time": "2021-11-10T00:52:44.204026Z", + "initial_height": "1" +} \ No newline at end of file diff --git a/starport/pkg/cosmosutil/testdata/gentx1.json b/starport/pkg/cosmosutil/testdata/gentx1.json new file mode 100644 index 0000000000..3b4aecdefa --- /dev/null +++ b/starport/pkg/cosmosutil/testdata/gentx1.json @@ -0,0 +1,61 @@ +{ + "auth_info": { + "fee": { + "amount": [], + "gas_limit": "200000", + "granter": "", + "payer": "" + }, + "signer_infos": [ + { + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AhLlX8QQEymFlvdKrb0xfYGHt7GTK8KiExAThDHQKSe4" + }, + "sequence": "0" + } + ] + }, + "body": { + "extension_options": [], + "memo": "9b1f4adbfb0c0b513040d914bfb717303c0eaa71@192.168.0.148:26656", + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "commission": { + "max_change_rate": "0.010000000000000000", + "max_rate": "0.200000000000000000", + "rate": "0.100000000000000000" + }, + "delegator_address": "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + "description": { + "details": "", + "identity": "", + "moniker": "default", + "security_contact": "", + "website": "" + }, + "min_self_delegation": "1", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs=" + }, + "validator_address": "cosmosvaloper1dd246yq6z5vzjz9gh8cff46pll75yyl8pu8cup", + "value": { + "amount": "95000000", + "denom": "stake" + } + } + ], + "non_critical_extension_options": [], + "timeout_height": "0" + }, + "signatures": [ + "sz0uixBOHJoZbvVrz670vLBRQ5Z2wnhHeNRxKJPz5dADKfz34/sg7FQv6nCeEomODMrgjUD70YBeguKIqxjcLw==" + ] +} \ No newline at end of file diff --git a/starport/pkg/cosmosutil/testdata/gentx2.json b/starport/pkg/cosmosutil/testdata/gentx2.json new file mode 100644 index 0000000000..65c3342848 --- /dev/null +++ b/starport/pkg/cosmosutil/testdata/gentx2.json @@ -0,0 +1,66 @@ +{ + "auth_info": { + "fee": { + "amount": [ + { + "amount": "5000", + "denom": "stake" + } + ], + "gas_limit": "200000", + "granter": "", + "payer": "" + }, + "signer_infos": [ + { + "mode_info": { + "single": { + "mode": "SIGN_MODE_DIRECT" + } + }, + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "AslH/zmmjEHI/jWup3tC/TfG4eRiD959tyE9z98xt/oO" + }, + "sequence": "0" + } + ] + }, + "body": { + "extension_options": [], + "memo": "a412c917cb29f73cc3ad0592bbd0152fe0e690bd@192.168.0.148:26656", + "messages": [ + { + "@type": "/cosmos.staking.v1beta1.MsgCreateValidator", + "commission": { + "max_change_rate": "0.010000000000000000", + "max_rate": "0.200000000000000000", + "rate": "0.100000000000000000" + }, + "delegator_address": "cosmos1mmlqwyqk7neqegffp99q86eckpm4pjah3ytlpa", + "description": { + "details": "", + "identity": "", + "moniker": "alice", + "security_contact": "", + "website": "" + }, + "min_self_delegation": "1", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "OL+EIoo7DwyaBFDbPbgAhwS5rvgIqoUa0x8qWqzfQVQ=" + }, + "validator_address": "cosmosvaloper1mmlqwyqk7neqegffp99q86eckpm4pjah5sl2dw", + "value": { + "amount": "95000000", + "denom": "stake" + } + } + ], + "non_critical_extension_options": [], + "timeout_height": "0" + }, + "signatures": [ + "XDwkcX6QNRDL7FYD/UCNXmwVZHR7tCVyTh+VKAC8KJESZyCEo4/Uo9HGRX2pWGX0nrn/v5h2HKzHxXc/41rDag==" + ] +} \ No newline at end of file diff --git a/starport/pkg/cosmosutil/testdata/gentx_invalid.json b/starport/pkg/cosmosutil/testdata/gentx_invalid.json new file mode 100644 index 0000000000..e3153028f8 --- /dev/null +++ b/starport/pkg/cosmosutil/testdata/gentx_invalid.json @@ -0,0 +1,6 @@ +{ + "amount": [], + "gas_limit": "200000", + "granter": "", + "payer": "" +} \ No newline at end of file diff --git a/starport/pkg/dirchange/dirchange.go b/starport/pkg/dirchange/dirchange.go index e24e1ed60b..4e29b3eb26 100644 --- a/starport/pkg/dirchange/dirchange.go +++ b/starport/pkg/dirchange/dirchange.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/md5" "errors" - "io/ioutil" "os" "path/filepath" ) @@ -27,7 +26,7 @@ func SaveDirChecksum(workdir string, paths []string, checksumSavePath string, ch // save checksum checksumFilePath := filepath.Join(checksumSavePath, checksumName) - return ioutil.WriteFile(checksumFilePath, checksum, 0644) + return os.WriteFile(checksumFilePath, checksum, 0644) } // HasDirChecksumChanged computes the md5 checksum of the provided paths (directories or files) @@ -57,7 +56,7 @@ func HasDirChecksumChanged(workdir string, paths []string, checksumSavePath stri } // Compare checksums - savedChecksum, err := ioutil.ReadFile(checksumFilePath) + savedChecksum, err := os.ReadFile(checksumFilePath) if err != nil { return false, err } @@ -103,7 +102,7 @@ func checksumFromPaths(workdir string, paths []string) ([]byte, error) { noFile = false // write file content - content, err := ioutil.ReadFile(subPath) + content, err := os.ReadFile(subPath) if err != nil { return err } diff --git a/starport/pkg/dirchange/dirchange_test.go b/starport/pkg/dirchange/dirchange_test.go index 27d2214673..de41a9062e 100644 --- a/starport/pkg/dirchange/dirchange_test.go +++ b/starport/pkg/dirchange/dirchange_test.go @@ -2,11 +2,11 @@ package dirchange import ( "crypto/rand" - "github.com/stretchr/testify/require" - "io/ioutil" "os" "path/filepath" "testing" + + "github.com/stretchr/testify/require" ) const ( @@ -37,25 +37,25 @@ func TestHasDirChecksumChanged(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(dir3) - dir11, err := ioutil.TempDir(dir1, TmpPattern) + dir11, err := os.MkdirTemp(dir1, TmpPattern) require.NoError(t, err) - dir12, err := ioutil.TempDir(dir1, TmpPattern) + dir12, err := os.MkdirTemp(dir1, TmpPattern) require.NoError(t, err) - dir21, err := ioutil.TempDir(dir2, TmpPattern) + dir21, err := os.MkdirTemp(dir2, TmpPattern) require.NoError(t, err) // Create files - err = ioutil.WriteFile(filepath.Join(dir1, "foo"), []byte("some bytes"), 0644) + err = os.WriteFile(filepath.Join(dir1, "foo"), []byte("some bytes"), 0644) require.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(dir11, "foo"), randomBytes(15), 0644) + err = os.WriteFile(filepath.Join(dir11, "foo"), randomBytes(15), 0644) require.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(dir12, "foo"), randomBytes(20), 0644) + err = os.WriteFile(filepath.Join(dir12, "foo"), randomBytes(20), 0644) require.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(dir21, "foo"), randomBytes(20), 0644) + err = os.WriteFile(filepath.Join(dir21, "foo"), randomBytes(20), 0644) require.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(dir3, "foo1"), randomBytes(10), 0644) + err = os.WriteFile(filepath.Join(dir3, "foo1"), randomBytes(10), 0644) require.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(dir3, "foo2"), randomBytes(10), 0644) + err = os.WriteFile(filepath.Join(dir3, "foo2"), randomBytes(10), 0644) require.NoError(t, err) // Check checksum @@ -68,7 +68,7 @@ func TestHasDirChecksumChanged(t *testing.T) { // Checksum remains the same if a file is deleted and recreated with the same content err = os.Remove(filepath.Join(dir1, "foo")) require.NoError(t, err) - err = ioutil.WriteFile(filepath.Join(dir1, "foo"), []byte("some bytes"), 0644) + err = os.WriteFile(filepath.Join(dir1, "foo"), []byte("some bytes"), 0644) require.NoError(t, err) tmpChecksum, err := checksumFromPaths("", paths) require.NoError(t, err) @@ -92,7 +92,7 @@ func TestHasDirChecksumChanged(t *testing.T) { require.NotEqual(t, checksum, tmpChecksum) // Checksum changes if a file is modified - err = ioutil.WriteFile(filepath.Join(dir3, "foo1"), randomBytes(10), 0644) + err = os.WriteFile(filepath.Join(dir3, "foo1"), randomBytes(10), 0644) require.NoError(t, err) newChecksum, err := checksumFromPaths("", paths) require.NoError(t, err) @@ -111,13 +111,13 @@ func TestHasDirChecksumChanged(t *testing.T) { require.Error(t, err) // SaveDirChecksum saves the checksum in the specified dir - saveDir, err := ioutil.TempDir(tempDir, TmpPattern) + saveDir, err := os.MkdirTemp(tempDir, TmpPattern) require.NoError(t, err) defer os.RemoveAll(saveDir) err = SaveDirChecksum("", paths, saveDir, ChecksumFile) require.NoError(t, err) require.FileExists(t, filepath.Join(saveDir, ChecksumFile)) - fileContent, err := ioutil.ReadFile(filepath.Join(saveDir, ChecksumFile)) + fileContent, err := os.ReadFile(filepath.Join(saveDir, ChecksumFile)) require.NoError(t, err) require.Equal(t, newChecksum, fileContent) @@ -131,7 +131,7 @@ func TestHasDirChecksumChanged(t *testing.T) { require.False(t, changed) // Return true if checksum file doesn't exist - newSaveDir, err := ioutil.TempDir(tempDir, TmpPattern) + newSaveDir, err := os.MkdirTemp(tempDir, TmpPattern) require.NoError(t, err) defer os.RemoveAll(newSaveDir) changed, err = HasDirChecksumChanged("", paths, newSaveDir, ChecksumFile) @@ -144,7 +144,7 @@ func TestHasDirChecksumChanged(t *testing.T) { require.True(t, changed) // Return true if it has been changed - err = ioutil.WriteFile(filepath.Join(dir21, "bar"), randomBytes(20), 0644) + err = os.WriteFile(filepath.Join(dir21, "bar"), randomBytes(20), 0644) require.NoError(t, err) changed, err = HasDirChecksumChanged("", paths, saveDir, ChecksumFile) require.NoError(t, err) diff --git a/starport/pkg/entrywriter/entrywriter.go b/starport/pkg/entrywriter/entrywriter.go new file mode 100644 index 0000000000..fa713eb6d8 --- /dev/null +++ b/starport/pkg/entrywriter/entrywriter.go @@ -0,0 +1,61 @@ +package entrywriter + +import ( + "fmt" + "io" + "strings" + "text/tabwriter" + + "github.com/pkg/errors" +) + +var ErrInvalidFormat = errors.New("invalid entry format") + +// MustWrite writes into out the tabulated entries and panic if the entry format is invalid +func MustWrite(out io.Writer, header []string, entries ...[]string) error { + err := Write(out, header, entries...) + if errors.Is(err, ErrInvalidFormat) { + panic(err) + } + return err +} + +// Write writes into out the tabulated entries +func Write(out io.Writer, header []string, entries ...[]string) error { + w := &tabwriter.Writer{} + w.Init(out, 0, 8, 0, '\t', 0) + + formatLine := func(line []string, title bool) (formatted string) { + for _, cell := range line { + if title { + cell = strings.Title(cell) + } + formatted += fmt.Sprintf("%s \t", cell) + } + return formatted + } + + if len(header) == 0 { + return errors.Wrap(ErrInvalidFormat, "empty header") + } + + // write header + if _, err := fmt.Fprintln(w, formatLine(header, true)); err != nil { + return err + } + + // write entries + for i, entry := range entries { + if len(entry) != len(header) { + return errors.Wrapf(ErrInvalidFormat, "entry %d doesn't match header length", i) + } + if _, err := fmt.Fprintf(w, formatLine(entry, false)+"\n"); err != nil { + return err + } + } + + if _, err := fmt.Fprintln(w); err != nil { + return err + } + return w.Flush() +} diff --git a/starport/pkg/entrywriter/entrywriter_test.go b/starport/pkg/entrywriter/entrywriter_test.go new file mode 100644 index 0000000000..55e4ce620f --- /dev/null +++ b/starport/pkg/entrywriter/entrywriter_test.go @@ -0,0 +1,39 @@ +package entrywriter_test + +import ( + "errors" + "io" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/starport/starport/pkg/entrywriter" +) + +type WriterWithError struct{} + +func (WriterWithError) Write(_ []byte) (n int, err error) { + return 0, errors.New("writer with error") +} + +func TestWrite(t *testing.T) { + header := []string{"foobar", "bar", "foo"} + + entries := [][]string{ + {"foo", "bar", "foobar"}, + {"bar", "foobar", "foo"}, + {"foobar", "foo", "bar"}, + } + + require.NoError(t, entrywriter.Write(io.Discard, header, entries...)) + require.NoError(t, entrywriter.Write(io.Discard, header), "should allow no entry") + + err := entrywriter.Write(io.Discard, []string{}) + require.ErrorIs(t, err, entrywriter.ErrInvalidFormat, "should prevent no header") + + entries[0] = []string{"foo", "bar"} + err = entrywriter.Write(io.Discard, header, entries...) + require.ErrorIs(t, err, entrywriter.ErrInvalidFormat, "should prevent entry length mismatch") + + var wErr WriterWithError + require.Error(t, entrywriter.Write(wErr, header, entries...), "should catch writer errors") +} diff --git a/starport/pkg/field/field.go b/starport/pkg/field/field.go deleted file mode 100644 index 3d28e4cb7e..0000000000 --- a/starport/pkg/field/field.go +++ /dev/null @@ -1,181 +0,0 @@ -// Package field provides methods to parse a field provided in a command with the format name:type -package field - -import ( - "fmt" - "strings" - - "github.com/tendermint/starport/starport/pkg/multiformatname" -) - -const ( - TypeCustom = "custom" - TypeString = "string" - TypeBool = "bool" - TypeInt = "int" - TypeInt32 = "int32" - TypeUint = "uint" - TypeUint64 = "uint64" - - TypeSeparator = ":" -) - -var ( - StaticDataTypes = map[string]string{ - TypeString: TypeString, - TypeBool: TypeBool, - TypeInt: TypeInt32, - TypeUint: TypeUint64, - } -) - -type ( - // Field represents a field inside a structure for a component - // it can be a field contained in a type or inside the response of a query, etc... - Field struct { - Name multiformatname.Name - Datatype string - DatatypeName string - Nested Fields - } - - // Fields represents a Field slice - Fields []Field -) - -// validateField validate the field name and type, and run the forbidden method -func validateField(field string, isForbiddenField func(string) error) (multiformatname.Name, string, error) { - fieldSplit := strings.Split(field, TypeSeparator) - if len(fieldSplit) > 2 { - return multiformatname.Name{}, "", fmt.Errorf("invalid field format: %s, should be 'name' or 'name:type'", field) - } - - name, err := multiformatname.NewName(fieldSplit[0]) - if err != nil { - return name, "", err - - } - - // Ensure the field name is not a Go reserved name, it would generate an incorrect code - if err := isForbiddenField(name.LowerCamel); err != nil { - return name, "", fmt.Errorf("%s can't be used as a field name: %s", name, err.Error()) - } - - // Check if the object has an explicit type. The default is a string - dataTypeName := TypeString - isTypeSpecified := len(fieldSplit) == 2 - if isTypeSpecified { - dataTypeName = fieldSplit[1] - } - return name, dataTypeName, nil -} - -// ParseFields parses the provided fields, analyses the types -// and checks there is no duplicated field -func ParseFields( - fields []string, - isForbiddenField func(string) error, -) (Fields, error) { - // Used to check duplicated field - existingFields := make(map[string]bool) - - var parsedFields Fields - for _, field := range fields { - name, datatypeName, err := validateField(field, isForbiddenField) - if err != nil { - return parsedFields, err - } - - // Ensure the field is not duplicated - if _, exists := existingFields[name.LowerCamel]; exists { - return parsedFields, fmt.Errorf("the field %s is duplicated", name.Original) - } - existingFields[name.LowerCamel] = true - - // Check if is a static type - if datatype, ok := StaticDataTypes[datatypeName]; ok { - parsedFields = append(parsedFields, Field{ - Name: name, - Datatype: datatype, - DatatypeName: datatypeName, - }) - continue - } - - parsedFields = append(parsedFields, Field{ - Name: name, - Datatype: datatypeName, - DatatypeName: TypeCustom, - }) - } - return parsedFields, nil -} - -// GetDatatype return the Datatype based in the DatatypeName -func (f Field) GetDatatype() string { - switch f.DatatypeName { - case TypeString, TypeBool, TypeInt, TypeUint: - return f.Datatype - case TypeCustom: - return fmt.Sprintf("*%s", f.Datatype) - default: - panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) - } -} - -// GetProtoDatatype return the proto Datatype based in the DatatypeName -func (f Field) GetProtoDatatype() string { - switch f.DatatypeName { - case TypeString, TypeBool, TypeInt, TypeUint, TypeCustom: - return f.Datatype - default: - panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) - } -} - -// NeedCastImport return true if the field slice -// needs import the cast library -func (f Fields) NeedCastImport() bool { - for _, field := range f { - if field.DatatypeName != TypeString && - field.DatatypeName != TypeCustom { - return true - } - } - return false -} - -// IsComplex return true if the field slice -// needs import the json library -func (f Fields) IsComplex() bool { - for _, field := range f { - if field.DatatypeName == TypeCustom { - return true - } - } - return false -} - -// String return all inline fields args for command usage -func (f Fields) String() string { - args := "" - for _, field := range f { - args += fmt.Sprintf(" [%s]", field.Name.Kebab) - } - return args -} - -// Custom return a list of custom fields -func (f Fields) Custom() []string { - fields := make([]string, 0) - for _, field := range f { - if field.DatatypeName == TypeCustom { - dataType, err := multiformatname.NewName(field.Datatype) - if err != nil { - panic(err) - } - fields = append(fields, dataType.Snake) - } - } - return fields -} diff --git a/starport/pkg/field/field_test.go b/starport/pkg/field/field_test.go deleted file mode 100644 index b9f5975ca0..0000000000 --- a/starport/pkg/field/field_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package field_test - -import ( - "errors" - "github.com/stretchr/testify/require" - "github.com/tendermint/starport/starport/pkg/field" - "github.com/tendermint/starport/starport/pkg/multiformatname" - "testing" -) - -var ( - noCheck = func(string) error { return nil } - alwaysInvalid = func(string) error { return errors.New("invalid name") } -) - -type testCases struct { - provided []string - expected field.Fields -} - -func TestParseFields(t *testing.T) { - names := []string{ - "foo", - "bar", - "fooBar", - "bar-foo", - "foo_foo", - } - cases := testCases{ - provided: []string{ - names[0], - names[1] + ":string", - names[2] + ":bool", - names[3] + ":int", - names[4] + ":uint", - }, - expected: field.Fields{ - { - Datatype: "string", - DatatypeName: "string", - }, - { - Datatype: "string", - DatatypeName: "string", - }, - { - Datatype: "bool", - DatatypeName: "bool", - }, - { - Datatype: "int32", - DatatypeName: "int", - }, - { - Datatype: "uint64", - DatatypeName: "uint", - }, - }, - } - for i, name := range names { - cases.expected[i].Name, _ = multiformatname.NewName(name) - } - - actual, err := field.ParseFields(cases.provided, noCheck) - require.NoError(t, err) - require.Equal(t, cases.expected, actual) - - // No field provided - actual, err = field.ParseFields([]string{}, noCheck) - require.NoError(t, err) - require.Empty(t, actual) -} - -func TestParseFields2(t *testing.T) { - // test failing cases - - // check doesn't pass - _, err := field.ParseFields([]string{"foo"}, alwaysInvalid) - require.Error(t, err) - - // duplicated field - _, err = field.ParseFields([]string{"foo", "foo:int"}, noCheck) - require.Error(t, err) - - // invalid type - _, err = field.ParseFields([]string{"foo:invalid"}, alwaysInvalid) - require.Error(t, err) - - // invalid field name - _, err = field.ParseFields([]string{"foo@bar:int"}, alwaysInvalid) - require.Error(t, err) - - // invalid format - _, err = field.ParseFields([]string{"foo:int:int"}, alwaysInvalid) - require.Error(t, err) -} diff --git a/starport/pkg/gomodule/gomodule.go b/starport/pkg/gomodule/gomodule.go index 1f24b46f5e..6b0e5d04a7 100644 --- a/starport/pkg/gomodule/gomodule.go +++ b/starport/pkg/gomodule/gomodule.go @@ -8,7 +8,7 @@ import ( "fmt" "io" "io/fs" - "io/ioutil" + "os" "path/filepath" "github.com/tendermint/starport/starport/pkg/cmdrunner" @@ -22,7 +22,7 @@ var ErrGoModNotFound = errors.New("go.mod not found") // ParseAt finds and parses go.mod at app's path. func ParseAt(path string) (*modfile.File, error) { - gomod, err := ioutil.ReadFile(filepath.Join(path, "go.mod")) + gomod, err := os.ReadFile(filepath.Join(path, "go.mod")) if err != nil { if errors.Is(err, fs.ErrNotExist) { return nil, ErrGoModNotFound diff --git a/starport/pkg/numbers/numbers.go b/starport/pkg/numbers/numbers.go index 3129d4ef25..8fcc32c076 100644 --- a/starport/pkg/numbers/numbers.go +++ b/starport/pkg/numbers/numbers.go @@ -2,29 +2,88 @@ package numbers import ( "fmt" + "sort" "strconv" "strings" ) -// ParseList parses comma separated numbers to []int. -func ParseList(list string) ([]int, error) { - ints := []int{} - for _, number := range strings.Split(list, ",") { - trimmed := strings.TrimSpace(number) - if trimmed == "" { +const ( + separator = "," + sepRange = "-" +) + +// ParseList parses comma separated numbers and range to []uint64. +func ParseList(arg string) ([]uint64, error) { + result := make([]uint64, 0) + listNumbers := make(map[uint64]struct{}) + // Split the slice by the separator + for _, numberRange := range strings.Split(arg, separator) { + trimmedRange := strings.TrimSpace(numberRange) + if trimmedRange == "" { continue } - i, err := strconv.ParseInt(trimmed, 10, 32) - if err != nil { - return nil, err + + // Split the number by the separator range + numbers := strings.Split(trimmedRange, sepRange) + switch len(numbers) { + // Parse a single number + case 1: + trimmed := strings.TrimSpace(numbers[0]) + i, err := strconv.ParseUint(trimmed, 10, 32) + if err != nil { + return nil, err + } + if _, ok := listNumbers[i]; ok { + continue + } + listNumbers[i] = struct{}{} + result = append(result, i) + + // Parse a range number (eg: 3-7) + case 2: + var ( + startN = strings.TrimSpace(numbers[0]) + endN = strings.TrimSpace(numbers[1]) + ) + if startN == "" { + startN = endN + } + if endN == "" { + endN = startN + } + if startN == "" { + continue + } + start, err := strconv.ParseUint(startN, 10, 32) + if err != nil { + return nil, err + } + end, err := strconv.ParseUint(endN, 10, 32) + if err != nil { + return nil, err + } + if start > end { + start, end = end, start + } + for ; start <= end; start++ { + if _, ok := listNumbers[start]; ok { + continue + } + listNumbers[start] = struct{}{} + result = append(result, start) + } + default: + return nil, fmt.Errorf("cannot parse the number range: %s", trimmedRange) } - ints = append(ints, int(i)) } - return ints, nil + sort.Slice(result, func(i, j int) bool { + return result[i] < result[j] + }) + return result, nil } -// List creates a comma separated int list with optional prefix for each int. -func List(numbers []int, prefix string) string { +// List creates a comma separated int list with optional prefix for each uint64. +func List(numbers []uint64, prefix string) string { var s []string for _, n := range numbers { s = append(s, fmt.Sprintf("%s%d", prefix, n)) diff --git a/starport/pkg/numbers/numbers_test.go b/starport/pkg/numbers/numbers_test.go index e721bc809c..4fdb712eca 100644 --- a/starport/pkg/numbers/numbers_test.go +++ b/starport/pkg/numbers/numbers_test.go @@ -10,15 +10,28 @@ import ( func TestParseList(t *testing.T) { cases := []struct { list string - parsed []int + parsed []uint64 }{ - {"1,2,3", []int{1, 2, 3}}, - {"1, 2,3 ", []int{1, 2, 3}}, - {",1, 2,", []int{1, 2}}, - {",", []int{}}, + {"1,2,3", []uint64{1, 2, 3}}, + {"1, 2,3 ", []uint64{1, 2, 3}}, + {",1, 2,", []uint64{1, 2}}, + {"1-3 ", []uint64{1, 2, 3}}, + {"1-3,8 ", []uint64{1, 2, 3, 8}}, + {"1-3,8-11 ", []uint64{1, 2, 3, 8, 9, 10, 11}}, + {"1-3,8-11,33 ", []uint64{1, 2, 3, 8, 9, 10, 11, 33}}, + {"1-3,8-11,33-36 ", []uint64{1, 2, 3, 8, 9, 10, 11, 33, 34, 35, 36}}, + {"1-5,2-7,9-11,1-8 ", []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}}, + {",", []uint64{}}, + {",-", []uint64{}}, + {",10-", []uint64{10}}, + {"10-", []uint64{10}}, + {"-10", []uint64{10}}, + {"10-10", []uint64{10}}, + {"12-8", []uint64{8, 9, 10, 11, 12}}, + {"12-8,4-1", []uint64{1, 2, 3, 4, 8, 9, 10, 11, 12}}, } - for i, tt := range cases { - t.Run(fmt.Sprintf("no: %d", i), func(t *testing.T) { + for _, tt := range cases { + t.Run("list "+tt.list, func(t *testing.T) { parsed, err := ParseList(tt.list) require.NoError(t, err) require.Equal(t, tt.parsed, parsed) @@ -28,12 +41,12 @@ func TestParseList(t *testing.T) { func TestList(t *testing.T) { cases := []struct { - parsed []int + parsed []uint64 list string }{ - {[]int{1, 2, 3}, "#1, #2, #3"}, - {[]int{1}, "#1"}, - {[]int{}, ""}, + {[]uint64{1, 2, 3}, "#1, #2, #3"}, + {[]uint64{1}, "#1"}, + {[]uint64{}, ""}, } for i, tt := range cases { t.Run(fmt.Sprintf("no: %d", i), func(t *testing.T) { diff --git a/starport/pkg/placeholder/tracer.go b/starport/pkg/placeholder/tracer.go index 641634a2f0..49444811a6 100644 --- a/starport/pkg/placeholder/tracer.go +++ b/starport/pkg/placeholder/tracer.go @@ -41,6 +41,7 @@ func New(opts ...Option) *Tracer { type Replacer interface { Replace(content, placeholder, replacement string) string + ReplaceAll(content, placeholder, replacement string) string ReplaceOnce(content, placeholder, replacement string) string AppendMiscError(miscError string) } @@ -52,6 +53,15 @@ type Tracer struct { additionalInfo string } +// ReplaceAll replace all placeholders in content with replacement string. +func (t *Tracer) ReplaceAll(content, placeholder, replacement string) string { + if strings.Count(content, placeholder) == 0 { + t.missing.Add(placeholder) + return content + } + return strings.ReplaceAll(content, placeholder, replacement) +} + // Replace placeholder in content with replacement string once. func (t *Tracer) Replace(content, placeholder, replacement string) string { // NOTE(dshulyak) we will count twice. once here and second time in strings.Replace diff --git a/starport/pkg/placeholder/tracer_test.go b/starport/pkg/placeholder/tracer_test.go index a4bceaf05c..693e61097e 100644 --- a/starport/pkg/placeholder/tracer_test.go +++ b/starport/pkg/placeholder/tracer_test.go @@ -55,3 +55,45 @@ func TestReplace(t *testing.T) { }) } } + +func TestReplaceAll(t *testing.T) { + for _, tc := range []struct { + desc string + content string + replace []string + missing []string + }{ + { + desc: "FoundAll", + content: "#one #one #two", + replace: []string{"#one", "#two"}, + }, + { + desc: "MissingAll", + content: "", + replace: []string{"#one", "#two"}, + missing: []string{"#one", "#two"}, + }, + { + desc: "MissingOne", + content: "#two #two", + replace: []string{"#one", "#two"}, + missing: []string{"#one"}, + }, + } { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + tr := New() + content := tc.content + for _, placeholder := range tc.replace { + content = tr.ReplaceAll(content, placeholder, "") + } + err := tr.Err() + if err != nil { + require.ErrorIs(t, err, newErrMissingPlaceholder(tc.missing)) + } else { + require.Empty(t, tc.missing) + } + }) + } +} diff --git a/starport/pkg/plushhelpers/cast.go b/starport/pkg/plushhelpers/cast.go deleted file mode 100644 index 83f015bf35..0000000000 --- a/starport/pkg/plushhelpers/cast.go +++ /dev/null @@ -1,73 +0,0 @@ -package plushhelpers - -import ( - "fmt" - "strings" - - "github.com/tendermint/starport/starport/pkg/field" -) - -// castArg returns the line of code to cast a value received from CLI of type string into its datatype -// Don't forget to import github.com/spf13/cast in templates -func castArg(prefix string, f field.Field, argIndex int) string { - switch f.DatatypeName { - case field.TypeString: - return fmt.Sprintf("%s%s := args[%d]", prefix, f.Name.UpperCamel, argIndex) - case field.TypeUint, field.TypeInt, field.TypeBool: - return fmt.Sprintf(`%s%s, err := cast.To%sE(args[%d]) - if err != nil { - return err - }`, - prefix, f.Name.UpperCamel, strings.Title(f.Datatype), argIndex) - case field.TypeCustom: - return fmt.Sprintf(`%[1]v%[2]v := new(types.%[3]v) - err = json.Unmarshal([]byte(args[%[4]v]), %[1]v%[2]v) - if err != nil { - return err - }`, prefix, f.Name.UpperCamel, f.Datatype, argIndex) - default: - panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) - } -} - -// CastToBytes returns the lines of code to cast a value into bytes -// the name of the cast type variable is [name]Bytes -func CastToBytes(varName string, datatypeName string) string { - switch datatypeName { - case field.TypeString: - return fmt.Sprintf("%[1]vBytes := []byte(%[1]v)", varName) - case field.TypeUint: - return fmt.Sprintf(`%[1]vBytes := make([]byte, 8) - binary.BigEndian.PutUint64(%[1]vBytes, %[1]v)`, varName) - case field.TypeInt: - return fmt.Sprintf(`%[1]vBytes := make([]byte, 4) - binary.BigEndian.PutUint32(%[1]vBytes, uint32(%[1]v))`, varName) - case field.TypeBool: - return fmt.Sprintf(`%[1]vBytes := []byte{0} - if %[1]v { - %[1]vBytes = []byte{1} - }`, varName) - case field.TypeCustom: - return fmt.Sprintf(`%[1]vBufferBytes := new(bytes.Buffer) - json.NewEncoder(%[1]vBytes).Encode(%[1]v) - %[1]vBytes := reqBodyBytes.Bytes()`, varName) - default: - panic(fmt.Sprintf("unknown type %s", datatypeName)) - } -} - -// CastToString returns the lines of code to cast a value into bytes -func CastToString(varName string, datatypeName string) string { - switch datatypeName { - case field.TypeString: - return varName - case field.TypeUint, field.TypeInt: - return fmt.Sprintf("strconv.Itoa(int(%s))", varName) - case field.TypeBool: - return fmt.Sprintf("strconv.FormatBool(%s)", varName) - case field.TypeCustom: - return fmt.Sprintf("fmt.Sprintf(\"%s\", %s)", "%+v", varName) - default: - panic(fmt.Sprintf("unknown type %s", datatypeName)) - } -} diff --git a/starport/pkg/plushhelpers/generate.go b/starport/pkg/plushhelpers/generate.go deleted file mode 100644 index e42b8257f8..0000000000 --- a/starport/pkg/plushhelpers/generate.go +++ /dev/null @@ -1,84 +0,0 @@ -package plushhelpers - -import ( - "fmt" - - "github.com/tendermint/starport/starport/pkg/field" -) - -const ( - valueFalse = "false" - valueNull = "null" -) - -// GenerateValidArg will produce a valid value for the specified type -// This function doesn't guarantee to produce unique values -// Note that return value needs to be wrapped into a string -func GenerateValidArg(datatypeName string) string { - switch datatypeName { - case field.TypeString: - return "xyz" - case field.TypeUint, field.TypeInt: - return "111" - case field.TypeBool: - return valueFalse - case field.TypeCustom: - return valueNull - default: - panic(fmt.Sprintf("unknown type %s", datatypeName)) - } -} - -// GenerateUniqueArg returns the line of code for the iterated value i for the type datatypeName -// The value is unique depending on i, except for bool which always returns true -// This method must be placed in the template inside a loop with an iterator i -func GenerateUniqueArg(datatypeName string) string { - switch datatypeName { - case field.TypeString: - return "strconv.Itoa(i)" - case field.TypeUint: - return "uint64(i)" - case field.TypeInt: - return "int32(i)" - case field.TypeBool: - return valueFalse - case field.TypeCustom: - return valueNull - default: - panic(fmt.Sprintf("unknown type %s", datatypeName)) - } -} - -// GenerateValidIndex returns the line of code for a valid index for a map depending on the type -func GenerateValidIndex(datatypeName string) string { - switch datatypeName { - case field.TypeString: - return "strconv.Itoa(0)" - case field.TypeUint, field.TypeInt: - return "0" - case field.TypeBool: - return valueFalse - case field.TypeCustom: - return valueNull - default: - panic(fmt.Sprintf("unknown type %s", datatypeName)) - } -} - -// GenerateNotFoundIndex returns the line of code for an index that doesn't exist for a map -// This is used for map tests generation, for test cases where the type is not found for the specified index -// NOTE: This method is not reliable for tests with a map with only booleans as indexes -func GenerateNotFoundIndex(datatypeName string) string { - switch datatypeName { - case field.TypeString: - return "strconv.Itoa(100000)" - case field.TypeUint, field.TypeInt: - return "100000" - case field.TypeBool: - return valueFalse - case field.TypeCustom: - return valueNull - default: - panic(fmt.Sprintf("unknown type %s", datatypeName)) - } -} diff --git a/starport/pkg/plushhelpers/plushhelpers.go b/starport/pkg/plushhelpers/plushhelpers.go deleted file mode 100644 index 8b8dce43a5..0000000000 --- a/starport/pkg/plushhelpers/plushhelpers.go +++ /dev/null @@ -1,19 +0,0 @@ -package plushhelpers - -import ( - "strings" - - "github.com/gobuffalo/plush" -) - -// ExtendPlushContext sets available helpers on the provided context. -func ExtendPlushContext(ctx *plush.Context) { - ctx.Set("castArg", castArg) - ctx.Set("castToBytes", CastToBytes) - ctx.Set("castToString", CastToString) - ctx.Set("genValidArg", GenerateValidArg) - ctx.Set("genUniqueArg", GenerateUniqueArg) - ctx.Set("genValidIndex", GenerateValidIndex) - ctx.Set("genNotFoundIndex", GenerateNotFoundIndex) - ctx.Set("title", strings.Title) -} diff --git a/starport/pkg/protoc-gen-dart/data/protoc-gen-dart_darwin_amd64 b/starport/pkg/protoc-gen-dart/data/protoc-gen-dart_darwin_amd64 index da365fec79..4011531a45 100755 Binary files a/starport/pkg/protoc-gen-dart/data/protoc-gen-dart_darwin_amd64 and b/starport/pkg/protoc-gen-dart/data/protoc-gen-dart_darwin_amd64 differ diff --git a/starport/pkg/protoc-gen-dart/data/protoc-gen-dart_linux_amd64 b/starport/pkg/protoc-gen-dart/data/protoc-gen-dart_linux_amd64 index 5013e6aaf4..3ed03d7d92 100755 Binary files a/starport/pkg/protoc-gen-dart/data/protoc-gen-dart_linux_amd64 and b/starport/pkg/protoc-gen-dart/data/protoc-gen-dart_linux_amd64 differ diff --git a/starport/pkg/xgenny/run.go b/starport/pkg/xgenny/run.go index a86ecfb591..ce0ef4b911 100644 --- a/starport/pkg/xgenny/run.go +++ b/starport/pkg/xgenny/run.go @@ -4,9 +4,11 @@ import ( "context" "errors" "os" + "strings" "github.com/gobuffalo/genny" "github.com/gobuffalo/logger" + "github.com/gobuffalo/packd" "github.com/tendermint/starport/starport/pkg/placeholder" "github.com/tendermint/starport/starport/pkg/validation" ) @@ -24,7 +26,7 @@ func (d *dryRunError) ValidationInfo() string { // DryRunner is a genny DryRunner with a logger func DryRunner(ctx context.Context) *genny.Runner { - runner := genny.DryRunner(context.Background()) + runner := genny.DryRunner(ctx) runner.Logger = logger.New(genny.DefaultLogLvl) return runner } @@ -80,3 +82,22 @@ func RunWithValidation( } return sm, nil } + +// Box will mount each file in the Box and wrap it, already existing files are ignored +func Box(g *genny.Generator, box packd.Walker) error { + return box.Walk(func(path string, bf packd.File) error { + f := genny.NewFile(path, bf) + f, err := g.Transform(f) + if err != nil { + return err + } + filePath := strings.TrimSuffix(f.Name(), ".plush") + _, err = os.Stat(filePath) + if os.IsNotExist(err) { + // path doesn't exist. move on. + g.File(f) + return nil + } + return err + }) +} diff --git a/starport/pkg/xhttp/response_test.go b/starport/pkg/xhttp/response_test.go index 36c574bc57..451eb8246e 100644 --- a/starport/pkg/xhttp/response_test.go +++ b/starport/pkg/xhttp/response_test.go @@ -3,7 +3,7 @@ package xhttp import ( "encoding/json" "errors" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" @@ -20,7 +20,7 @@ func TestResponseJSON(t *testing.T) { require.Equal(t, http.StatusCreated, resp.StatusCode) require.Equal(t, "application/json", resp.Header.Get("Content-Type")) - body, _ := ioutil.ReadAll(resp.Body) + body, _ := io.ReadAll(resp.Body) dataJSON, _ := json.Marshal(data) require.Equal(t, dataJSON, body) } diff --git a/starport/pkg/xtime/unix.go b/starport/pkg/xtime/unix.go new file mode 100644 index 0000000000..6dfe5d8e11 --- /dev/null +++ b/starport/pkg/xtime/unix.go @@ -0,0 +1,21 @@ +package xtime + +import ( + "time" +) + +// Seconds creates a time.Duration based on the seconds parameter +func Seconds(seconds uint64) time.Duration { + return time.Duration(seconds) * time.Second +} + +// NowAfter returns a unix date string from now plus the duration +func NowAfter(unix time.Duration) string { + date := time.Now().Add(unix) + return FormatUnix(date) +} + +// FormatUnix formats the time.Time to unix date string +func FormatUnix(date time.Time) string { + return date.Format(time.UnixDate) +} diff --git a/starport/pkg/xtime/unix_test.go b/starport/pkg/xtime/unix_test.go new file mode 100644 index 0000000000..069743d336 --- /dev/null +++ b/starport/pkg/xtime/unix_test.go @@ -0,0 +1,66 @@ +package xtime + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestSeconds(t *testing.T) { + tests := []uint64{ + 9999999999, + 10000, + 100, + 0, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("test %d value", tt), func(t *testing.T) { + got := Seconds(tt) + require.Equal(t, time.Duration(tt)*time.Second, got) + }) + } +} + +func TestNowAfter(t *testing.T) { + tests := []uint64{ + 9999999999, + 10000, + 100, + 0, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("test %d value", tt), func(t *testing.T) { + got := NowAfter(Seconds(tt)) + date := time.Now().Add(time.Duration(tt) * time.Second) + require.Equal(t, date.Format(time.UnixDate), got) + }) + } +} + +func TestFormatUnix(t *testing.T) { + tests := []struct { + date time.Time + want string + }{ + { + date: time.Time{}, + want: "Mon Jan 1 00:00:00 UTC 0001", + }, + { + date: time.Unix(10000000000, 100).In(time.UTC), + want: "Sat Nov 20 17:46:40 UTC 2286", + }, + { + date: time.Date(2020, 10, 11, 12, 30, 50, 0, time.FixedZone("Europe/Berlin", 3*60*60)), + want: "Sun Oct 11 12:30:50 Europe/Berlin 2020", + }, + } + for _, tt := range tests { + t.Run("test date "+tt.date.String(), func(t *testing.T) { + got := FormatUnix(tt.date) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/starport/pkg/yaml/yaml.go b/starport/pkg/yaml/yaml.go new file mode 100644 index 0000000000..9bb1569443 --- /dev/null +++ b/starport/pkg/yaml/yaml.go @@ -0,0 +1,43 @@ +package yaml + +import ( + "context" + "errors" + "strings" + + "github.com/goccy/go-yaml" + "github.com/goccy/go-yaml/parser" +) + +// Marshal converts an object to a string in a YAML format and transforms +// the byte slice fields from the path to string to be more readable. +func Marshal(ctx context.Context, obj interface{}, paths ...string) (string, error) { + requestYaml, err := yaml.MarshalContext(ctx, obj) + if err != nil { + return "", err + } + file, err := parser.ParseBytes(requestYaml, 0) + if err != nil { + return "", err + } + + // normalize the structure converting the byte slice fields to string + for _, path := range paths { + pathString, err := yaml.PathString(path) + if err != nil { + return "", err + } + var byteSlice []byte + err = pathString.Read(strings.NewReader(string(requestYaml)), &byteSlice) + if err != nil && !errors.Is(err, yaml.ErrNotFoundNode) { + return "", err + } + if err := pathString.ReplaceWithReader(file, + strings.NewReader(string(byteSlice)), + ); err != nil { + return "", err + } + } + + return file.String(), nil +} diff --git a/starport/pkg/yaml/yaml_test.go b/starport/pkg/yaml/yaml_test.go new file mode 100644 index 0000000000..4b166dd445 --- /dev/null +++ b/starport/pkg/yaml/yaml_test.go @@ -0,0 +1,149 @@ +package yaml + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMarshal(t *testing.T) { + type byteSliceParser struct { + Field1 string `json:"field1"` + Field2 struct { + Field1 []byte `json:"field1"` + Field2 struct { + Field1 []byte `json:"field1"` + Field2 string `json:"field2"` + } `json:"field2"` + Field3 string `json:"field3"` + } `json:"field2"` + Field3 string `json:"field3"` + } + bParser := &byteSliceParser{ + Field1: "field1", + Field3: "field3", + } + bParser.Field2.Field1 = []byte("field1") + bParser.Field2.Field3 = "field3" + bParser.Field2.Field2.Field1 = []byte("field1") + bParser.Field2.Field2.Field2 = "field2" + + type simpleParser struct { + Field1 string `json:"field1"` + Field2 string `json:"field2"` + } + sParser := &simpleParser{ + Field1: "field1", + Field2: "field2", + } + + type args struct { + obj interface{} + paths []string + } + tests := []struct { + name string + args args + want string + err error + }{ + { + name: "parse nil obj", + want: "null", + }, + { + name: "parse map without byte slice", + args: args{ + obj: map[string]string{ + "field1": "field1", + "field2": "field2", + }, + }, + want: `field1: field1 +field2: field2`, + }, + { + name: "parse map with byte slice", + args: args{ + obj: map[string][]byte{ + "field1": []byte("field1"), + "field2": []byte("field2"), + }, + paths: []string{ + "$.field1", + "$.field2", + }, + }, + want: `field1: field1 +field2: field2`, + }, + { + name: "parse struct without byte slice", + args: args{ + obj: sParser, + }, + want: `field1: field1 +field2: field2`, + }, + { + name: "parse struct with byte slice", + args: args{ + obj: bParser, + paths: []string{ + "$.field2.field1", + "$.field2.field2.field1", + }, + }, + want: `field1: field1 +field2: + field1: field1 + field2: + field1: field1 + field2: field2 + field3: field3 +field3: field3`, + }, + { + name: "parse struct with byte slice and wrong path", + args: args{ + obj: bParser, + paths: []string{ + "$.field2.field30", + "$.field2.field31", + }, + }, + want: `field1: field1 +field2: + field1: + - 102 + - 105 + - 101 + - 108 + - 100 + - 49 + field2: + field1: + - 102 + - 105 + - 101 + - 108 + - 100 + - 49 + field2: field2 + field3: field3 +field3: field3`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Marshal(context.Background(), tt.args.obj, tt.args.paths...) + if tt.err != nil { + require.ErrorIs(t, tt.err, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/starport/services/chain/chain.go b/starport/services/chain/chain.go index d9c69de477..ca63fcb5df 100644 --- a/starport/services/chain/chain.go +++ b/starport/services/chain/chain.go @@ -3,13 +3,12 @@ package chain import ( "context" "io" - "io/ioutil" "os" "path/filepath" "github.com/go-git/go-git/v5" "github.com/gookit/color" - conf "github.com/tendermint/starport/starport/chainconf" + "github.com/tendermint/starport/starport/chainconfig" sperrors "github.com/tendermint/starport/starport/errors" "github.com/tendermint/starport/starport/pkg/chaincmd" chaincmdrunner "github.com/tendermint/starport/starport/pkg/chaincmd/runner" @@ -143,8 +142,8 @@ func New(path string, options ...Option) (*Chain, error) { app: app, logLevel: LogSilent, serveRefresher: make(chan struct{}, 1), - stdout: ioutil.Discard, - stderr: ioutil.Discard, + stdout: io.Discard, + stderr: io.Discard, } // Apply the options @@ -210,7 +209,7 @@ func (c *Chain) ConfigPath() string { if c.options.ConfigFile != "" { return c.options.ConfigFile } - path, err := conf.LocateDefault(c.app.Path) + path, err := chainconfig.LocateDefault(c.app.Path) if err != nil { return "" } @@ -218,12 +217,12 @@ func (c *Chain) ConfigPath() string { } // Config returns the config of the chain -func (c *Chain) Config() (conf.Config, error) { +func (c *Chain) Config() (chainconfig.Config, error) { configPath := c.ConfigPath() if configPath == "" { - return conf.DefaultConf, nil + return chainconfig.DefaultConf, nil } - return conf.ParseFile(configPath) + return chainconfig.ParseFile(configPath) } // ID returns the chain's id. @@ -247,6 +246,10 @@ func (c *Chain) ID() (string, error) { return c.app.N(), nil } +func (c *Chain) Name() string { + return c.app.N() +} + // Binary returns the name of app's default (appd) binary. func (c *Chain) Binary() (string, error) { conf, err := c.Config() @@ -261,6 +264,11 @@ func (c *Chain) Binary() (string, error) { return c.app.D(), nil } +// SetHome sets the chain home directory. +func (c *Chain) SetHome(home string) { + c.options.homePath = home +} + // Home returns the blockchain node's home dir. func (c *Chain) Home() (string, error) { // check if home is explicitly defined for the app @@ -295,6 +303,15 @@ func (c *Chain) DefaultHome() (string, error) { return c.plugin.Home(), nil } +// DefaultGentxPath returns default gentx.json path of the app. +func (c *Chain) DefaultGentxPath() (string, error) { + home, err := c.Home() + if err != nil { + return "", err + } + return filepath.Join(home, "config/gentx/gentx.json"), nil +} + // GenesisPath returns genesis.json path of the app. func (c *Chain) GenesisPath() (string, error) { home, err := c.Home() @@ -304,6 +321,15 @@ func (c *Chain) GenesisPath() (string, error) { return filepath.Join(home, "config/genesis.json"), nil } +// GentxsPath returns the directory where gentxs are stored for the app. +func (c *Chain) GentxsPath() (string, error) { + home, err := c.Home() + if err != nil { + return "", err + } + return filepath.Join(home, "config/gentx"), nil +} + // AppTOMLPath returns app.toml path of the app. func (c *Chain) AppTOMLPath() (string, error) { home, err := c.Home() diff --git a/starport/services/chain/faucet.go b/starport/services/chain/faucet.go index 32de9ff451..7a8cec3bc9 100644 --- a/starport/services/chain/faucet.go +++ b/starport/services/chain/faucet.go @@ -62,7 +62,7 @@ func (c *Chain) Faucet(ctx context.Context) (cosmosfaucet.Faucet, error) { } faucetOptions := []cosmosfaucet.Option{ - cosmosfaucet.Account(*conf.Faucet.Name, ""), + cosmosfaucet.Account(*conf.Faucet.Name, "", ""), cosmosfaucet.ChainID(id), cosmosfaucet.OpenAPI(xurl.HTTP(apiAddress)), } diff --git a/starport/services/chain/init.go b/starport/services/chain/init.go index c980c838b8..cc87f140c1 100644 --- a/starport/services/chain/init.go +++ b/starport/services/chain/init.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/imdario/mergo" - conf "github.com/tendermint/starport/starport/chainconf" + "github.com/tendermint/starport/starport/chainconfig" chaincmdrunner "github.com/tendermint/starport/starport/pkg/chaincmd/runner" "github.com/tendermint/starport/starport/pkg/confile" ) @@ -124,7 +124,7 @@ func (c *Chain) InitChain(ctx context.Context) error { } // InitAccounts initializes the chain accounts and creates validator gentxs -func (c *Chain) InitAccounts(ctx context.Context, conf conf.Config) error { +func (c *Chain) InitAccounts(ctx context.Context, conf chainconfig.Config) error { commands, err := c.Commands(ctx) if err != nil { return err @@ -137,7 +137,7 @@ func (c *Chain) InitAccounts(ctx context.Context, conf conf.Config) error { // If the account doesn't provide an address, we create one if accountAddress == "" { - generatedAccount, err = commands.AddAccount(ctx, account.Name, account.Mnemonic) + generatedAccount, err = commands.AddAccount(ctx, account.Name, account.Mnemonic, account.CoinType) if err != nil { return err } @@ -167,20 +167,28 @@ func (c *Chain) InitAccounts(ctx context.Context, conf conf.Config) error { } } - // create the gentx from the validator from the config - if _, err := c.plugin.Gentx(ctx, commands, Validator{ + _, err = c.IssueGentx(ctx, Validator{ Name: conf.Validator.Name, StakingAmount: conf.Validator.Staked, - }); err != nil { - return err + }) + return err +} + +// IssueGentx generates a gentx from the validator information in chain config and import it in the chain genesis +func (c Chain) IssueGentx(ctx context.Context, v Validator) (string, error) { + commands, err := c.Commands(ctx) + if err != nil { + return "", err } - // import the gentx into the genesis - if err := commands.CollectGentxs(ctx); err != nil { - return err + // create the gentx from the validator from the config + gentxPath, err := c.plugin.Gentx(ctx, commands, v) + if err != nil { + return "", err } - return nil + // import the gentx into the genesis + return gentxPath, commands.CollectGentxs(ctx) } // IsInitialized checks if the chain is initialized @@ -212,6 +220,10 @@ type Validator struct { CommissionMaxChangeRate string MinSelfDelegation string GasPrices string + Details string + Identity string + Website string + SecurityContact string } // Account represents an account in the chain. @@ -219,5 +231,6 @@ type Account struct { Name string Address string Mnemonic string `json:"mnemonic"` + CoinType string Coins string } diff --git a/starport/services/chain/plugin-stargate.go b/starport/services/chain/plugin-stargate.go index 51563f76d7..d302a82147 100644 --- a/starport/services/chain/plugin-stargate.go +++ b/starport/services/chain/plugin-stargate.go @@ -5,12 +5,10 @@ import ( "os" "path/filepath" - chaincmdrunner "github.com/tendermint/starport/starport/pkg/chaincmd/runner" - - "github.com/tendermint/starport/starport/pkg/chaincmd" - "github.com/pelletier/go-toml" - starportconf "github.com/tendermint/starport/starport/chainconf" + "github.com/tendermint/starport/starport/chainconfig" + "github.com/tendermint/starport/starport/pkg/chaincmd" + chaincmdrunner "github.com/tendermint/starport/starport/pkg/chaincmd/runner" "github.com/tendermint/starport/starport/pkg/cosmosver" "github.com/tendermint/starport/starport/pkg/xurl" ) @@ -40,10 +38,14 @@ func (p *stargatePlugin) Gentx(ctx context.Context, runner chaincmdrunner.Runner chaincmd.GentxWithCommissionMaxChangeRate(v.CommissionMaxChangeRate), chaincmd.GentxWithMinSelfDelegation(v.MinSelfDelegation), chaincmd.GentxWithGasPrices(v.GasPrices), + chaincmd.GentxWithDetails(v.Details), + chaincmd.GentxWithIdentity(v.Identity), + chaincmd.GentxWithWebsite(v.Website), + chaincmd.GentxWithSecurityContact(v.SecurityContact), ) } -func (p *stargatePlugin) Configure(homePath string, conf starportconf.Config) error { +func (p *stargatePlugin) Configure(homePath string, conf chainconfig.Config) error { if err := p.appTOML(homePath, conf); err != nil { return err } @@ -53,7 +55,7 @@ func (p *stargatePlugin) Configure(homePath string, conf starportconf.Config) er return p.configTOML(homePath, conf) } -func (p *stargatePlugin) appTOML(homePath string, conf starportconf.Config) error { +func (p *stargatePlugin) appTOML(homePath string, conf chainconfig.Config) error { // TODO find a better way in order to not delete comments in the toml.yml path := filepath.Join(homePath, "config/app.toml") config, err := toml.LoadFile(path) @@ -75,7 +77,7 @@ func (p *stargatePlugin) appTOML(homePath string, conf starportconf.Config) erro return err } -func (p *stargatePlugin) configTOML(homePath string, conf starportconf.Config) error { +func (p *stargatePlugin) configTOML(homePath string, conf chainconfig.Config) error { // TODO find a better way in order to not delete comments in the toml.yml path := filepath.Join(homePath, "config/config.toml") config, err := toml.LoadFile(path) @@ -117,7 +119,7 @@ func (p *stargatePlugin) clientTOML(homePath string) error { return err } -func (p *stargatePlugin) Start(ctx context.Context, runner chaincmdrunner.Runner, conf starportconf.Config) error { +func (p *stargatePlugin) Start(ctx context.Context, runner chaincmdrunner.Runner, conf chainconfig.Config) error { err := runner.Start(ctx, "--pruning", "nothing", diff --git a/starport/services/chain/plugin.go b/starport/services/chain/plugin.go index 7d7d4309d7..734ea3abf6 100644 --- a/starport/services/chain/plugin.go +++ b/starport/services/chain/plugin.go @@ -3,7 +3,7 @@ package chain import ( "context" - starportconf "github.com/tendermint/starport/starport/chainconf" + "github.com/tendermint/starport/starport/chainconfig" chaincmdrunner "github.com/tendermint/starport/starport/pkg/chaincmd/runner" ) @@ -13,14 +13,14 @@ type Plugin interface { // Name of a Cosmos version. Name() string - // GentxCommand returns step.Exec configuration for gentx command. + // Gentx returns step.Exec configuration for gentx command. Gentx(context.Context, chaincmdrunner.Runner, Validator) (path string, err error) // Configure configures config defaults. - Configure(string, starportconf.Config) error + Configure(string, chainconfig.Config) error - // StartCommands returns step.Exec configuration to start servers. - Start(context.Context, chaincmdrunner.Runner, starportconf.Config) error + // Start returns step.Exec configuration to start servers. + Start(context.Context, chaincmdrunner.Runner, chainconfig.Config) error // Home returns the blockchain node's home dir. Home() string diff --git a/starport/services/chain/serve.go b/starport/services/chain/serve.go index d30a32ca2e..de1625db77 100644 --- a/starport/services/chain/serve.go +++ b/starport/services/chain/serve.go @@ -12,7 +12,7 @@ import ( "github.com/otiai10/copy" "github.com/pkg/errors" - conf "github.com/tendermint/starport/starport/chainconf" + "github.com/tendermint/starport/starport/chainconfig" chaincmdrunner "github.com/tendermint/starport/starport/pkg/chaincmd/runner" "github.com/tendermint/starport/starport/pkg/cosmosfaucet" "github.com/tendermint/starport/starport/pkg/dirchange" @@ -21,7 +21,6 @@ import ( "github.com/tendermint/starport/starport/pkg/xfilepath" "github.com/tendermint/starport/starport/pkg/xhttp" "github.com/tendermint/starport/starport/pkg/xurl" - "github.com/tendermint/starport/starport/services" "golang.org/x/sync/errgroup" ) @@ -45,7 +44,7 @@ var ( // starportSavePath is the place where chain exported genesis are saved starportSavePath = xfilepath.Join( - services.StarportConfPath, + chainconfig.ConfigDirPath, xfilepath.Path("local-chains"), ) ) @@ -98,7 +97,7 @@ func (c *Chain) Serve(ctx context.Context, options ...ServeOption) error { if _, err := os.Stat(c.options.ConfigFile); err != nil { return err } - } else if _, err := conf.LocateDefault(c.app.Path); err != nil { + } else if _, err := chainconfig.LocateDefault(c.app.Path); err != nil { return err } @@ -162,7 +161,7 @@ func (c *Chain) Serve(ctx context.Context, options ...ServeOption) error { case errors.As(err, &buildErr): fmt.Fprintf(c.stdLog().err, "%s\n", errorColor(err.Error())) - var validationErr *conf.ValidationError + var validationErr *chainconfig.ValidationError if errors.As(err, &validationErr) { fmt.Fprintln(c.stdLog().out, "see: https://github.com/tendermint/starport#configure") } @@ -379,7 +378,7 @@ func (c *Chain) serve(ctx context.Context, forceReset bool) error { return c.start(ctx, conf) } -func (c *Chain) start(ctx context.Context, config conf.Config) error { +func (c *Chain) start(ctx context.Context, config chainconfig.Config) error { commands, err := c.Commands(ctx) if err != nil { return err @@ -418,7 +417,7 @@ func (c *Chain) start(ctx context.Context, config conf.Config) error { fmt.Fprintf(c.stdLog().out, "🌍 Blockchain API: %s\n", xurl.HTTP(config.Host.API)) if isFaucetEnabled { - fmt.Fprintf(c.stdLog().out, "🌍 Token faucet: %s\n", xurl.HTTP(conf.FaucetHost(config))) + fmt.Fprintf(c.stdLog().out, "🌍 Token faucet: %s\n", xurl.HTTP(chainconfig.FaucetHost(config))) } return g.Wait() @@ -431,7 +430,7 @@ func (c *Chain) runFaucetServer(ctx context.Context, faucet cosmosfaucet.Faucet) } return xhttp.Serve(ctx, &http.Server{ - Addr: conf.FaucetHost(config), + Addr: chainconfig.FaucetHost(config), Handler: faucet, }) } diff --git a/starport/services/chain/simulate.go b/starport/services/chain/simulate.go new file mode 100644 index 0000000000..907c8d1387 --- /dev/null +++ b/starport/services/chain/simulate.go @@ -0,0 +1,80 @@ +package chain + +import ( + "context" + + "github.com/cosmos/cosmos-sdk/types/simulation" +) + +type simappOptions struct { + enabled bool + verbose bool + config simulation.Config + period uint + genesisTime int64 +} + +func newSimappOptions() simappOptions { + return simappOptions{ + config: simulation.Config{ + Commit: true, + }, + enabled: true, + verbose: false, + period: 0, + genesisTime: 0, + } +} + +// SimappOption provides options for the simapp command +type SimappOption func(*simappOptions) + +// SimappWithVerbose enable the verbose mode +func SimappWithVerbose(verbose bool) SimappOption { + return func(c *simappOptions) { + c.verbose = verbose + } +} + +// SimappWithPeriod allows running slow invariants only once every period assertions +func SimappWithPeriod(period uint) SimappOption { + return func(c *simappOptions) { + c.period = period + } +} + +// SimappWithGenesisTime allows overriding genesis UNIX time instead of using a random UNIX time +func SimappWithGenesisTime(genesisTime int64) SimappOption { + return func(c *simappOptions) { + c.genesisTime = genesisTime + } +} + +// SimappWithConfig allows to add a simulation config +func SimappWithConfig(config simulation.Config) SimappOption { + return func(c *simappOptions) { + c.config = config + } +} + +func (c *Chain) Simulate(ctx context.Context, options ...SimappOption) error { + simappOptions := newSimappOptions() + + // apply the options + for _, apply := range options { + apply(&simappOptions) + } + + commands, err := c.Commands(ctx) + if err != nil { + return err + } + return commands.Simulation(ctx, + c.app.Path, + simappOptions.enabled, + simappOptions.verbose, + simappOptions.config, + simappOptions.period, + simappOptions.genesisTime, + ) +} diff --git a/starport/services/config.go b/starport/services/config.go deleted file mode 100644 index d76a69d53f..0000000000 --- a/starport/services/config.go +++ /dev/null @@ -1,22 +0,0 @@ -package services - -import ( - "os" - - "github.com/tendermint/starport/starport/pkg/xfilepath" -) - -var ( - // StarportConfPath returns the Starport Configuration directory - StarportConfPath = xfilepath.JoinFromHome(xfilepath.Path(".starport")) -) - -// InitConfig creates config directory if it is not yet created -func InitConfig() error { - confPath, err := StarportConfPath() - if err != nil { - return err - } - - return os.MkdirAll(confPath, 0755) -} diff --git a/starport/services/network/join.go b/starport/services/network/join.go new file mode 100644 index 0000000000..f694de070a --- /dev/null +++ b/starport/services/network/join.go @@ -0,0 +1,222 @@ +package network + +import ( + "context" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + launchtypes "github.com/tendermint/spn/x/launch/types" + "github.com/tendermint/starport/starport/pkg/cosmoserror" + "github.com/tendermint/starport/starport/pkg/cosmosutil" + "github.com/tendermint/starport/starport/pkg/events" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +// Join to the network. +func (n Network) Join( + ctx context.Context, + c Chain, + launchID uint64, + amount sdk.Coin, + publicAddress string, + gentxPath string, +) error { + peer, err := c.Peer(ctx, publicAddress) + if err != nil { + return err + } + + isCustomGentx := gentxPath != "" + + // if the custom gentx is not provided, get the chain default from the chain home folder. + if !isCustomGentx { + gentxPath, err = c.DefaultGentxPath() + if err != nil { + return err + } + } + + // get the chain genesis path from the home folder + genesisPath, err := c.GenesisPath() + if err != nil { + return err + } + + // parse the gentx content + gentxInfo, gentx, err := cosmosutil.GentxFromPath(gentxPath) + if err != nil { + return err + } + + // change the chain address prefix to spn + accountAddress, err := cosmosutil.ChangeAddressPrefix(gentxInfo.DelegatorAddress, networkchain.SPN) + if err != nil { + return err + } + + if err := n.sendAccountRequest( + ctx, + genesisPath, + isCustomGentx, + launchID, + accountAddress, + amount, + ); err != nil { + return err + } + + return n.sendValidatorRequest(ctx, launchID, peer, accountAddress, gentx, gentxInfo) +} + +// sendAccountRequest creates an add AddAccount request message. +func (n Network) sendAccountRequest( + ctx context.Context, + genesisPath string, + isCustomGentx bool, + launchID uint64, + accountAddress string, + amount sdk.Coin, +) (err error) { + address := n.account.Address(networkchain.SPN) + n.ev.Send(events.New(events.StatusOngoing, "Verifying account already exists "+address)) + + // if is custom gentx path, avoid to check account into genesis from the home folder + var accExist bool + if !isCustomGentx { + accExist, err = cosmosutil.CheckGenesisContainsAddress(genesisPath, address) + if err != nil { + return err + } + if accExist { + return fmt.Errorf("account %s already exist", address) + } + } + // check if account exists as a genesis account in SPN chain launch information + hasAccount, err := n.hasAccount(ctx, launchID, address) + if err != nil { + return err + } + if hasAccount { + return fmt.Errorf("account %s already exist", address) + } + + msg := launchtypes.NewMsgRequestAddAccount( + n.account.Address(networkchain.SPN), + launchID, + accountAddress, + sdk.NewCoins(amount), + ) + + n.ev.Send(events.New(events.StatusOngoing, "Broadcasting account transactions")) + res, err := n.cosmos.BroadcastTx(n.account.Name, msg) + if err != nil { + return cosmoserror.Unwrap(err) + } + + var requestRes launchtypes.MsgRequestAddAccountResponse + if err := res.Decode(&requestRes); err != nil { + return cosmoserror.Unwrap(err) + } + + if requestRes.AutoApproved { + n.ev.Send(events.New(events.StatusDone, "Account added to the network by the coordinator!")) + } else { + n.ev.Send(events.New(events.StatusDone, + fmt.Sprintf("Request %d to add account to the network has been submitted!", + requestRes.RequestID), + )) + } + return nil +} + +// sendValidatorRequest creates the RequestAddValidator message into the SPN +func (n Network) sendValidatorRequest( + ctx context.Context, + launchID uint64, + peer string, + valAddress string, + gentx []byte, + gentxInfo cosmosutil.GentxInfo, +) error { + // Check if the validator request already exist + hasValidator, err := n.hasValidator(ctx, launchID, valAddress) + if err != nil { + return err + } + if hasValidator { + return fmt.Errorf("validator %s already exist", valAddress) + } + + msg := launchtypes.NewMsgRequestAddValidator( + n.account.Address(networkchain.SPN), + launchID, + valAddress, + gentx, + gentxInfo.PubKey, + gentxInfo.SelfDelegation, + peer, + ) + + n.ev.Send(events.New(events.StatusOngoing, "Broadcasting validator transaction")) + + res, err := n.cosmos.BroadcastTx(n.account.Name, msg) + if err != nil { + return cosmoserror.Unwrap(err) + } + + var requestRes launchtypes.MsgRequestAddValidatorResponse + if err := res.Decode(&requestRes); err != nil { + return cosmoserror.Unwrap(err) + } + + if requestRes.AutoApproved { + n.ev.Send(events.New(events.StatusDone, "Validator added to the network by the coordinator!")) + } else { + n.ev.Send(events.New(events.StatusDone, + fmt.Sprintf("Request %d to join the network as a validator has been submitted!", + requestRes.RequestID), + )) + } + return nil +} + +// hasValidator verify if the validator already exist into the SPN store +func (n Network) hasValidator(ctx context.Context, launchID uint64, address string) (bool, error) { + _, err := launchtypes.NewQueryClient(n.cosmos.Context).GenesisValidator(ctx, &launchtypes.QueryGetGenesisValidatorRequest{ + LaunchID: launchID, + Address: address, + }) + err = cosmoserror.Unwrap(err) + if err == cosmoserror.ErrInvalidRequest { + return false, nil + } else if err != nil { + return false, err + } + return true, nil +} + +// hasAccount verify if the account already exist into the SPN store +func (n Network) hasAccount(ctx context.Context, launchID uint64, address string) (bool, error) { + _, err := launchtypes.NewQueryClient(n.cosmos.Context).VestingAccount(ctx, &launchtypes.QueryGetVestingAccountRequest{ + LaunchID: launchID, + Address: address, + }) + err = cosmoserror.Unwrap(err) + if err == cosmoserror.ErrInvalidRequest { + return false, nil + } else if err != nil { + return false, err + } + + _, err = launchtypes.NewQueryClient(n.cosmos.Context).GenesisAccount(ctx, &launchtypes.QueryGetGenesisAccountRequest{ + LaunchID: launchID, + Address: address, + }) + err = cosmoserror.Unwrap(err) + if err == cosmoserror.ErrInvalidRequest { + return false, nil + } else if err != nil { + return false, err + } + return true, nil +} diff --git a/starport/services/network/launch.go b/starport/services/network/launch.go new file mode 100644 index 0000000000..497ecd051a --- /dev/null +++ b/starport/services/network/launch.go @@ -0,0 +1,67 @@ +package network + +import ( + "context" + "fmt" + "time" + + launchtypes "github.com/tendermint/spn/x/launch/types" + "github.com/tendermint/starport/starport/pkg/cosmoserror" + "github.com/tendermint/starport/starport/pkg/events" + "github.com/tendermint/starport/starport/pkg/xtime" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +// LaunchParams fetches the chain launch module params from SPN +func (n Network) LaunchParams(ctx context.Context) (launchtypes.Params, error) { + res, err := launchtypes.NewQueryClient(n.cosmos.Context).Params(ctx, &launchtypes.QueryParamsRequest{}) + if err != nil { + return launchtypes.Params{}, cosmoserror.Unwrap(err) + } + return res.GetParams(), nil +} + +// TriggerLaunch launches a chain as a coordinator +func (n Network) TriggerLaunch(ctx context.Context, launchID uint64, remainingTime time.Duration) error { + n.ev.Send(events.New(events.StatusOngoing, fmt.Sprintf("Launching chain %d", launchID))) + params, err := n.LaunchParams(ctx) + if err != nil { + return cosmoserror.Unwrap(err) + } + + var ( + minLaunch = xtime.Seconds(params.MinLaunchTime) + maxLaunch = xtime.Seconds(params.MaxLaunchTime) + address = n.account.Address(networkchain.SPN) + ) + switch { + case remainingTime == 0: + // if the user does not specify the remaining time, use the minimal one + remainingTime = minLaunch + case remainingTime < minLaunch: + return fmt.Errorf("remaining time %s lower than minimum %s", + xtime.NowAfter(remainingTime), + xtime.NowAfter(minLaunch)) + case remainingTime > maxLaunch: + return fmt.Errorf("remaining time %s greater than maximum %s", + xtime.NowAfter(remainingTime), + xtime.NowAfter(maxLaunch)) + } + + msg := launchtypes.NewMsgTriggerLaunch(address, launchID, uint64(remainingTime.Seconds())) + n.ev.Send(events.New(events.StatusOngoing, "Setting launch time")) + res, err := n.cosmos.BroadcastTx(n.account.Name, msg) + if err != nil { + return cosmoserror.Unwrap(err) + } + + var launchRes launchtypes.MsgTriggerLaunchResponse + if err := res.Decode(&launchRes); err != nil { + return cosmoserror.Unwrap(err) + } + + n.ev.Send(events.New(events.StatusDone, + fmt.Sprintf("Chain %d will be launched on %s", launchID, xtime.NowAfter(remainingTime)), + )) + return nil +} diff --git a/starport/services/network/network.go b/starport/services/network/network.go new file mode 100644 index 0000000000..27e4ef3bba --- /dev/null +++ b/starport/services/network/network.go @@ -0,0 +1,61 @@ +package network + +import ( + "context" + "strconv" + + "github.com/pkg/errors" + "github.com/tendermint/starport/starport/pkg/cosmosaccount" + "github.com/tendermint/starport/starport/pkg/cosmosclient" + "github.com/tendermint/starport/starport/pkg/events" +) + +// Network is network builder. +type Network struct { + ev events.Bus + cosmos cosmosclient.Client + account cosmosaccount.Account +} + +type Chain interface { + ID() (string, error) + Name() string + SourceURL() string + SourceHash() string + GenesisPath() (string, error) + GentxsPath() (string, error) + DefaultGentxPath() (string, error) + Peer(ctx context.Context, addr string) (string, error) +} + +type Option func(*Network) + +// CollectEvents collects events from the network builder. +func CollectEvents(ev events.Bus) Option { + return func(b *Network) { + b.ev = ev + } +} + +// New creates a Builder. +func New(cosmos cosmosclient.Client, account cosmosaccount.Account, options ...Option) (Network, error) { + n := Network{ + cosmos: cosmos, + account: account, + } + for _, opt := range options { + opt(&n) + } + return n, nil +} + +func ParseLaunchID(id string) (uint64, error) { + launchID, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return 0, errors.Wrap(err, "error parsing launchID") + } + if launchID == 0 { + return 0, errors.New("launch ID must be greater than 0") + } + return launchID, nil +} diff --git a/starport/services/network/network_test.go b/starport/services/network/network_test.go new file mode 100644 index 0000000000..d8995e5604 --- /dev/null +++ b/starport/services/network/network_test.go @@ -0,0 +1,50 @@ +package network + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseLaunchID(t *testing.T) { + tests := []struct { + name string + id string + want uint64 + err error + }{ + { + name: "valid number", + id: "10", + want: 10, + }, + { + name: "invalid uint", + id: "-10", + err: errors.New("error parsing launchID: strconv.ParseUint: parsing \"-10\": invalid syntax"), + }, + { + name: "invalid string", + id: "test", + err: errors.New("error parsing launchID: strconv.ParseUint: parsing \"test\": invalid syntax"), + }, + { + name: "invalid launch id", + id: "0", + err: errors.New("launch ID must be greater than 0"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseLaunchID(tt.id) + if tt.err != nil { + require.Error(t, err) + require.Equal(t, tt.err.Error(), err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/starport/services/network/networkchain/account.go b/starport/services/network/networkchain/account.go new file mode 100644 index 0000000000..3dd42c6cec --- /dev/null +++ b/starport/services/network/networkchain/account.go @@ -0,0 +1,106 @@ +package networkchain + +import ( + "context" + "errors" + "os" + "path/filepath" + + chaincmdrunner "github.com/tendermint/starport/starport/pkg/chaincmd/runner" + "github.com/tendermint/starport/starport/pkg/cosmosutil" + "github.com/tendermint/starport/starport/pkg/randstr" + "github.com/tendermint/starport/starport/services/chain" +) + +const ( + passphraseLength = 32 + sampleAccount = "alice" +) + +// InitAccount initializes an account for the blockchain and issue a gentx in config/gentx/gentx.json +func (c Chain) InitAccount(ctx context.Context, v chain.Validator, accountName string) (string, error) { + if !c.isInitialized { + return "", errors.New("the blockchain must be initialized to initialize an account") + } + + chainCmd, err := c.chain.Commands(ctx) + if err != nil { + return "", err + } + + // create the chain account. + address, err := c.ImportAccount(ctx, accountName) + if err != nil { + return "", err + } + + // add account into the genesis + err = chainCmd.AddGenesisAccount(ctx, address, v.StakingAmount) + if err != nil { + return "", err + } + + // create the gentx. + issuedGentxPath, err := c.chain.IssueGentx(ctx, v) + if err != nil { + return "", err + } + + // rename the issued gentx into gentx.json + gentxPath := filepath.Join(filepath.Dir(issuedGentxPath), cosmosutil.GentxFilename) + return gentxPath, os.Rename(issuedGentxPath, gentxPath) +} + +// ImportAccount imports an account from Starport into the chain. +// we first export the account into a temporary key file and import it with the chain CLI. +func (c *Chain) ImportAccount(ctx context.Context, name string) (string, error) { + // keys import command of chain CLI requires that the key file is encrypted with a passphrase of at least 8 characters + // we generate a random passphrase to import the account + passphrase := randstr.Runes(passphraseLength) + + // export the key in a temporary file. + armored, err := c.ar.Export(name, passphrase) + if err != nil { + return "", err + } + + keyFile, err := os.CreateTemp("", "") + if err != nil { + return "", err + } + defer os.Remove(keyFile.Name()) + + if _, err := keyFile.Write([]byte(armored)); err != nil { + return "", err + } + + // import the key file into the chain. + chainCmd, err := c.chain.Commands(ctx) + if err != nil { + return "", err + } + + acc, err := chainCmd.ImportAccount(ctx, name, keyFile.Name(), passphrase) + return acc.Address, err +} + +// detectPrefix detects the account address prefix for the chain +// the method create a sample account and parse the address prefix from it +func (c Chain) detectPrefix(ctx context.Context) (string, error) { + chainCmd, err := c.chain.Commands(ctx) + if err != nil { + return "", err + } + + var acc chaincmdrunner.Account + acc, err = chainCmd.ShowAccount(ctx, sampleAccount) + if errors.Is(err, chaincmdrunner.ErrAccountDoesNotExist) { + // the sample account doesn't exist, we create it + acc, err = chainCmd.AddAccount(ctx, sampleAccount, "", "") + } + if err != nil { + return "", err + } + + return cosmosutil.GetAddressPrefix(acc.Address) +} diff --git a/starport/services/network/networkchain/home.go b/starport/services/network/networkchain/home.go new file mode 100644 index 0000000000..0e9217d3f5 --- /dev/null +++ b/starport/services/network/networkchain/home.go @@ -0,0 +1,28 @@ +package networkchain + +import ( + "os" + "path/filepath" + "strconv" +) + +// ChainHome returns the default home dir used for a chain from SPN. +func ChainHome(launchID uint64) (path string) { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + return filepath.Join(home, SPN, strconv.FormatUint(launchID, 10)) +} + +// IsChainHomeExist checks if a home with the provided launchID already exist. +func IsChainHomeExist(launchID uint64) (path string, ok bool, err error) { + home := ChainHome(launchID) + + if _, err := os.Stat(home); os.IsNotExist(err) { + return home, false, nil + } + + return home, true, nil +} diff --git a/starport/services/network/networkchain/home_test.go b/starport/services/network/networkchain/home_test.go new file mode 100644 index 0000000000..da8441251d --- /dev/null +++ b/starport/services/network/networkchain/home_test.go @@ -0,0 +1,21 @@ +package networkchain_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +func TestChainHome(t *testing.T) { + home, err := os.UserHomeDir() + require.NoError(t, err) + + chainHome := networkchain.ChainHome(0) + require.Equal(t, filepath.Join(home, networkchain.SPN, "0"), chainHome) + + chainHome = networkchain.ChainHome(10) + require.Equal(t, filepath.Join(home, networkchain.SPN, "10"), chainHome) +} diff --git a/starport/services/network/networkchain/init.go b/starport/services/network/networkchain/init.go new file mode 100644 index 0000000000..4f82285de7 --- /dev/null +++ b/starport/services/network/networkchain/init.go @@ -0,0 +1,113 @@ +package networkchain + +import ( + "context" + "fmt" + "os" + + "github.com/tendermint/starport/starport/pkg/cosmosutil" + "github.com/tendermint/starport/starport/pkg/events" +) + +// Init initializes blockchain by building the binaries and running the init command and +// create the initial genesis of the chain, and set up a validator key +func (c *Chain) Init(ctx context.Context) error { + chainHome, err := c.chain.Home() + if err != nil { + return err + } + + // cleanup home dir of app if exists. + if err := os.RemoveAll(chainHome); err != nil { + return err + } + + // build the chain and initialize it with a new validator key + c.ev.Send(events.New(events.StatusOngoing, "Building the blockchain")) + if _, err := c.chain.Build(ctx, ""); err != nil { + return err + } + + c.ev.Send(events.New(events.StatusDone, "Blockchain built")) + c.ev.Send(events.New(events.StatusOngoing, "Initializing the blockchain")) + + if err := c.chain.Init(ctx, false); err != nil { + return err + } + + c.ev.Send(events.New(events.StatusDone, "Blockchain initialized")) + + // initialize and verify the genesis + if err := c.initGenesis(ctx); err != nil { + return err + } + + c.isInitialized = true + + return nil +} + +// initGenesis creates the initial genesis of the genesis depending on the initial genesis type (default, url, ...) +func (c *Chain) initGenesis(ctx context.Context) error { + genesisPath, err := c.chain.GenesisPath() + if err != nil { + return err + } + + // remove existing genesis + if err := os.RemoveAll(genesisPath); err != nil { + return err + } + + // if the blockchain has a genesis URL, the initial genesis is fetched from the URL + // otherwise, the default genesis is used, which requires no action since the default genesis is generated from the init command + if c.genesisURL != "" { + genesis, hash, err := cosmosutil.GenesisAndHashFromURL(ctx, c.genesisURL) + if err != nil { + return err + } + + // if the blockchain has been initialized with no genesis hash, we assign the fetched hash to it + // otherwise we check the genesis integrity with the existing hash + if c.genesisHash == "" { + c.genesisHash = hash + } else if hash != c.genesisHash { + return fmt.Errorf("genesis from URL %s is invalid. Expected hash %s, actual hash %s", c.genesisURL, c.genesisHash, hash) + } + + // replace the default genesis with the fetched genesis + if err := os.WriteFile(genesisPath, genesis, 0644); err != nil { + return err + } + } else { + // default genesis is used, init CLI command is used to generate it + cmd, err := c.chain.Commands(ctx) + if err != nil { + return err + } + + // TODO: use validator moniker https://github.com/tendermint/starport/issues/1834 + if err := cmd.Init(ctx, "moniker"); err != nil { + return err + } + + } + + // check the genesis is valid + return c.checkGenesis(ctx) +} + +// checkGenesis checks the stored genesis is valid +func (c *Chain) checkGenesis(ctx context.Context) error { + // perform static analysis of the chain with the validate-genesis command. + chainCmd, err := c.chain.Commands(ctx) + if err != nil { + return err + } + + return chainCmd.ValidateGenesis(ctx) + + // TODO: static analysis of the genesis with validate-genesis doesn't check the full validity of the genesis + // example: gentxs formats are not checked + // to perform a full validity check of the genesis we must try to start the chain with sample accounts +} diff --git a/starport/services/network/networkchain/networkchain.go b/starport/services/network/networkchain/networkchain.go new file mode 100644 index 0000000000..6007a06ce3 --- /dev/null +++ b/starport/services/network/networkchain/networkchain.go @@ -0,0 +1,304 @@ +package networkchain + +import ( + "context" + "fmt" + "os" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + sperrors "github.com/tendermint/starport/starport/errors" + "github.com/tendermint/starport/starport/pkg/chaincmd" + "github.com/tendermint/starport/starport/pkg/cosmosaccount" + "github.com/tendermint/starport/starport/pkg/cosmosver" + "github.com/tendermint/starport/starport/pkg/events" + "github.com/tendermint/starport/starport/pkg/gitpod" + "github.com/tendermint/starport/starport/services/chain" + "github.com/tendermint/starport/starport/services/network/networktypes" +) + +const ( + // SPN name used as an address prefix and as a home dir for chains to publish. + SPN = "spn" + + // SPNDenom is the denom used for the spn chain native token + SPNDenom = "uspn" +) + +// Chain represents a network blockchain and lets you interact with its source code and binary. +type Chain struct { + id string + + path string + home string + + url string + hash string + genesisURL string + genesisHash string + launchTime int64 + + keyringBackend chaincmd.KeyringBackend + + isInitialized bool + + ref plumbing.ReferenceName + + chain *chain.Chain + ev events.Bus + ar cosmosaccount.Registry +} + +// SourceOption sets the source for blockchain. +type SourceOption func(*Chain) + +// Option sets other initialization options. +type Option func(*Chain) + +// SourceRemote sets the default branch on a remote as source for the blockchain. +func SourceRemote(url string) SourceOption { + return func(c *Chain) { + c.url = url + } +} + +// SourceRemoteBranch sets the branch on a remote as source for the blockchain. +func SourceRemoteBranch(url, branch string) SourceOption { + return func(c *Chain) { + c.url = url + c.ref = plumbing.NewBranchReferenceName(branch) + } +} + +// SourceRemoteTag sets the tag on a remote as source for the blockchain. +func SourceRemoteTag(url, tag string) SourceOption { + return func(c *Chain) { + c.url = url + c.ref = plumbing.NewTagReferenceName(tag) + } +} + +// SourceRemoteHash uses a remote hash as source for the blockchain. +func SourceRemoteHash(url, hash string) SourceOption { + return func(c *Chain) { + c.url = url + c.hash = hash + } +} + +// SourceLaunch returns a source option for initializing a chain from a launch +func SourceLaunch(launch networktypes.ChainLaunch) SourceOption { + return func(c *Chain) { + c.id = launch.ChainID + c.url = launch.SourceURL + c.hash = launch.SourceHash + c.genesisURL = launch.GenesisURL + c.genesisHash = launch.GenesisHash + c.home = ChainHome(launch.ID) + c.launchTime = launch.LaunchTime + } +} + +// WithHome provides a specific home path for the blockchain for the initialization. +func WithHome(path string) Option { + return func(c *Chain) { + c.home = path + } +} + +// WithKeyringBackend provides the keyring backend to use to initialize the blockchain +func WithKeyringBackend(keyringBackend chaincmd.KeyringBackend) Option { + return func(c *Chain) { + c.keyringBackend = keyringBackend + } +} + +// WithGenesisFromURL provides a genesis url for the initial genesis of the chain blockchain +func WithGenesisFromURL(genesisURL string) Option { + return func(c *Chain) { + c.genesisURL = genesisURL + } +} + +// CollectEvents collects events from the chain. +func CollectEvents(ev events.Bus) Option { + return func(c *Chain) { + c.ev = ev + } +} + +// New initializes a network blockchain from source and options. +func New(ctx context.Context, ar cosmosaccount.Registry, source SourceOption, options ...Option) (*Chain, error) { + c := &Chain{ + ar: ar, + } + for _, apply := range options { + apply(c) + } + source(c) + + c.ev.Send(events.New(events.StatusOngoing, "Fetching the source code")) + + var err error + if c.path, c.hash, err = fetchSource(ctx, c.url, c.ref, c.hash); err != nil { + return nil, err + } + + c.ev.Send(events.New(events.StatusDone, "Source code fetched")) + c.ev.Send(events.New(events.StatusOngoing, "Setting up the blockchain")) + + chainOption := []chain.Option{ + chain.ID(c.id), + chain.HomePath(c.home), + chain.LogLevel(chain.LogSilent), + } + + // use test keyring backend on Gitpod in order to prevent prompting for keyring + // password. This happens because Gitpod uses containers. + if gitpod.IsOnGitpod() { + c.keyringBackend = chaincmd.KeyringBackendTest + } + + chainOption = append(chainOption, chain.KeyringBackend(c.keyringBackend)) + + chain, err := chain.New(c.path, chainOption...) + if err != nil { + return nil, err + } + + if !chain.Version.IsFamily(cosmosver.Stargate) { + return nil, sperrors.ErrOnlyStargateSupported + } + + c.chain = chain + c.ev.Send(events.New(events.StatusDone, "Blockchain set up")) + + return c, nil +} + +func (c Chain) ID() (string, error) { + return c.chain.ID() +} + +func (c Chain) Name() string { + return c.chain.Name() +} + +func (c Chain) SetHome(home string) { + c.chain.SetHome(home) +} + +func (c Chain) Home() (path string, err error) { + return c.chain.Home() +} + +func (c Chain) GenesisPath() (path string, err error) { + return c.chain.GenesisPath() +} + +func (c Chain) GentxsPath() (path string, err error) { + return c.chain.GentxsPath() +} + +func (c Chain) DefaultGentxPath() (path string, err error) { + return c.chain.DefaultGentxPath() +} + +func (c Chain) SourceURL() string { + return c.url +} + +func (c Chain) SourceHash() string { + return c.hash +} + +func (c Chain) IsHomeDirExist() (ok bool, err error) { + home, err := c.chain.Home() + if err != nil { + return false, err + } + + _, err = os.Stat(home) + if os.IsNotExist(err) { + return false, nil + } + return err == nil, err +} + +// Peer returns the chain peer string `@` of node for others to connect. +func (c Chain) Peer(ctx context.Context, addr string) (string, error) { + chainCmd, err := c.chain.Commands(ctx) + if err != nil { + return "", err + } + + nodeID, err := chainCmd.ShowNodeID(ctx) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s@%s", nodeID, addr), nil +} + +// fetchSource fetches the chain source from url and returns a temporary path where source is saved +func fetchSource( + ctx context.Context, + url string, + ref plumbing.ReferenceName, + customHash string, +) (path, hash string, err error) { + var repo *git.Repository + + if path, err = os.MkdirTemp("", ""); err != nil { + return "", "", err + } + + // ensure the path for chain source exists + if err := os.MkdirAll(path, 0755); err != nil { + return "", "", err + } + + // prepare clone options. + gitoptions := &git.CloneOptions{ + URL: url, + } + + // clone the ref when specified, this is used by chain coordinators on create. + if ref != "" { + gitoptions.ReferenceName = ref + gitoptions.SingleBranch = true + } + if repo, err = git.PlainCloneContext(ctx, path, false, gitoptions); err != nil { + return "", "", err + } + + if customHash != "" { + hash = customHash + + // checkout to a certain hash when specified. this is used by validators to make sure to use + // the locked version of the blockchain. + wt, err := repo.Worktree() + if err != nil { + return "", "", err + } + h, err := repo.ResolveRevision(plumbing.Revision(customHash)) + if err != nil { + return "", "", err + } + githash := *h + if err := wt.Checkout(&git.CheckoutOptions{ + Hash: githash, + }); err != nil { + return "", "", err + } + } else { + // when no specific hash is provided. HEAD is fetched + ref, err := repo.Head() + if err != nil { + return "", "", err + } + hash = ref.Hash().String() + } + + return path, hash, nil +} diff --git a/starport/services/network/networkchain/prepare.go b/starport/services/network/networkchain/prepare.go new file mode 100644 index 0000000000..cec6ec0421 --- /dev/null +++ b/starport/services/network/networkchain/prepare.go @@ -0,0 +1,227 @@ +package networkchain + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pelletier/go-toml" + "github.com/pkg/errors" + "github.com/tendermint/starport/starport/pkg/cosmosutil" + "github.com/tendermint/starport/starport/pkg/events" + "github.com/tendermint/starport/starport/services/network/networktypes" +) + +// Prepare prepares the chain to be launched from genesis information +func (c Chain) Prepare(ctx context.Context, gi networktypes.GenesisInformation) error { + // chain initialization + chainHome, err := c.chain.Home() + if err != nil { + return err + } + + _, err = os.Stat(chainHome) + + switch { + case os.IsNotExist(err): + // if no config exists, perform a full initialization of the chain with a new validator key + if err := c.Init(ctx); err != nil { + return err + } + case err != nil: + return err + default: + // if config and validator key already exists, build the chain and initialize the genesis + c.ev.Send(events.New(events.StatusOngoing, "Building the blockchain")) + if _, err := c.chain.Build(ctx, ""); err != nil { + return err + } + c.ev.Send(events.New(events.StatusDone, "Blockchain built")) + + c.ev.Send(events.New(events.StatusOngoing, "Initializing the genesis")) + if err := c.initGenesis(ctx); err != nil { + return err + } + c.ev.Send(events.New(events.StatusDone, "Genesis initialized")) + } + + if err := c.buildGenesis(ctx, gi); err != nil { + return err + } + + cmd, err := c.chain.Commands(ctx) + if err != nil { + return err + } + return cmd.UnsafeReset(ctx) +} + +// buildGenesis builds the genesis for the chain from the launch approved requests +func (c Chain) buildGenesis(ctx context.Context, gi networktypes.GenesisInformation) error { + c.ev.Send(events.New(events.StatusOngoing, "Building the genesis")) + + addressPrefix, err := c.detectPrefix(ctx) + if err != nil { + return errors.Wrap(err, "error detecting chain prefix") + } + + // apply genesis information to the genesis + if err := c.applyGenesisAccounts(ctx, gi.GenesisAccounts, addressPrefix); err != nil { + return errors.Wrap(err, "error applying genesis accounts to genesis") + } + if err := c.applyVestingAccounts(ctx, gi.VestingAccounts, addressPrefix); err != nil { + return errors.Wrap(err, "error applying vesting accounts to genesis") + } + if err := c.applyGenesisValidators(ctx, gi.GenesisValidators); err != nil { + return errors.Wrap(err, "error applying genesis validators to genesis") + } + + // set the genesis time for the chain + genesisPath, err := c.chain.GenesisPath() + if err != nil { + return errors.Wrap(err, "genesis of the blockchain can't be read") + } + if err := cosmosutil.SetGenesisTime(genesisPath, c.launchTime); err != nil { + return errors.Wrap(err, "genesis time can't be set") + } + + c.ev.Send(events.New(events.StatusDone, "Genesis built")) + + return nil +} + +// applyGenesisAccounts adds the genesis account into the genesis using the chain CLI +func (c Chain) applyGenesisAccounts( + ctx context.Context, + genesisAccs []networktypes.GenesisAccount, + addressPrefix string, +) error { + var err error + + cmd, err := c.chain.Commands(ctx) + if err != nil { + return err + } + + for _, acc := range genesisAccs { + // change the address prefix to the target chain prefix + acc.Address, err = cosmosutil.ChangeAddressPrefix(acc.Address, addressPrefix) + if err != nil { + return err + } + + // call the add genesis account CLI command + err = cmd.AddGenesisAccount(ctx, acc.Address, acc.Coins) + if err != nil { + return err + } + } + + return nil +} + +// applyVestingAccounts adds the genesis vesting account into the genesis using the chain CLI +func (c Chain) applyVestingAccounts( + ctx context.Context, + vestingAccs []networktypes.VestingAccount, + addressPrefix string, +) error { + cmd, err := c.chain.Commands(ctx) + if err != nil { + return err + } + + for _, acc := range vestingAccs { + acc.Address, err = cosmosutil.ChangeAddressPrefix(acc.Address, addressPrefix) + if err != nil { + return err + } + + // call the add genesis account CLI command with delayed vesting option + err = cmd.AddVestingAccount( + ctx, + acc.Address, + acc.TotalBalance, + acc.Vesting, + acc.EndTime, + ) + if err != nil { + return err + } + } + + return nil +} + +// applyGenesisValidators gathers the validator gentxs into the genesis and adds peers in config +func (c Chain) applyGenesisValidators(ctx context.Context, genesisVals []networktypes.GenesisValidator) error { + // no validator + if len(genesisVals) == 0 { + return nil + } + + // reset the gentx directory + gentxDir, err := c.chain.GentxsPath() + if err != nil { + return err + } + if err := os.RemoveAll(gentxDir); err != nil { + return err + } + if err := os.MkdirAll(gentxDir, 0700); err != nil { + return err + } + + // write gentxs + for i, val := range genesisVals { + gentxPath := filepath.Join(gentxDir, fmt.Sprintf("gentx%d.json", i)) + if err = ioutil.WriteFile(gentxPath, val.Gentx, 0666); err != nil { + return err + } + } + + // gather gentxs + cmd, err := c.chain.Commands(ctx) + if err != nil { + return err + } + if err := cmd.CollectGentxs(ctx); err != nil { + return err + } + + return c.updateConfigFromGenesisValidators(genesisVals) +} + +// updateConfigFromGenesisValidators adds the peer addresses into the config.toml of the chain +func (c Chain) updateConfigFromGenesisValidators(genesisVals []networktypes.GenesisValidator) error { + var p2pAddresses []string + for _, val := range genesisVals { + p2pAddresses = append(p2pAddresses, val.Peer) + } + + // set persistent peers + configPath, err := c.chain.ConfigTOMLPath() + if err != nil { + return err + } + configToml, err := toml.LoadFile(configPath) + if err != nil { + return err + } + configToml.Set("p2p.persistent_peers", strings.Join(p2pAddresses, ",")) + if err != nil { + return err + } + + // save config.toml file + configTomlFile, err := os.OpenFile(configPath, os.O_RDWR|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer configTomlFile.Close() + _, err = configToml.WriteTo(configTomlFile) + return err +} diff --git a/starport/services/network/networktypes/chainlaunch.go b/starport/services/network/networktypes/chainlaunch.go new file mode 100644 index 0000000000..5c074e6c70 --- /dev/null +++ b/starport/services/network/networktypes/chainlaunch.go @@ -0,0 +1,40 @@ +package networktypes + +import launchtypes "github.com/tendermint/spn/x/launch/types" + +// ChainLaunch represents the launch of a chain on SPN +type ChainLaunch struct { + ID uint64 `json:"ID"` + ChainID string `json:"ChainID"` + SourceURL string `json:"SourceURL"` + SourceHash string `json:"SourceHash"` + GenesisURL string `json:"GenesisURL"` + GenesisHash string `json:"GenesisHash"` + LaunchTime int64 `json:"LaunchTime"` + CampaignID uint64 `json:"CampaignID"` +} + +// ToChainLaunch converts a chain launch data from SPN and returns a ChainLaunch object +func ToChainLaunch(chain launchtypes.Chain) ChainLaunch { + var launchTime int64 + if chain.LaunchTriggered { + launchTime = chain.LaunchTimestamp + } + + launch := ChainLaunch{ + ID: chain.LaunchID, + ChainID: chain.GenesisChainID, + SourceURL: chain.SourceURL, + SourceHash: chain.SourceHash, + LaunchTime: launchTime, + CampaignID: chain.CampaignID, + } + + // check if custom genesis URL is provided. + if customGenesisURL := chain.InitialGenesis.GetGenesisURL(); customGenesisURL != nil { + launch.GenesisURL = customGenesisURL.Url + launch.GenesisHash = customGenesisURL.Hash + } + + return launch +} diff --git a/starport/services/network/networktypes/chainlaunch_test.go b/starport/services/network/networktypes/chainlaunch_test.go new file mode 100644 index 0000000000..2cb7c3001c --- /dev/null +++ b/starport/services/network/networktypes/chainlaunch_test.go @@ -0,0 +1,69 @@ +package networktypes_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + launchtypes "github.com/tendermint/spn/x/launch/types" + "github.com/tendermint/starport/starport/services/network/networktypes" +) + +func TestToChainLaunch(t *testing.T) { + tests := []struct { + name string + fetched launchtypes.Chain + expected networktypes.ChainLaunch + }{ + { + name: "chain with default genesis", + fetched: launchtypes.Chain{ + LaunchID: 1, + GenesisChainID: "foo-1", + SourceURL: "foo.com", + SourceHash: "0xaaa", + HasCampaign: true, + CampaignID: 1, + InitialGenesis: launchtypes.NewDefaultInitialGenesis(), + }, + expected: networktypes.ChainLaunch{ + ID: 1, + ChainID: "foo-1", + SourceURL: "foo.com", + SourceHash: "0xaaa", + GenesisURL: "", + GenesisHash: "", + CampaignID: 1, + }, + }, + { + name: "launched chain with custom genesis url and no campaign", + fetched: launchtypes.Chain{ + LaunchID: 1, + GenesisChainID: "bar-1", + SourceURL: "bar.com", + SourceHash: "0xbbb", + LaunchTriggered: true, + LaunchTimestamp: 100, + InitialGenesis: launchtypes.NewGenesisURL( + "genesisfoo.com", + "0xccc", + ), + }, + expected: networktypes.ChainLaunch{ + ID: 1, + ChainID: "bar-1", + SourceURL: "bar.com", + SourceHash: "0xbbb", + GenesisURL: "genesisfoo.com", + GenesisHash: "0xccc", + LaunchTime: 100, + CampaignID: 0, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.EqualValues(t, tt.expected, networktypes.ToChainLaunch(tt.fetched)) + }) + } +} diff --git a/starport/services/network/networktypes/genesisinformation.go b/starport/services/network/networktypes/genesisinformation.go new file mode 100644 index 0000000000..95fbeb8610 --- /dev/null +++ b/starport/services/network/networktypes/genesisinformation.go @@ -0,0 +1,84 @@ +package networktypes + +import ( + "errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + launchtypes "github.com/tendermint/spn/x/launch/types" +) + +// GenesisInformation represents all information for a chain to construct the genesis. +type GenesisInformation struct { + GenesisAccounts []GenesisAccount + VestingAccounts []VestingAccount + GenesisValidators []GenesisValidator +} + +// GenesisAccount represents an account with initial coin allocation for the chain for the chain genesis +type GenesisAccount struct { + Address string + Coins string +} + +// VestingAccount represents a vesting account with initial coin allocation and vesting option for the chain genesis +// VestingAccount supports currently only delayed vesting option +type VestingAccount struct { + Address string + TotalBalance string + Vesting string + EndTime int64 +} + +// GenesisValidator represents a genesis validator associated with a gentx in the chain genesis +type GenesisValidator struct { + Gentx []byte + Peer string + Address string + SelfDelegation sdk.Coin +} + +// NewGenesisInformation initializes a new GenesisInformation +func NewGenesisInformation( + genAccs []GenesisAccount, + vestingAccs []VestingAccount, + genVals []GenesisValidator, +) GenesisInformation { + return GenesisInformation{ + GenesisAccounts: genAccs, + VestingAccounts: vestingAccs, + GenesisValidators: genVals, + } +} + +// ToGenesisAccount converts genesis account from SPN +func ToGenesisAccount(acc launchtypes.GenesisAccount) GenesisAccount { + return GenesisAccount{ + Address: acc.Address, + Coins: acc.Coins.String(), + } +} + +// ToVestingAccount converts vesting account from SPN +func ToVestingAccount(acc launchtypes.VestingAccount) (VestingAccount, error) { + delayedVesting := acc.VestingOptions.GetDelayedVesting() + if delayedVesting == nil { + return VestingAccount{}, errors.New("only delayed vesting option is supported") + } + + return VestingAccount{ + Address: acc.Address, + TotalBalance: delayedVesting.TotalBalance.String(), + Vesting: delayedVesting.Vesting.String(), + EndTime: delayedVesting.EndTime, + }, nil +} + +// ToGenesisValidator converts genesis validator from SPN +func ToGenesisValidator(val launchtypes.GenesisValidator) GenesisValidator { + return GenesisValidator{ + Gentx: val.GenTx, + Address: val.Address, + SelfDelegation: val.SelfDelegation, + Peer: val.Peer, + } +} diff --git a/starport/services/network/networktypes/genesisinformation_test.go b/starport/services/network/networktypes/genesisinformation_test.go new file mode 100644 index 0000000000..fa042070b6 --- /dev/null +++ b/starport/services/network/networktypes/genesisinformation_test.go @@ -0,0 +1,115 @@ +package networktypes_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + launchtypes "github.com/tendermint/spn/x/launch/types" + "github.com/tendermint/starport/starport/services/network/networktypes" +) + +var ( + sampleCoins = sdk.NewCoins(sdk.NewCoin("bar", sdk.NewInt(1000)), sdk.NewCoin("foo", sdk.NewInt(2000))) + sampleCoinsStr = sampleCoins.String() +) + +func TestToGenesisAccount(t *testing.T) { + tests := []struct { + name string + fetched launchtypes.GenesisAccount + expected networktypes.GenesisAccount + }{ + { + name: "genesis account", + fetched: launchtypes.GenesisAccount{ + Address: "spn123", + Coins: sampleCoins, + }, + expected: networktypes.GenesisAccount{ + Address: "spn123", + Coins: sampleCoinsStr, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { + require.EqualValues(t, tt.expected, networktypes.ToGenesisAccount(tt.fetched)) + }) + }) + } +} + +func TestToVestingAccount(t *testing.T) { + tests := []struct { + name string + fetched launchtypes.VestingAccount + expected networktypes.VestingAccount + isError bool + }{ + { + name: "vesting account", + fetched: launchtypes.VestingAccount{ + Address: "spn123", + VestingOptions: *launchtypes.NewDelayedVesting( + sampleCoins, + sampleCoins, + 1000, + ), + }, + expected: networktypes.VestingAccount{ + Address: "spn123", + TotalBalance: sampleCoinsStr, + Vesting: sampleCoinsStr, + EndTime: 1000, + }, + }, + { + name: "unrecognized vesting option", + fetched: launchtypes.VestingAccount{ + Address: "spn123", + VestingOptions: launchtypes.VestingOptions{ + Options: nil, + }, + }, + isError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { + vestingAcc, err := networktypes.ToVestingAccount(tt.fetched) + require.EqualValues(t, tt.isError, err != nil) + require.EqualValues(t, tt.expected, vestingAcc) + }) + }) + } +} + +func TestToGenesisValidator(t *testing.T) { + tests := []struct { + name string + fetched launchtypes.GenesisValidator + expected networktypes.GenesisValidator + }{ + { + name: "genesis validator", + fetched: launchtypes.GenesisValidator{ + GenTx: []byte("abc"), + Peer: "abc@0.0.0.0", + }, + expected: networktypes.GenesisValidator{ + Gentx: []byte("abc"), + Peer: "abc@0.0.0.0", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { + require.EqualValues(t, tt.expected, networktypes.ToGenesisValidator(tt.fetched)) + }) + }) + } +} diff --git a/starport/services/network/publish.go b/starport/services/network/publish.go new file mode 100644 index 0000000000..fb160abcdb --- /dev/null +++ b/starport/services/network/publish.go @@ -0,0 +1,151 @@ +package network + +import ( + "context" + + campaigntypes "github.com/tendermint/spn/x/campaign/types" + launchtypes "github.com/tendermint/spn/x/launch/types" + profiletypes "github.com/tendermint/spn/x/profile/types" + "github.com/tendermint/starport/starport/pkg/cosmoserror" + "github.com/tendermint/starport/starport/pkg/cosmosutil" + "github.com/tendermint/starport/starport/pkg/events" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +// publishOptions holds info about how to create a chain. +type publishOptions struct { + genesisURL string + chainID string + campaignID uint64 + noCheck bool +} + +// PublishOption configures chain creation. +type PublishOption func(*publishOptions) + +// WithCampaign add a campaign id. +func WithCampaign(id uint64) PublishOption { + return func(o *publishOptions) { + o.campaignID = id + } +} + +// WithChainID use a custom chain id. +func WithChainID(chainID string) PublishOption { + return func(o *publishOptions) { + o.chainID = chainID + } +} + +// WithNoCheck disables checking integrity of the chain. +func WithNoCheck() PublishOption { + return func(o *publishOptions) { + o.noCheck = true + } +} + +// WithCustomGenesis enables using a custom genesis during publish. +func WithCustomGenesis(url string) PublishOption { + return func(o *publishOptions) { + o.genesisURL = url + } +} + +// Publish submits Genesis to SPN to announce a new network. +func (n Network) Publish(ctx context.Context, c Chain, options ...PublishOption) (launchID, campaignID uint64, err error) { + o := publishOptions{} + for _, apply := range options { + apply(&o) + } + + var genesisHash string + + // if the initial genesis is a genesis URL and no check are performed, we simply fetch it and get its hash. + if o.noCheck && o.genesisURL != "" { + if _, genesisHash, err = cosmosutil.GenesisAndHashFromURL(ctx, o.genesisURL); err != nil { + return 0, 0, err + } + } + + chainID := o.chainID + if chainID == "" { + chainID, err = c.ID() + if err != nil { + return 0, 0, err + } + } + + coordinatorAddress := n.account.Address(networkchain.SPN) + campaignID = o.campaignID + + n.ev.Send(events.New(events.StatusOngoing, "Publishing the network")) + + _, err = profiletypes. + NewQueryClient(n.cosmos.Context). + CoordinatorByAddress(ctx, &profiletypes.QueryGetCoordinatorByAddressRequest{ + Address: coordinatorAddress, + }) + err = cosmoserror.Unwrap(err) + if err == cosmoserror.ErrInvalidRequest { + msgCreateCoordinator := profiletypes.NewMsgCreateCoordinator( + coordinatorAddress, + "", + "", + "", + ) + if _, err := n.cosmos.BroadcastTx(n.account.Name, msgCreateCoordinator); err != nil { + return 0, 0, err + } + } else if err != nil { + return 0, 0, err + } + + if campaignID != 0 { + _, err = campaigntypes. + NewQueryClient(n.cosmos.Context). + Campaign(ctx, &campaigntypes.QueryGetCampaignRequest{ + CampaignID: o.campaignID, + }) + if err != nil { + return 0, 0, cosmoserror.Unwrap(err) + } + } else { + msgCreateCampaign := campaigntypes.NewMsgCreateCampaign( + coordinatorAddress, + c.Name(), + nil, + ) + res, err := n.cosmos.BroadcastTx(n.account.Name, msgCreateCampaign) + if err != nil { + return 0, 0, cosmoserror.Unwrap(err) + } + + var createCampaignRes campaigntypes.MsgCreateCampaignResponse + if err := res.Decode(&createCampaignRes); err != nil { + return 0, 0, cosmoserror.Unwrap(err) + } + campaignID = createCampaignRes.CampaignID + } + + msgCreateChain := launchtypes.NewMsgCreateChain( + n.account.Address(networkchain.SPN), + chainID, + c.SourceURL(), + c.SourceHash(), + o.genesisURL, + genesisHash, + true, + campaignID, + ) + res, err := n.cosmos.BroadcastTx(n.account.Name, msgCreateChain) + if err != nil { + return 0, 0, cosmoserror.Unwrap(err) + } + + var createChainRes launchtypes.MsgCreateChainResponse + if err := res.Decode(&createChainRes); err != nil { + return 0, 0, cosmoserror.Unwrap(err) + } + + return createChainRes.LaunchID, campaignID, nil +} diff --git a/starport/services/network/queries.go b/starport/services/network/queries.go new file mode 100644 index 0000000000..2ad5608d1b --- /dev/null +++ b/starport/services/network/queries.go @@ -0,0 +1,121 @@ +package network + +import ( + "context" + + "github.com/pkg/errors" + launchtypes "github.com/tendermint/spn/x/launch/types" + "github.com/tendermint/starport/starport/pkg/cosmoserror" + "github.com/tendermint/starport/starport/pkg/events" + "github.com/tendermint/starport/starport/services/network/networktypes" +) + +// ChainLaunch fetches the chain launch from Starport Network by launch id. +func (n Network) ChainLaunch(ctx context.Context, id uint64) (networktypes.ChainLaunch, error) { + n.ev.Send(events.New(events.StatusOngoing, "Fetching chain information")) + + res, err := launchtypes.NewQueryClient(n.cosmos.Context).Chain(ctx, &launchtypes.QueryGetChainRequest{ + LaunchID: id, + }) + if err != nil { + return networktypes.ChainLaunch{}, cosmoserror.Unwrap(err) + } + + n.ev.Send(events.New(events.StatusOngoing, "Chain information fetched")) + + return networktypes.ToChainLaunch(res.Chain), nil +} + +// ChainLaunches fetches the chain launches from Starport Network +func (n Network) ChainLaunches(ctx context.Context) ([]networktypes.ChainLaunch, error) { + var chainLaunches []networktypes.ChainLaunch + + n.ev.Send(events.New(events.StatusOngoing, "Fetching chains information")) + res, err := launchtypes.NewQueryClient(n.cosmos.Context).ChainAll(ctx, &launchtypes.QueryAllChainRequest{}) + if err != nil { + return chainLaunches, cosmoserror.Unwrap(err) + } + + // Parse fetched chains + for _, chain := range res.Chain { + chainLaunches = append(chainLaunches, networktypes.ToChainLaunch(chain)) + } + + return chainLaunches, nil +} + +// GenesisInformation returns all the information to construct the genesis from a chain ID +func (n Network) GenesisInformation(ctx context.Context, launchID uint64) (gi networktypes.GenesisInformation, err error) { + genAccs, err := n.GenesisAccounts(ctx, launchID) + if err != nil { + return gi, errors.Wrap(err, "error querying genesis accounts") + } + + vestingAccs, err := n.VestingAccounts(ctx, launchID) + if err != nil { + return gi, errors.Wrap(err, "error querying vesting accounts") + } + + genVals, err := n.GenesisValidators(ctx, launchID) + if err != nil { + return gi, errors.Wrap(err, "error querying genesis validators") + } + + return networktypes.NewGenesisInformation(genAccs, vestingAccs, genVals), nil +} + +// GenesisAccounts returns the list of approved genesis accounts for a launch from SPN +func (n Network) GenesisAccounts(ctx context.Context, launchID uint64) (genAccs []networktypes.GenesisAccount, err error) { + n.ev.Send(events.New(events.StatusOngoing, "Fetching genesis accounts")) + res, err := launchtypes.NewQueryClient(n.cosmos.Context).GenesisAccountAll(ctx, &launchtypes.QueryAllGenesisAccountRequest{ + LaunchID: launchID, + }) + if err != nil { + return genAccs, cosmoserror.Unwrap(err) + } + + for _, acc := range res.GenesisAccount { + genAccs = append(genAccs, networktypes.ToGenesisAccount(acc)) + } + + return genAccs, nil +} + +// VestingAccounts returns the list of approved genesis vesting accounts for a launch from SPN +func (n Network) VestingAccounts(ctx context.Context, launchID uint64) (vestingAccs []networktypes.VestingAccount, err error) { + n.ev.Send(events.New(events.StatusOngoing, "Fetching genesis vesting accounts")) + res, err := launchtypes.NewQueryClient(n.cosmos.Context).VestingAccountAll(ctx, &launchtypes.QueryAllVestingAccountRequest{ + LaunchID: launchID, + }) + if err != nil { + return vestingAccs, cosmoserror.Unwrap(err) + } + + for i, acc := range res.VestingAccount { + parsedAcc, err := networktypes.ToVestingAccount(acc) + if err != nil { + return vestingAccs, errors.Wrapf(err, "error parsing vesting account %d", i) + } + + vestingAccs = append(vestingAccs, parsedAcc) + } + + return vestingAccs, nil +} + +// GenesisValidators returns the list of approved genesis validators for a launch from SPN +func (n Network) GenesisValidators(ctx context.Context, launchID uint64) (genVals []networktypes.GenesisValidator, err error) { + n.ev.Send(events.New(events.StatusOngoing, "Fetching genesis validators")) + res, err := launchtypes.NewQueryClient(n.cosmos.Context).GenesisValidatorAll(ctx, &launchtypes.QueryAllGenesisValidatorRequest{ + LaunchID: launchID, + }) + if err != nil { + return genVals, cosmoserror.Unwrap(err) + } + + for _, acc := range res.GenesisValidator { + genVals = append(genVals, networktypes.ToGenesisValidator(acc)) + } + + return genVals, nil +} diff --git a/starport/services/network/request.go b/starport/services/network/request.go new file mode 100644 index 0000000000..aa72b1f6ba --- /dev/null +++ b/starport/services/network/request.go @@ -0,0 +1,174 @@ +package network + +import ( + "context" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + launchtypes "github.com/tendermint/spn/x/launch/types" + "github.com/tendermint/starport/starport/pkg/cosmoserror" + "github.com/tendermint/starport/starport/pkg/cosmosutil" + "github.com/tendermint/starport/starport/pkg/events" + "github.com/tendermint/starport/starport/services/network/networkchain" +) + +// Reviewal keeps a request's reviewal. +type Reviewal struct { + RequestID uint64 + IsApproved bool +} + +// ApproveRequest returns approval for a request with id. +func ApproveRequest(requestID uint64) Reviewal { + return Reviewal{ + RequestID: requestID, + IsApproved: true, + } +} + +// RejectRequest returns rejection for a request with id. +func RejectRequest(requestID uint64) Reviewal { + return Reviewal{ + RequestID: requestID, + IsApproved: false, + } +} + +// Requests fetches the chain requests from SPN by launch id +func (n Network) Requests(ctx context.Context, launchID uint64) ([]launchtypes.Request, error) { + res, err := launchtypes.NewQueryClient(n.cosmos.Context).RequestAll(ctx, &launchtypes.QueryAllRequestRequest{ + LaunchID: launchID, + }) + if err != nil { + return nil, cosmoserror.Unwrap(err) + } + return res.Request, nil +} + +// Request fetches the chain request from SPN by launch and request id +func (n Network) Request(ctx context.Context, launchID, requestID uint64) (launchtypes.Request, error) { + res, err := launchtypes.NewQueryClient(n.cosmos.Context).Request(ctx, &launchtypes.QueryGetRequestRequest{ + LaunchID: launchID, + RequestID: requestID, + }) + if err != nil { + return launchtypes.Request{}, cosmoserror.Unwrap(err) + } + return res.Request, nil +} + +// SubmitRequest submits reviewals for proposals in batch for chain. +func (n Network) SubmitRequest(launchID uint64, reviewal ...Reviewal) error { + n.ev.Send(events.New(events.StatusOngoing, "Submitting requests...")) + + messages := make([]sdk.Msg, len(reviewal)) + for i, reviewal := range reviewal { + messages[i] = launchtypes.NewMsgSettleRequest( + n.account.Address(networkchain.SPN), + launchID, + reviewal.RequestID, + reviewal.IsApproved, + ) + } + + res, err := n.cosmos.BroadcastTx(n.account.Name, messages...) + if err != nil { + return cosmoserror.Unwrap(err) + } + + var requestRes launchtypes.MsgSettleRequestResponse + err = res.Decode(&requestRes) + if err != nil { + return cosmoserror.Unwrap(err) + } + return nil +} + +// verifyAddValidatorRequest verify the validator request parameters +func (Network) verifyAddValidatorRequest(req *launchtypes.RequestContent_GenesisValidator) error { + // If this is an add validator request + var ( + peer = req.GenesisValidator.Peer + valAddress = req.GenesisValidator.Address + consPubKey = req.GenesisValidator.ConsPubKey + selfDelegation = req.GenesisValidator.SelfDelegation + ) + + // Check values inside the gentx are correct + info, _, err := cosmosutil.ParseGentx(req.GenesisValidator.GenTx) + if err != nil { + return fmt.Errorf("cannot parse gentx %s", err.Error()) + } + + // Change the address prefix fetched from the gentx to the one used on SPN + // Because all on-chain stored address on SPN uses the SPN prefix + spnFetchedAddress, err := cosmosutil.ChangeAddressPrefix(info.DelegatorAddress, networkchain.SPN) + if err != nil { + return err + } + + // Check validator address + if valAddress != spnFetchedAddress { + return fmt.Errorf( + "the validator address %s doesn't match the one inside the gentx %s", + valAddress, + spnFetchedAddress, + ) + } + + // Check validator address + if !info.PubKey.Equal(consPubKey) { + return fmt.Errorf( + "the consensus pub key %s doesn't match the one inside the gentx %s", + string(consPubKey), + string(info.PubKey), + ) + } + + // Check self delegation + if selfDelegation.Denom != info.SelfDelegation.Denom || + !selfDelegation.IsEqual(info.SelfDelegation) { + return fmt.Errorf( + "the self delegation %s doesn't match the one inside the gentx %s", + selfDelegation.String(), + info.SelfDelegation.String(), + ) + } + + // Check the format of the peer + if !cosmosutil.VerifyPeerFormat(peer) { + return fmt.Errorf( + "the peer %s doesn't match the peer format @", + peer, + ) + } + return nil +} + +// VerifyRequests if the requests are correct and simulate them with the current launch information +// Correctness means checks that have to be performed off-chain +func (n Network) VerifyRequests(ctx context.Context, launchID uint64, requests ...uint64) error { + n.ev.Send(events.New(events.StatusOngoing, "Verifying requests...")) + // Check all request + for _, id := range requests { + request, err := n.Request(ctx, launchID, id) + if err != nil { + return err + } + + req, ok := request.Content.Content.(*launchtypes.RequestContent_GenesisValidator) + if ok { + err := n.verifyAddValidatorRequest(req) + if err != nil { + return fmt.Errorf("request %d error: %s", id, err.Error()) + } + } + } + n.ev.Send(events.New(events.StatusDone, "Requests verified")) + + // TODO simulate the requests + // If all requests are correct, simulate them + // return n.SimulateRequests(ctx, launchID, requests) + + return nil +} diff --git a/starport/services/network/request_test.go b/starport/services/network/request_test.go new file mode 100644 index 0000000000..4988936c1f --- /dev/null +++ b/starport/services/network/request_test.go @@ -0,0 +1,153 @@ +package network + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + launchtypes "github.com/tendermint/spn/x/launch/types" +) + +func TestBuilderVerifyAddValidatorRequest(t *testing.T) { + gentx := []byte(`{ + "body": { + "messages": [ + { + "delegator_address": "cosmos1dd246yq6z5vzjz9gh8cff46pll75yyl8ygndsj", + "pubkey": { + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": "aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs=" + }, + "validator_address": "cosmosvaloper1dd246yq6z5vzjz9gh8cff46pll75yyl8pu8cup", + "value": { + "amount": "95000000", + "denom": "stake" + } + } + ] + } +}`) + + tests := []struct { + name string + req *launchtypes.RequestContent_GenesisValidator + want error + }{ + { + name: "valid genesis validator request", + req: &launchtypes.RequestContent_GenesisValidator{ + GenesisValidator: &launchtypes.GenesisValidator{ + Address: "spn1dd246yq6z5vzjz9gh8cff46pll75yyl8c5tt7g", + GenTx: gentx, + ConsPubKey: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + SelfDelegation: sdk.NewCoin("stake", sdk.NewInt(95000000)), + Peer: "nodeid@127.163.0.1:2446", + }, + }, + }, + { + name: "invalid peer node id", + req: &launchtypes.RequestContent_GenesisValidator{ + GenesisValidator: &launchtypes.GenesisValidator{ + Address: "spn1dd246yq6z5vzjz9gh8cff46pll75yyl8c5tt7g", + GenTx: gentx, + ConsPubKey: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + SelfDelegation: sdk.NewCoin("stake", sdk.NewInt(95000000)), + Peer: "@127.163.0.1:2446", + }, + }, + want: fmt.Errorf("the peer @127.163.0.1:2446 doesn't match the peer format @"), + }, + { + name: "invalid peer host", + req: &launchtypes.RequestContent_GenesisValidator{ + GenesisValidator: &launchtypes.GenesisValidator{ + Address: "spn1dd246yq6z5vzjz9gh8cff46pll75yyl8c5tt7g", + GenTx: gentx, + ConsPubKey: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + SelfDelegation: sdk.NewCoin("stake", sdk.NewInt(95000000)), + Peer: "nodeid@", + }, + }, + want: fmt.Errorf("the peer nodeid@ doesn't match the peer format @"), + }, + { + name: "invalid gentx", + req: &launchtypes.RequestContent_GenesisValidator{ + GenesisValidator: &launchtypes.GenesisValidator{ + Address: "spn1dd246yq6z5vzjz9gh8cff46pll75yyl8c5tt7g", + GenTx: []byte(`{}`), + ConsPubKey: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + SelfDelegation: sdk.NewCoin("stake", sdk.NewInt(95000000)), + Peer: "nodeid@127.163.0.1:2446", + }, + }, + want: fmt.Errorf("cannot parse gentx the gentx cannot be parsed"), + }, + { + name: "invalid self delegation denom", + req: &launchtypes.RequestContent_GenesisValidator{ + GenesisValidator: &launchtypes.GenesisValidator{ + Address: "spn1dd246yq6z5vzjz9gh8cff46pll75yyl8c5tt7g", + GenTx: gentx, + ConsPubKey: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + SelfDelegation: sdk.NewCoin("foo", sdk.NewInt(95000000)), + Peer: "nodeid@127.163.0.1:2446", + }, + }, + want: fmt.Errorf("the self delegation 95000000foo doesn't match the one inside the gentx 95000000stake"), + }, + { + name: "invalid self delegation value", + req: &launchtypes.RequestContent_GenesisValidator{ + GenesisValidator: &launchtypes.GenesisValidator{ + Address: "spn1dd246yq6z5vzjz9gh8cff46pll75yyl8c5tt7g", + GenTx: gentx, + ConsPubKey: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + SelfDelegation: sdk.NewCoin("stake", sdk.NewInt(3)), + Peer: "nodeid@127.163.0.1:2446", + }, + }, + want: fmt.Errorf("the self delegation 3stake doesn't match the one inside the gentx 95000000stake"), + }, + { + name: "invalid consensus pub key", + req: &launchtypes.RequestContent_GenesisValidator{ + GenesisValidator: &launchtypes.GenesisValidator{ + Address: "spn1dd246yq6z5vzjz9gh8cff46pll75yyl8c5tt7g", + GenTx: gentx, + ConsPubKey: []byte("cosmos1gkheudhhjsvq0s8fxt7p6pwe0k3k30kepcnz9p="), + SelfDelegation: sdk.NewCoin("stake", sdk.NewInt(95000000)), + Peer: "nodeid@127.163.0.1:2446", + }, + }, + want: fmt.Errorf("the consensus pub key cosmos1gkheudhhjsvq0s8fxt7p6pwe0k3k30kepcnz9p= doesn't match the one inside the gentx aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + }, + { + name: "invalid validator address", + req: &launchtypes.RequestContent_GenesisValidator{ + GenesisValidator: &launchtypes.GenesisValidator{ + Address: "spn1gkheudhhjsvq0s8fxt7p6pwe0k3k30keaytytm", + GenTx: gentx, + ConsPubKey: []byte("aeQLCJOjXUyB7evOodI4mbrshIt3vhHGlycJDbUkaMs="), + SelfDelegation: sdk.NewCoin("stake", sdk.NewInt(95000000)), + Peer: "nodeid@127.163.0.1:2446", + }, + }, + want: fmt.Errorf("the validator address spn1gkheudhhjsvq0s8fxt7p6pwe0k3k30keaytytm doesn't match the one inside the gentx spn1dd246yq6z5vzjz9gh8cff46pll75yyl8c5tt7g"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := Network{} + err := n.verifyAddValidatorRequest(tt.req) + if tt.want != nil { + require.Error(t, err) + require.Equal(t, tt.want.Error(), err.Error()) + return + } + require.NoError(t, err) + }) + } +} diff --git a/starport/services/scaffolder/component.go b/starport/services/scaffolder/component.go index c9b0925d28..ac24e75675 100644 --- a/starport/services/scaffolder/component.go +++ b/starport/services/scaffolder/component.go @@ -11,9 +11,9 @@ import ( "path/filepath" "strings" - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/multiformatname" "github.com/tendermint/starport/starport/pkg/protoanalysis" + "github.com/tendermint/starport/starport/templates/field/datatype" ) const ( @@ -22,8 +22,7 @@ const ( componentQuery = "query" componentPacket = "packet" - protoFolder = "proto" - typeSeparator = ":" + protoFolder = "proto" ) // checkComponentValidity performs various checks common to all components to verify if it can be scaffolded @@ -48,7 +47,7 @@ func checkComponentValidity(appPath, moduleName string, compName multiformatname // checkForbiddenComponentName returns true if the name is forbidden as a component name func checkForbiddenComponentName(name multiformatname.Name) error { // Check with names already used from the scaffolded code - switch name.LowerCamel { + switch name.LowerCase { case "oracle", "logger", @@ -56,7 +55,8 @@ func checkForbiddenComponentName(name multiformatname.Name) error { "query", "genesis", "types", - "tx": + "tx", + datatype.TypeCustom: return fmt.Errorf("%s is used by Starport scaffolder", name.LowerCamel) } @@ -200,7 +200,6 @@ func checkForbiddenOracleFieldName(name string) error { "ASKCOUNT", "MINCOUNT", "FEELIMIT", - "REQUESTKEY", "PREPAREGAS", "EXECUTEGAS": return fmt.Errorf("%s is used by Starport scaffolder", name) @@ -213,14 +212,29 @@ func checkCustomTypes(ctx context.Context, path, module string, fields []string) protoPath := filepath.Join(path, protoFolder, module) customFields := make([]string, 0) for _, name := range fields { - fieldSplit := strings.Split(name, typeSeparator) + fieldSplit := strings.Split(name, datatype.Separator) if len(fieldSplit) <= 1 { continue } - fieldType := fieldSplit[1] - if _, ok := field.StaticDataTypes[fieldType]; !ok { - customFields = append(customFields, fieldType) + fieldType := datatype.Name(fieldSplit[1]) + if _, ok := datatype.SupportedTypes[fieldType]; !ok { + customFields = append(customFields, string(fieldType)) } } return protoanalysis.HasMessages(ctx, protoPath, customFields...) } + +// containCustomTypes returns true if the list of fields contains at least one custom type +func containCustomTypes(fields []string) bool { + for _, name := range fields { + fieldSplit := strings.Split(name, datatype.Separator) + if len(fieldSplit) <= 1 { + continue + } + fieldType := datatype.Name(fieldSplit[1]) + if _, ok := datatype.SupportedTypes[fieldType]; !ok { + return true + } + } + return false +} diff --git a/starport/services/scaffolder/init.go b/starport/services/scaffolder/init.go index 623abb1b25..774c0dbdba 100644 --- a/starport/services/scaffolder/init.go +++ b/starport/services/scaffolder/init.go @@ -8,7 +8,7 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "github.com/gobuffalo/genny" - "github.com/tendermint/flutter" + "github.com/tendermint/flutter/v2" "github.com/tendermint/starport/starport/pkg/giturl" "github.com/tendermint/starport/starport/pkg/gomodulepath" "github.com/tendermint/starport/starport/pkg/localfs" diff --git a/starport/services/scaffolder/message.go b/starport/services/scaffolder/message.go index 276cfdbf94..2fbd8b9a9b 100644 --- a/starport/services/scaffolder/message.go +++ b/starport/services/scaffolder/message.go @@ -5,10 +5,11 @@ import ( "fmt" "github.com/gobuffalo/genny" - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/multiformatname" "github.com/tendermint/starport/starport/pkg/placeholder" "github.com/tendermint/starport/starport/pkg/xgenny" + "github.com/tendermint/starport/starport/templates/field" + "github.com/tendermint/starport/starport/templates/field/datatype" "github.com/tendermint/starport/starport/templates/message" modulecreate "github.com/tendermint/starport/starport/templates/module/create" ) @@ -83,7 +84,7 @@ func (s Scaffolder) AddMessage( if err := checkCustomTypes(ctx, s.path, moduleName, fields); err != nil { return sm, err } - parsedMsgFields, err := field.ParseFields(fields, checkForbiddenMessageField) + parsedMsgFields, err := field.ParseFields(fields, checkForbiddenMessageField, scaffoldingOpts.signer) if err != nil { return sm, err } @@ -92,7 +93,7 @@ func (s Scaffolder) AddMessage( if err := checkCustomTypes(ctx, s.path, moduleName, resFields); err != nil { return sm, err } - parsedResFields, err := field.ParseFields(resFields, checkGoReservedWord) + parsedResFields, err := field.ParseFields(resFields, checkGoReservedWord, scaffoldingOpts.signer) if err != nil { return sm, err } @@ -136,6 +137,16 @@ func (s Scaffolder) AddMessage( return sm, err } + gens, err = supportSimulation( + gens, + opts.AppPath, + opts.ModulePath, + opts.ModuleName, + ) + if err != nil { + return sm, err + } + // Scaffold g, err = message.NewStargate(tracer, opts) if err != nil { @@ -151,7 +162,12 @@ func (s Scaffolder) AddMessage( // checkForbiddenMessageField returns true if the name is forbidden as a message name func checkForbiddenMessageField(name string) error { - if name == "creator" { + mfName, err := multiformatname.NewName(name) + if err != nil { + return err + } + + if mfName.LowerCase == datatype.TypeCustom { return fmt.Errorf("%s is used by the message scaffolder", name) } diff --git a/starport/services/scaffolder/module.go b/starport/services/scaffolder/module.go index 82df6c003e..947bea1928 100644 --- a/starport/services/scaffolder/module.go +++ b/starport/services/scaffolder/module.go @@ -20,6 +20,7 @@ import ( "github.com/tendermint/starport/starport/pkg/placeholder" "github.com/tendermint/starport/starport/pkg/validation" "github.com/tendermint/starport/starport/pkg/xgenny" + "github.com/tendermint/starport/starport/templates/field" "github.com/tendermint/starport/starport/templates/module" modulecreate "github.com/tendermint/starport/starport/templates/module/create" moduleimport "github.com/tendermint/starport/starport/templates/module/import" @@ -89,13 +90,16 @@ var ( // moduleCreationOptions holds options for creating a new module type moduleCreationOptions struct { - // chainID is the chain's id. + // ibc true if the module is an ibc module ibc bool - // homePath of the chain's config dir. + // params list of parameters + params []string + + // ibcChannelOrdering ibc channel ordering ibcChannelOrdering string - // list of module depencies + // dependencies list of module dependencies dependencies []modulecreate.Dependency } @@ -109,6 +113,13 @@ func WithIBC() ModuleCreationOption { } } +// WithParams scaffolds a module with params +func WithParams(params []string) ModuleCreationOption { + return func(m *moduleCreationOptions) { + m.params = params + } +} + // WithIBCChannelOrdering configures channel ordering of the IBC module func WithIBCChannelOrdering(ordering string) ModuleCreationOption { return func(m *moduleCreationOptions) { @@ -162,6 +173,12 @@ func (s Scaffolder) CreateModule( apply(&creationOpts) } + // Parse params with the associated type + params, err := field.ParseFields(creationOpts.params, checkForbiddenTypeIndex) + if err != nil { + return sm, err + } + // Check dependencies if err := checkDependencies(creationOpts.dependencies, s.path); err != nil { return sm, err @@ -170,6 +187,7 @@ func (s Scaffolder) CreateModule( opts := &modulecreate.CreateOptions{ ModuleName: moduleName, ModulePath: s.modpath.RawPath, + Params: params, AppName: s.modpath.Package, AppPath: s.path, OwnerName: owner(s.modpath.RawPath), diff --git a/starport/services/scaffolder/oracle.go b/starport/services/scaffolder/oracle.go index 053b0ea4a8..86bb9c6c9c 100644 --- a/starport/services/scaffolder/oracle.go +++ b/starport/services/scaffolder/oracle.go @@ -40,7 +40,7 @@ func OracleWithSigner(signer string) OracleOption { } } -// AddOracle adds a new BandChain oracle integration. +// AddOracle adds a new BandChain oracle envtest. func (s *Scaffolder) AddOracle( tracer *placeholder.Tracer, moduleName, diff --git a/starport/services/scaffolder/packet.go b/starport/services/scaffolder/packet.go index 3321546cd4..3db3866792 100644 --- a/starport/services/scaffolder/packet.go +++ b/starport/services/scaffolder/packet.go @@ -7,10 +7,11 @@ import ( "path/filepath" "github.com/gobuffalo/genny" - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/multiformatname" "github.com/tendermint/starport/starport/pkg/placeholder" "github.com/tendermint/starport/starport/pkg/xgenny" + "github.com/tendermint/starport/starport/templates/field" + "github.com/tendermint/starport/starport/templates/field/datatype" "github.com/tendermint/starport/starport/templates/ibc" ) @@ -93,11 +94,16 @@ func (s Scaffolder) AddPacket( return sm, fmt.Errorf("the module %s doesn't implement IBC module interface", moduleName) } + signer := "" + if !o.withoutMessage { + signer = o.signer + } + // Check and parse packet fields if err := checkCustomTypes(ctx, s.path, moduleName, packetFields); err != nil { return sm, err } - parsedPacketFields, err := field.ParseFields(packetFields, checkForbiddenPacketField) + parsedPacketFields, err := field.ParseFields(packetFields, checkForbiddenPacketField, signer) if err != nil { return sm, err } @@ -106,7 +112,7 @@ func (s Scaffolder) AddPacket( if err := checkCustomTypes(ctx, s.path, moduleName, ackFields); err != nil { return sm, err } - parsedAcksFields, err := field.ParseFields(ackFields, checkGoReservedWord) + parsedAcksFields, err := field.ParseFields(ackFields, checkGoReservedWord, signer) if err != nil { return sm, err } @@ -157,11 +163,17 @@ func isIBCModule(appPath string, moduleName string) (bool, error) { // checkForbiddenPacketField returns true if the name is forbidden as a packet name func checkForbiddenPacketField(name string) error { - switch name { + mfName, err := multiformatname.NewName(name) + if err != nil { + return err + } + + switch mfName.LowerCase { case "sender", "port", - "channelID": + "channelid", + datatype.TypeCustom: return fmt.Errorf("%s is used by the packet scaffolder", name) } diff --git a/starport/services/scaffolder/patch.go b/starport/services/scaffolder/patch.go index 598ea07213..76813fed54 100644 --- a/starport/services/scaffolder/patch.go +++ b/starport/services/scaffolder/patch.go @@ -9,6 +9,26 @@ import ( modulecreate "github.com/tendermint/starport/starport/templates/module/create" ) +// supportSimulation checks if module_simulation.go exists +// appends the generator to create the file if it doesn't +func supportSimulation( + gens []*genny.Generator, + appPath, + modulePath, + moduleName string, +) ([]*genny.Generator, error) { + simulation, err := modulecreate.AddSimulation( + appPath, + modulePath, + moduleName, + ) + if err != nil { + return gens, err + } + gens = append(gens, simulation) + return gens, nil +} + // supportGenesisTests checks if types/genesis_test.go exists // appends the generator to create the file if it doesn't func supportGenesisTests( @@ -18,32 +38,21 @@ func supportGenesisTests( modulePath, moduleName string, ) ([]*genny.Generator, error) { - path, err := filepath.Abs(filepath.Join(appPath, "x", moduleName)) + isIBC, err := isIBCModule(appPath, moduleName) if err != nil { - return nil, err - } - - gmPath := filepath.Join(path, "genesis_test.go") - if _, err := os.Stat(gmPath); os.IsNotExist(err) { - g, err := modulecreate.AddGenesisModuleTest(appPath, appName, modulePath, moduleName) - if err != nil { - return nil, err - } - gens = append(gens, g) - } else if err != nil { - return nil, err + return gens, err } - - gtPath := filepath.Join(path, "types/genesis_test.go") - if _, err := os.Stat(gtPath); os.IsNotExist(err) { - g, err := modulecreate.AddGenesisTypesTest(appPath, appName, modulePath, moduleName) - if err != nil { - return nil, err - } - gens = append(gens, g) - } else if err != nil { - return nil, err + genesisTest, err := modulecreate.AddGenesisTest( + appPath, + appName, + modulePath, + moduleName, + isIBC, + ) + if err != nil { + return gens, err } + gens = append(gens, genesisTest) return gens, nil } diff --git a/starport/services/scaffolder/query.go b/starport/services/scaffolder/query.go index 18e3e21b6e..6753651b38 100644 --- a/starport/services/scaffolder/query.go +++ b/starport/services/scaffolder/query.go @@ -2,12 +2,13 @@ package scaffolder import ( "context" + "errors" "github.com/gobuffalo/genny" - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/multiformatname" "github.com/tendermint/starport/starport/pkg/placeholder" "github.com/tendermint/starport/starport/pkg/xgenny" + "github.com/tendermint/starport/starport/templates/field" "github.com/tendermint/starport/starport/templates/query" ) @@ -42,8 +43,8 @@ func (s Scaffolder) AddQuery( } // Check and parse provided request fields - if err := checkCustomTypes(ctx, s.path, moduleName, reqFields); err != nil { - return sm, err + if ok := containCustomTypes(reqFields); ok { + return sm, errors.New("query request params can't contain custom type") } parsedReqFields, err := field.ParseFields(reqFields, checkGoReservedWord) if err != nil { diff --git a/starport/services/scaffolder/scaffolder.go b/starport/services/scaffolder/scaffolder.go index d786e1dd03..2145115de0 100644 --- a/starport/services/scaffolder/scaffolder.go +++ b/starport/services/scaffolder/scaffolder.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - conf "github.com/tendermint/starport/starport/chainconf" + "github.com/tendermint/starport/starport/chainconfig" sperrors "github.com/tendermint/starport/starport/errors" "github.com/tendermint/starport/starport/pkg/cmdrunner" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" @@ -90,11 +90,11 @@ func protoc(projectPath, gomodPath string) error { return err } - confpath, err := conf.LocateDefault(projectPath) + confpath, err := chainconfig.LocateDefault(projectPath) if err != nil { return err } - conf, err := conf.ParseFile(confpath) + conf, err := chainconfig.ParseFile(confpath) if err != nil { return err } diff --git a/starport/services/scaffolder/type.go b/starport/services/scaffolder/type.go index 96aa005e8e..3d54edc812 100644 --- a/starport/services/scaffolder/type.go +++ b/starport/services/scaffolder/type.go @@ -6,10 +6,11 @@ import ( "strings" "github.com/gobuffalo/genny" - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/multiformatname" "github.com/tendermint/starport/starport/pkg/placeholder" "github.com/tendermint/starport/starport/pkg/xgenny" + "github.com/tendermint/starport/starport/templates/field" + "github.com/tendermint/starport/starport/templates/field/datatype" modulecreate "github.com/tendermint/starport/starport/templates/module/create" "github.com/tendermint/starport/starport/templates/typed" "github.com/tendermint/starport/starport/templates/typed/dry" @@ -134,11 +135,16 @@ func (s Scaffolder) AddType( return sm, err } + signer := "" + if !o.withoutMessage { + signer = o.signer + } + // Check and parse provided fields if err := checkCustomTypes(ctx, s.path, moduleName, o.fields); err != nil { return sm, err } - tFields, err := field.ParseFields(o.fields, checkForbiddenTypeField) + tFields, err := field.ParseFields(o.fields, checkForbiddenTypeField, signer) if err != nil { return sm, err } @@ -148,6 +154,11 @@ func (s Scaffolder) AddType( return sm, err } + isIBC, err := isIBCModule(s.path, moduleName) + if err != nil { + return sm, err + } + var ( g *genny.Generator opts = &typed.Options{ @@ -160,6 +171,7 @@ func (s Scaffolder) AddType( Fields: tFields, NoMessage: o.withoutMessage, MsgSigner: mfSigner, + IsIBC: isIBC, } gens []*genny.Generator ) @@ -191,8 +203,17 @@ func (s Scaffolder) AddType( return sm, err } + gens, err = supportSimulation( + gens, + opts.AppPath, + opts.ModulePath, + opts.ModuleName, + ) + if err != nil { + return sm, err + } + // create the type generator depending on the model - // TODO: rename the template packages to make it consistent with the type new naming switch { case o.isList: g, err = list.NewStargate(tracer, opts) @@ -219,11 +240,11 @@ func (s Scaffolder) AddType( // checkForbiddenTypeIndex returns true if the name is forbidden as a field name func checkForbiddenTypeIndex(name string) error { - fieldSplit := strings.Split(name, typeSeparator) + fieldSplit := strings.Split(name, datatype.Separator) if len(fieldSplit) > 1 { name = fieldSplit[0] - fieldType := fieldSplit[1] - if _, ok := field.StaticDataTypes[fieldType]; !ok { + fieldType := datatype.Name(fieldSplit[1]) + if f, ok := datatype.SupportedTypes[fieldType]; !ok || f.NonIndex { return fmt.Errorf("invalid index type %s", fieldType) } } @@ -232,11 +253,6 @@ func checkForbiddenTypeIndex(name string) error { // checkForbiddenTypeField returns true if the name is forbidden as a field name func checkForbiddenTypeField(name string) error { - fieldSplit := strings.Split(name, typeSeparator) - if len(fieldSplit) > 1 { - name = fieldSplit[0] - } - mfName, err := multiformatname.NewName(name) if err != nil { return err @@ -245,8 +261,9 @@ func checkForbiddenTypeField(name string) error { switch mfName.LowerCase { case "id", + "params", "appendedvalue", - "creator": + datatype.TypeCustom: return fmt.Errorf("%s is used by type scaffolder", name) } diff --git a/starport/templates/app/app.go b/starport/templates/app/app.go index d8331b0868..3aaa937d48 100644 --- a/starport/templates/app/app.go +++ b/starport/templates/app/app.go @@ -6,9 +6,9 @@ import ( "github.com/gobuffalo/genny" "github.com/gobuffalo/plush" "github.com/gobuffalo/plushgen" - "github.com/tendermint/starport/starport/pkg/plushhelpers" "github.com/tendermint/starport/starport/pkg/xgenny" "github.com/tendermint/starport/starport/pkg/xstrings" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" "github.com/tendermint/starport/starport/templates/testutil" ) @@ -37,14 +37,15 @@ func New(opts *Options) (*genny.Generator, error) { // Used for proto package name ctx.Set("formatOwnerName", xstrings.FormatUsername) - // Create the 'testutil' package with the test helpers - if err := testutil.Register(ctx, g, opts.AppPath); err != nil { - return g, err - } - plushhelpers.ExtendPlushContext(ctx) g.Transformer(plushgen.Transformer(ctx)) g.Transformer(genny.Replace("{{appName}}", opts.AppName)) g.Transformer(genny.Replace("{{binaryNamePrefix}}", opts.BinaryNamePrefix)) + + // Create the 'testutil' package with the test helpers + if err := testutil.Register(g, opts.AppPath); err != nil { + return g, err + } + return g, nil } diff --git a/starport/templates/app/stargate/.gitignore b/starport/templates/app/stargate/.gitignore index 54e6ec525b..0d0535f6c6 100644 --- a/starport/templates/app/stargate/.gitignore +++ b/starport/templates/app/stargate/.gitignore @@ -1,3 +1,6 @@ vue/node_modules vue/dist release/ +.idea/ +.vscode/ +.DS_Store diff --git a/starport/templates/app/stargate/app/app.go.plush b/starport/templates/app/stargate/app/app.go.plush index 76a8896d39..f9a43816cd 100644 --- a/starport/templates/app/stargate/app/app.go.plush +++ b/starport/templates/app/stargate/app/app.go.plush @@ -15,6 +15,7 @@ import ( "github.com/cosmos/cosmos-sdk/server/api" "github.com/cosmos/cosmos-sdk/server/config" servertypes "github.com/cosmos/cosmos-sdk/server/types" + "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/version" @@ -22,6 +23,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/auth/ante" authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authsims "github.com/cosmos/cosmos-sdk/x/auth/simulation" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/x/auth/vesting" @@ -155,8 +157,9 @@ var ( ) var ( - _ cosmoscmd.CosmosApp = (*App)(nil) + _ cosmoscmd.App = (*App)(nil) _ servertypes.Application = (*App)(nil) + _ simapp.App = (*App)(nil) ) func init() { @@ -208,11 +211,14 @@ type App struct { // this line is used by starport scaffolding # stargate/app/keeperDeclaration - // the module manager + // mm is the module manager mm *module.Manager + + // sm is the simulation manager + sm *module.SimulationManager } -// New returns a reference to an initialized Gaia. +// New returns a reference to an initialized blockchain app func New( logger log.Logger, db dbm.DB, @@ -416,6 +422,25 @@ func New( app.mm.RegisterRoutes(app.Router(), app.QueryRouter(), encodingConfig.Amino) app.mm.RegisterServices(module.NewConfigurator(app.appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter())) + // create the simulation manager and define the order of the modules for deterministic simulations + app.sm = module.NewSimulationManager( + auth.NewAppModule(appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts), + bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper), + capability.NewAppModule(appCodec, *app.CapabilityKeeper), + feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), + gov.NewAppModule(appCodec, app.GovKeeper, app.AccountKeeper, app.BankKeeper), + mint.NewAppModule(appCodec, app.MintKeeper, app.AccountKeeper), + staking.NewAppModule(appCodec, app.StakingKeeper, app.AccountKeeper, app.BankKeeper), + distr.NewAppModule(appCodec, app.DistrKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper), + slashing.NewAppModule(appCodec, app.SlashingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper), + params.NewAppModule(app.ParamsKeeper), + evidence.NewAppModule(app.EvidenceKeeper), + ibc.NewAppModule(app.IBCKeeper), + transferModule, + // this line is used by starport scaffolding # stargate/app/appModule + ) + app.sm.RegisterStoreDecoders() + // initialize stores app.MountKVStores(keys) app.MountTransientStores(tkeys) @@ -457,6 +482,9 @@ func New( // Name returns the name of the App func (app *App) Name() string { return app.BaseApp.Name() } +// GetBaseApp returns the base app of the application +func (app App) GetBaseApp() *baseapp.BaseApp { return app.BaseApp } + // BeginBlocker application updates every begin block func (app *App) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { return app.mm.BeginBlock(ctx, req) @@ -500,7 +528,7 @@ func (app *App) LegacyAmino() *codec.LegacyAmino { return app.cdc } -// AppCodec returns Gaia's app codec. +// AppCodec returns an app codec. // // NOTE: This is solely to be used for testing purposes as it may be desirable // for modules to register their own custom testing types. @@ -508,7 +536,7 @@ func (app *App) AppCodec() codec.Codec { return app.appCodec } -// InterfaceRegistry returns Gaia's InterfaceRegistry +// InterfaceRegistry returns an InterfaceRegistry func (app *App) InterfaceRegistry() types.InterfaceRegistry { return app.interfaceRegistry } @@ -600,3 +628,8 @@ func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino return paramsKeeper } + +// SimulationManager implements the SimulationApp interface +func (app *App) SimulationManager() *module.SimulationManager { + return app.sm +} diff --git a/starport/templates/app/stargate/app/simulation_test.go.plush b/starport/templates/app/stargate/app/simulation_test.go.plush new file mode 100644 index 0000000000..cbe14edce0 --- /dev/null +++ b/starport/templates/app/stargate/app/simulation_test.go.plush @@ -0,0 +1,113 @@ +package app_test + +import ( + "os" + "testing" + "time" + + "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" + simulationtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/stretchr/testify/require" + "github.com/tendermint/spm/cosmoscmd" + "<%= ModulePath %>/app" + abci "github.com/tendermint/tendermint/abci/types" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +func init() { + simapp.GetSimulatorFlags() +} + +type SimApp interface { + cosmoscmd.App + GetBaseApp() *baseapp.BaseApp + AppCodec() codec.Codec + SimulationManager() *module.SimulationManager + ModuleAccountAddrs() map[string]bool + Name() string + LegacyAmino() *codec.LegacyAmino + BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock + EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock + InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain +} + +var defaultConsensusParams = &abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxBytes: 200000, + MaxGas: 2000000, + }, + Evidence: &tmproto.EvidenceParams{ + MaxAgeNumBlocks: 302400, + MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration + MaxBytes: 10000, + }, + Validator: &tmproto.ValidatorParams{ + PubKeyTypes: []string{ + tmtypes.ABCIPubKeyTypeEd25519, + }, + }, +} + +// BenchmarkSimulation run the chain simulation +// Running using starport command: +// `starport chain simulate -v --numBlocks 200 --blockSize 50` +// Running as go benchmark test: +// `go test -benchmem -run=^$ -bench ^BenchmarkSimulation ./app -NumBlocks=200 -BlockSize 50 -Commit=true -Verbose=true -Enabled=true` +func BenchmarkSimulation(b *testing.B) { + simapp.FlagEnabledValue = true + simapp.FlagCommitValue = true + + config, db, dir, logger, _, err := simapp.SetupSimulation("goleveldb-app-sim", "Simulation") + require.NoError(b, err, "simulation setup failed") + + b.Cleanup(func() { + db.Close() + err = os.RemoveAll(dir) + require.NoError(b, err) + }) + + encoding := cosmoscmd.MakeEncodingConfig(app.ModuleBasics) + + app := app.New( + logger, + db, + nil, + true, + map[int64]bool{}, + app.DefaultNodeHome, + 0, + encoding, + simapp.EmptyAppOptions{}, + ) + + simApp, ok := app.(SimApp) + require.True(b, ok, "can't use simapp") + + // Run randomized simulations + _, simParams, simErr := simulation.SimulateFromSeed( + b, + os.Stdout, + simApp.GetBaseApp(), + simapp.AppStateFn(simApp.AppCodec(), simApp.SimulationManager()), + simulationtypes.RandomAccounts, + simapp.SimulationOperations(simApp, simApp.AppCodec(), config), + simApp.ModuleAccountAddrs(), + config, + simApp.AppCodec(), + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(simApp, config, simParams) + require.NoError(b, err) + require.NoError(b, simErr) + + if config.Commit { + simapp.PrintStats(db) + } +} diff --git a/starport/templates/app/stargate/go.mod.plush b/starport/templates/app/stargate/go.mod.plush index 97d177a906..85d192f100 100644 --- a/starport/templates/app/stargate/go.mod.plush +++ b/starport/templates/app/stargate/go.mod.plush @@ -3,7 +3,7 @@ module <%= ModulePath %> go 1.16 require ( - github.com/cosmos/cosmos-sdk v0.44.3 + github.com/cosmos/cosmos-sdk v0.44.5 github.com/cosmos/ibc-go v1.2.2 github.com/gogo/protobuf v1.3.3 github.com/google/go-cmp v0.5.6 // indirect @@ -12,7 +12,7 @@ require ( github.com/spf13/cast v1.3.1 github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 - github.com/tendermint/spm v0.1.8 + github.com/tendermint/spm v0.1.9 github.com/tendermint/tendermint v0.34.14 github.com/tendermint/tm-db v0.6.4 google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 diff --git a/starport/templates/app/stargate/readme.md.plush b/starport/templates/app/stargate/readme.md.plush index f4c21a484c..7676842a7f 100644 --- a/starport/templates/app/stargate/readme.md.plush +++ b/starport/templates/app/stargate/readme.md.plush @@ -1,5 +1,5 @@ # <%= AppName %> -**<%= AppName %>** is a blockchain built using Cosmos SDK and Tendermint and created with [Starport](https://github.com/tendermint/starport). +**<%= AppName %>** is a blockchain built using Cosmos SDK and Tendermint and created with [Starport](https://starport.com). ## Get started @@ -11,11 +11,7 @@ starport chain serve ### Configure -Your blockchain in development can be configured with `config.yml`. To learn more, see the [Starport docs](https://docs.starport.network). - -### Launch - -To launch your blockchain live on multiple nodes, use `starport network` commands. Learn more about [Starport Network](https://github.com/tendermint/spn). +Your blockchain in development can be configured with `config.yml`. To learn more, see the [Starport docs](https://docs.starport.com). ### Web Frontend @@ -43,14 +39,14 @@ After a draft release is created, make your final changes from the release page To install the latest version of your blockchain node's binary, execute the following command on your machine: ``` -curl https://get.starport.network/<%= OwnerAndRepoName %>@latest! | sudo bash +curl https://get.starport.com/<%= OwnerAndRepoName %>@latest! | sudo bash ``` `<%= OwnerAndRepoName %>` should match the `username` and `repo_name` of the Github repository to which the source code was pushed. Learn more about [the install process](https://github.com/allinbits/starport-installer). ## Learn more -- [Starport](https://github.com/tendermint/starport) -- [Starport Docs](https://docs.starport.network) -- [Cosmos SDK documentation](https://docs.cosmos.network) -- [Cosmos SDK Tutorials](https://tutorials.cosmos.network) -- [Discord](https://discord.gg/cosmosnetwork) +- [Starport](https://starport.com) +- [Tutorials](https://docs.starport.com/guide) +- [Starport docs](https://docs.starport.com) +- [Cosmos SDK docs](https://docs.cosmos.network) +- [Developer Chat](https://discord.gg/H6wGTY8sxw) diff --git a/starport/templates/field/datatype/bool.go b/starport/templates/field/datatype/bool.go new file mode 100644 index 0000000000..0d655a8074 --- /dev/null +++ b/starport/templates/field/datatype/bool.go @@ -0,0 +1,41 @@ +package datatype + +import ( + "fmt" + + "github.com/tendermint/starport/starport/pkg/multiformatname" +) + +var ( + // DataBool bool data type definition + DataBool = DataType{ + DataType: func(string) string { return "bool" }, + DefaultTestValue: "false", + ValueLoop: "false", + ValueIndex: "false", + ValueInvalidIndex: "false", + ProtoType: func(_, name string, index int) string { + return fmt.Sprintf("bool %s = %d", name, index) + }, + GenesisArgs: func(name multiformatname.Name, value int) string { + return fmt.Sprintf("%s: %t,\n", name.UpperCamel, value%2 == 0) + }, + CLIArgs: func(name multiformatname.Name, _, prefix string, argIndex int) string { + return fmt.Sprintf(`%s%s, err := cast.ToBoolE(args[%d]) + if err != nil { + return err + }`, + prefix, name.UpperCamel, argIndex) + }, + ToBytes: func(name string) string { + return fmt.Sprintf(`%[1]vBytes := []byte{0} + if %[1]v { + %[1]vBytes = []byte{1} + }`, name) + }, + ToString: func(name string) string { + return fmt.Sprintf("strconv.FormatBool(%s)", name) + }, + GoCLIImports: []GoImport{{Name: "github.com/spf13/cast"}}, + } +) diff --git a/starport/templates/field/datatype/coin.go b/starport/templates/field/datatype/coin.go new file mode 100644 index 0000000000..96410e5674 --- /dev/null +++ b/starport/templates/field/datatype/coin.go @@ -0,0 +1,49 @@ +package datatype + +import ( + "fmt" + + "github.com/tendermint/starport/starport/pkg/multiformatname" +) + +var ( + // DataCoin coin data type definition + DataCoin = DataType{ + DataType: func(string) string { return "sdk.Coin" }, + DefaultTestValue: "10token", + ProtoType: func(_, name string, index int) string { + return fmt.Sprintf("cosmos.base.v1beta1.Coin %s = %d [(gogoproto.nullable) = false]", + name, index) + }, + GenesisArgs: func(multiformatname.Name, int) string { return "" }, + CLIArgs: func(name multiformatname.Name, _, prefix string, argIndex int) string { + return fmt.Sprintf(`%s%s, err := sdk.ParseCoinNormalized(args[%d]) + if err != nil { + return err + }`, prefix, name.UpperCamel, argIndex) + }, + GoCLIImports: []GoImport{{Name: "github.com/cosmos/cosmos-sdk/types", Alias: "sdk"}}, + ProtoImports: []string{"gogoproto/gogo.proto", "cosmos/base/v1beta1/coin.proto"}, + NonIndex: true, + } + + // DataCoinSlice coin array data type definition + DataCoinSlice = DataType{ + DataType: func(string) string { return "sdk.Coins" }, + DefaultTestValue: "10token,20stake", + ProtoType: func(_, name string, index int) string { + return fmt.Sprintf("repeated cosmos.base.v1beta1.Coin %s = %d [(gogoproto.nullable) = false]", + name, index) + }, + GenesisArgs: func(multiformatname.Name, int) string { return "" }, + CLIArgs: func(name multiformatname.Name, _, prefix string, argIndex int) string { + return fmt.Sprintf(`%s%s, err := sdk.ParseCoinsNormalized(args[%d]) + if err != nil { + return err + }`, prefix, name.UpperCamel, argIndex) + }, + GoCLIImports: []GoImport{{Name: "github.com/cosmos/cosmos-sdk/types", Alias: "sdk"}}, + ProtoImports: []string{"gogoproto/gogo.proto", "cosmos/base/v1beta1/coin.proto"}, + NonIndex: true, + } +) diff --git a/starport/templates/field/datatype/custom.go b/starport/templates/field/datatype/custom.go new file mode 100644 index 0000000000..d677bb6f39 --- /dev/null +++ b/starport/templates/field/datatype/custom.go @@ -0,0 +1,30 @@ +package datatype + +import ( + "fmt" + + "github.com/tendermint/starport/starport/pkg/multiformatname" +) + +var ( + // DataCustom custom data type definition + DataCustom = DataType{ + DataType: func(datatype string) string { return fmt.Sprintf("*%s", datatype) }, + DefaultTestValue: "null", + ProtoType: func(datatype, name string, index int) string { + return fmt.Sprintf("%s %s = %d", datatype, name, index) + }, + GenesisArgs: func(name multiformatname.Name, value int) string { + return fmt.Sprintf("%s: new(types.%s),\n", name.UpperCamel, name.UpperCamel) + }, + CLIArgs: func(name multiformatname.Name, datatype, prefix string, argIndex int) string { + return fmt.Sprintf(`%[1]v%[2]v := new(types.%[3]v) + err = json.Unmarshal([]byte(args[%[4]v]), %[1]v%[2]v) + if err != nil { + return err + }`, prefix, name.UpperCamel, datatype, argIndex) + }, + GoCLIImports: []GoImport{{Name: "encoding/json"}}, + NonIndex: true, + } +) diff --git a/starport/templates/field/datatype/int.go b/starport/templates/field/datatype/int.go new file mode 100644 index 0000000000..f45917f87a --- /dev/null +++ b/starport/templates/field/datatype/int.go @@ -0,0 +1,64 @@ +package datatype + +import ( + "fmt" + + "github.com/tendermint/starport/starport/pkg/multiformatname" +) + +var ( + // DataInt int data type definition + DataInt = DataType{ + DataType: func(string) string { return "int32" }, + DefaultTestValue: "111", + ValueLoop: "int32(i)", + ValueIndex: "0", + ValueInvalidIndex: "100000", + ProtoType: func(_, name string, index int) string { + return fmt.Sprintf("int32 %s = %d", name, index) + }, + GenesisArgs: func(name multiformatname.Name, value int) string { + return fmt.Sprintf("%s: %d,\n", name.UpperCamel, value) + }, + CLIArgs: func(name multiformatname.Name, _, prefix string, argIndex int) string { + return fmt.Sprintf(`%s%s, err := cast.ToInt32E(args[%d]) + if err != nil { + return err + }`, + prefix, name.UpperCamel, argIndex) + }, + ToBytes: func(name string) string { + return fmt.Sprintf(`%[1]vBytes := make([]byte, 4) + binary.BigEndian.PutUint32(%[1]vBytes, uint32(%[1]v))`, name) + }, + ToString: func(name string) string { + return fmt.Sprintf("strconv.Itoa(int(%s))", name) + }, + GoCLIImports: []GoImport{{Name: "github.com/spf13/cast"}}, + } + + // DataIntSlice int array data type definition + DataIntSlice = DataType{ + DataType: func(string) string { return "[]int32" }, + DefaultTestValue: "1,2,3,4,5", + ProtoType: func(_, name string, index int) string { + return fmt.Sprintf("repeated int32 %s = %d", name, index) + }, + GenesisArgs: func(name multiformatname.Name, value int) string { + return fmt.Sprintf("%s: []int32{%d},\n", name.UpperCamel, value) + }, + CLIArgs: func(name multiformatname.Name, _, prefix string, argIndex int) string { + return fmt.Sprintf(`%[1]vCast%[2]v := strings.Split(args[%[3]v], listSeparator) + %[1]v%[2]v := make([]int32, len(%[1]vCast%[2]v)) + for i, arg := range %[1]vCast%[2]v { + value, err := cast.ToInt32E(arg) + if err != nil { + return err + } + %[1]v%[2]v[i] = value + }`, prefix, name.UpperCamel, argIndex) + }, + GoCLIImports: []GoImport{{Name: "github.com/spf13/cast"}, {Name: "strings"}}, + NonIndex: true, + } +) diff --git a/starport/templates/field/datatype/string.go b/starport/templates/field/datatype/string.go new file mode 100644 index 0000000000..7b3db3246e --- /dev/null +++ b/starport/templates/field/datatype/string.go @@ -0,0 +1,51 @@ +package datatype + +import ( + "fmt" + + "github.com/tendermint/starport/starport/pkg/multiformatname" +) + +var ( + // DataString string data type definition + DataString = DataType{ + DataType: func(string) string { return "string" }, + DefaultTestValue: "xyz", + ValueLoop: "strconv.Itoa(i)", + ValueIndex: "strconv.Itoa(0)", + ValueInvalidIndex: "strconv.Itoa(100000)", + ProtoType: func(_, name string, index int) string { + return fmt.Sprintf("string %s = %d", name, index) + }, + GenesisArgs: func(name multiformatname.Name, value int) string { + return fmt.Sprintf("%s: \"%d\",\n", name.UpperCamel, value) + }, + CLIArgs: func(name multiformatname.Name, _, prefix string, argIndex int) string { + return fmt.Sprintf("%s%s := args[%d]", prefix, name.UpperCamel, argIndex) + }, + ToBytes: func(name string) string { + return fmt.Sprintf("%[1]vBytes := []byte(%[1]v)", name) + }, + ToString: func(name string) string { + return name + }, + } + + // DataStringSlice string array data type definition + DataStringSlice = DataType{ + DataType: func(string) string { return "[]string" }, + DefaultTestValue: "abc,xyz", + ProtoType: func(_, name string, index int) string { + return fmt.Sprintf("repeated string %s = %d", name, index) + }, + GenesisArgs: func(name multiformatname.Name, value int) string { + return fmt.Sprintf("%s: []string{\"%d\"},\n", name.UpperCamel, value) + }, + CLIArgs: func(name multiformatname.Name, _, prefix string, argIndex int) string { + return fmt.Sprintf(`%[1]v%[2]v := strings.Split(args[%[3]v], listSeparator)`, + prefix, name.UpperCamel, argIndex) + }, + GoCLIImports: []GoImport{{Name: "strings"}}, + NonIndex: true, + } +) diff --git a/starport/templates/field/datatype/types.go b/starport/templates/field/datatype/types.go new file mode 100644 index 0000000000..f67faa8f03 --- /dev/null +++ b/starport/templates/field/datatype/types.go @@ -0,0 +1,89 @@ +package datatype + +import ( + "github.com/tendermint/starport/starport/pkg/multiformatname" +) + +const ( + // Separator represents the type separator + Separator = ":" +) + +const ( + // String represents the string type name + String Name = "string" + // StringSlice represents the string array type name + StringSlice Name = "array.string" + // Bool represents the bool type name + Bool Name = "bool" + // Int represents the int type name + Int Name = "int" + // IntSlice represents the int array type name + IntSlice Name = "array.int" + // Uint represents the uint type name + Uint Name = "uint" + // UintSlice represents the uint array type name + UintSlice Name = "array.uint" + // Coin represents the coin type name + Coin Name = "coin" + // Coins represents the coin array type name + Coins Name = "array.coin" + // Custom represents the custom type name + Custom Name = Name(TypeCustom) + + // StringSliceAlias represents the string array type name alias + StringSliceAlias Name = "strings" + // IntSliceAlias represents the int array type name alias + IntSliceAlias Name = "ints" + // UintSliceAlias represents the uint array type name alias + UintSliceAlias Name = "uints" + // CoinSliceAlias represents the coin array type name alias + CoinSliceAlias Name = "coins" + + // TypeCustom represents the string type name id + TypeCustom = "customstarporttype" +) + +// SupportedTypes all support data types and definitions +var SupportedTypes = map[Name]DataType{ + String: DataString, + StringSlice: DataStringSlice, + StringSliceAlias: DataStringSlice, + Bool: DataBool, + Int: DataInt, + IntSlice: DataIntSlice, + IntSliceAlias: DataIntSlice, + Uint: DataUint, + UintSlice: DataUintSlice, + UintSliceAlias: DataUintSlice, + Coin: DataCoin, + Coins: DataCoinSlice, + CoinSliceAlias: DataCoinSlice, + Custom: DataCustom, +} + +// Name represents the Alias Name for the data type +type Name string + +// DataType represents the data types for code replacement +type DataType struct { + DataType func(datatype string) string + ProtoType func(datatype, name string, index int) string + GenesisArgs func(name multiformatname.Name, value int) string + ProtoImports []string + GoCLIImports []GoImport + DefaultTestValue string + ValueLoop string + ValueIndex string + ValueInvalidIndex string + ToBytes func(name string) string + ToString func(name string) string + CLIArgs func(name multiformatname.Name, datatype, prefix string, argIndex int) string + NonIndex bool +} + +// GoImport represents the go import repo name with the alias +type GoImport struct { + Name string + Alias string +} diff --git a/starport/templates/field/datatype/uint.go b/starport/templates/field/datatype/uint.go new file mode 100644 index 0000000000..caeb51b2fb --- /dev/null +++ b/starport/templates/field/datatype/uint.go @@ -0,0 +1,65 @@ +package datatype + +import ( + "fmt" + + "github.com/tendermint/starport/starport/pkg/multiformatname" +) + +var ( + // DataUint uint data type definition + DataUint = DataType{ + DataType: func(string) string { return "uint64" }, + DefaultTestValue: "111", + ValueLoop: "uint64(i)", + ValueIndex: "0", + ValueInvalidIndex: "100000", + ProtoType: func(_, name string, index int) string { + return fmt.Sprintf("uint64 %s = %d", name, index) + }, + GenesisArgs: func(name multiformatname.Name, value int) string { + return fmt.Sprintf("%s: %d,\n", name.UpperCamel, value) + }, + CLIArgs: func(name multiformatname.Name, _, prefix string, argIndex int) string { + return fmt.Sprintf(`%s%s, err := cast.ToUint64E(args[%d]) + if err != nil { + return err + }`, + prefix, name.UpperCamel, argIndex) + }, + ToBytes: func(name string) string { + return fmt.Sprintf(`%[1]vBytes := make([]byte, 8) + binary.BigEndian.PutUint64(%[1]vBytes, %[1]v)`, name) + }, + ToString: func(name string) string { + return fmt.Sprintf("strconv.Itoa(int(%s))", name) + }, + GoCLIImports: []GoImport{{Name: "github.com/spf13/cast"}}, + } + + // DataUintSlice uint array data type definition + DataUintSlice = DataType{ + DataType: func(string) string { return "[]uint64" }, + DefaultTestValue: "1,2,3,4,5", + ProtoType: func(_, name string, index int) string { + return fmt.Sprintf("repeated uint64 %s = %d", name, index) + }, + GenesisArgs: func(name multiformatname.Name, value int) string { + return fmt.Sprintf("%s: []uint64{%d},\n", name.UpperCamel, value) + }, + CLIArgs: func(name multiformatname.Name, _, prefix string, argIndex int) string { + return fmt.Sprintf(`%[1]vCast%[2]v := strings.Split(args[%[3]v], listSeparator) + %[1]v%[2]v := make([]uint64, len(%[1]vCast%[2]v)) + for i, arg := range %[1]vCast%[2]v { + value, err := cast.ToUint64E(arg) + if err != nil { + return err + } + %[1]v%[2]v[i] = value + }`, + prefix, name.UpperCamel, argIndex) + }, + GoCLIImports: []GoImport{{Name: "github.com/spf13/cast"}, {Name: "strings"}}, + NonIndex: true, + } +) diff --git a/starport/templates/field/field.go b/starport/templates/field/field.go new file mode 100644 index 0000000000..e8c169aaae --- /dev/null +++ b/starport/templates/field/field.go @@ -0,0 +1,145 @@ +// Package field provides methods to parse a field provided in a command with the format name:type +package field + +import ( + "fmt" + + "github.com/tendermint/starport/starport/pkg/multiformatname" + "github.com/tendermint/starport/starport/templates/field/datatype" +) + +// Field represents a field inside a structure for a component +// it can be a field contained in a type or inside the response of a query, etc... +type Field struct { + Name multiformatname.Name + DatatypeName datatype.Name + Datatype string +} + +// DataType returns the field Datatype +func (f Field) DataType() string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + return dt.DataType(f.Datatype) +} + +// ProtoFieldName returns the field name used in proto +func (f Field) ProtoFieldName() string { + return f.Name.LowerCamel +} + +// ProtoType returns the field proto Datatype +func (f Field) ProtoType(index int) string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + return dt.ProtoType(f.Datatype, f.ProtoFieldName(), index) +} + +// DefaultTestValue returns the Datatype value default +func (f Field) DefaultTestValue() string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + return dt.DefaultTestValue +} + +// ValueLoop returns the Datatype value for loop iteration +func (f Field) ValueLoop() string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + if dt.NonIndex { + panic(fmt.Sprintf("non index type %s", f.DatatypeName)) + } + return dt.ValueLoop +} + +// ValueIndex returns the Datatype value for indexes +func (f Field) ValueIndex() string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + if dt.NonIndex { + panic(fmt.Sprintf("non index type %s", f.DatatypeName)) + } + return dt.ValueIndex +} + +// ValueInvalidIndex returns the Datatype value for invalid indexes +func (f Field) ValueInvalidIndex() string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + if dt.NonIndex { + panic(fmt.Sprintf("non index type %s", f.DatatypeName)) + } + return dt.ValueInvalidIndex +} + +// GenesisArgs returns the Datatype genesis args +func (f Field) GenesisArgs(value int) string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + return dt.GenesisArgs(f.Name, value) +} + +// CLIArgs returns the Datatype CLI args +func (f Field) CLIArgs(prefix string, argIndex int) string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + return dt.CLIArgs(f.Name, f.Datatype, prefix, argIndex) +} + +// ToBytes returns the Datatype byte array cast +func (f Field) ToBytes(name string) string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + if dt.NonIndex { + panic(fmt.Sprintf("non index type %s", f.DatatypeName)) + } + return dt.ToBytes(name) +} + +// ToString returns the Datatype byte array cast +func (f Field) ToString(name string) string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + if dt.NonIndex { + panic(fmt.Sprintf("non index type %s", f.DatatypeName)) + } + return dt.ToString(name) +} + +// GoCLIImports returns the Datatype imports for CLI package +func (f Field) GoCLIImports() []datatype.GoImport { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + return dt.GoCLIImports +} + +// ProtoImports return the Datatype imports for proto files +func (f Field) ProtoImports() []string { + dt, ok := datatype.SupportedTypes[f.DatatypeName] + if !ok { + panic(fmt.Sprintf("unknown type %s", f.DatatypeName)) + } + return dt.ProtoImports +} diff --git a/starport/templates/field/fields.go b/starport/templates/field/fields.go new file mode 100644 index 0000000000..27c5a527df --- /dev/null +++ b/starport/templates/field/fields.go @@ -0,0 +1,67 @@ +package field + +import ( + "fmt" + + "github.com/tendermint/starport/starport/pkg/multiformatname" + "github.com/tendermint/starport/starport/templates/field/datatype" +) + +// Fields represents a Field slice +type Fields []Field + +// GoCLIImports return all go CLI imports +func (f Fields) GoCLIImports() []datatype.GoImport { + allImports := make([]datatype.GoImport, 0) + exist := make(map[string]struct{}) + for _, fields := range f { + for _, goImport := range fields.GoCLIImports() { + if _, ok := exist[goImport.Name]; ok { + continue + } + exist[goImport.Name] = struct{}{} + allImports = append(allImports, goImport) + } + } + return allImports +} + +// ProtoImports return all proto imports +func (f Fields) ProtoImports() []string { + allImports := make([]string, 0) + exist := make(map[string]struct{}) + for _, fields := range f { + for _, protoImport := range fields.ProtoImports() { + if _, ok := exist[protoImport]; ok { + continue + } + exist[protoImport] = struct{}{} + allImports = append(allImports, protoImport) + } + } + return allImports +} + +// String return all inline fields args for command usage +func (f Fields) String() string { + args := "" + for _, field := range f { + args += fmt.Sprintf(" [%s]", field.Name.Kebab) + } + return args +} + +// Custom return a list of custom fields +func (f Fields) Custom() []string { + fields := make([]string, 0) + for _, field := range f { + if field.DatatypeName == datatype.TypeCustom { + dataType, err := multiformatname.NewName(field.Datatype) + if err != nil { + panic(err) + } + fields = append(fields, dataType.Snake) + } + } + return fields +} diff --git a/starport/templates/field/parse.go b/starport/templates/field/parse.go new file mode 100644 index 0000000000..23c1e4aeb0 --- /dev/null +++ b/starport/templates/field/parse.go @@ -0,0 +1,82 @@ +package field + +import ( + "fmt" + "strings" + + "github.com/tendermint/starport/starport/pkg/multiformatname" + "github.com/tendermint/starport/starport/templates/field/datatype" +) + +// validateField validates the field Name and type, and checks the name is not forbidden by Starport +func validateField(field string, isForbiddenField func(string) error) (multiformatname.Name, datatype.Name, error) { + fieldSplit := strings.Split(field, datatype.Separator) + if len(fieldSplit) > 2 { + return multiformatname.Name{}, "", fmt.Errorf("invalid field format: %s, should be 'Name' or 'Name:type'", field) + } + + name, err := multiformatname.NewName(fieldSplit[0]) + if err != nil { + return name, "", err + + } + + // Ensure the field Name is not a Go reserved Name, it would generate an incorrect code + if err := isForbiddenField(name.LowerCamel); err != nil { + return name, "", fmt.Errorf("%s can't be used as a field Name: %s", name, err.Error()) + } + + // Check if the object has an explicit type. The default is a string + dataTypeName := datatype.String + isTypeSpecified := len(fieldSplit) == 2 + if isTypeSpecified { + dataTypeName = datatype.Name(fieldSplit[1]) + } + return name, dataTypeName, nil +} + +// ParseFields parses the provided fields, analyses the types +// and checks there is no duplicated field +func ParseFields( + fields []string, + isForbiddenField func(string) error, + forbiddenFieldNames ...string, +) (Fields, error) { + // Used to check duplicated field + existingFields := make(map[string]struct{}) + for _, name := range forbiddenFieldNames { + if name != "" { + existingFields[name] = struct{}{} + } + } + + var parsedFields Fields + for _, field := range fields { + name, datatypeName, err := validateField(field, isForbiddenField) + if err != nil { + return parsedFields, err + } + + // Ensure the field is not duplicated + if _, exists := existingFields[name.LowerCamel]; exists { + return parsedFields, fmt.Errorf("the field %s is duplicated", name.Original) + } + existingFields[name.LowerCamel] = struct{}{} + + // Check if is a static type + if _, ok := datatype.SupportedTypes[datatypeName]; ok { + parsedFields = append(parsedFields, Field{ + Name: name, + DatatypeName: datatypeName, + }) + continue + } + + parsedFields = append(parsedFields, Field{ + Name: name, + Datatype: string(datatypeName), + DatatypeName: datatype.TypeCustom, + }) + } + return parsedFields, nil +} diff --git a/starport/templates/field/parse_test.go b/starport/templates/field/parse_test.go new file mode 100644 index 0000000000..398328c72e --- /dev/null +++ b/starport/templates/field/parse_test.go @@ -0,0 +1,196 @@ +package field + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tendermint/starport/starport/pkg/multiformatname" + "github.com/tendermint/starport/starport/templates/field/datatype" +) + +var ( + noCheck = func(string) error { return nil } + alwaysInvalid = func(string) error { return errors.New("invalid Name") } +) + +func TestForbiddenParseFields(t *testing.T) { + // check doesn't pass + _, err := ParseFields([]string{"foo"}, alwaysInvalid) + require.Error(t, err) + + // duplicated field + _, err = ParseFields([]string{"foo", "foo:int"}, noCheck) + require.Error(t, err) + + // invalid type + _, err = ParseFields([]string{"foo:invalid"}, alwaysInvalid) + require.Error(t, err) + + // invalid field Name + _, err = ParseFields([]string{"foo@bar:int"}, alwaysInvalid) + require.Error(t, err) + + // invalid format + _, err = ParseFields([]string{"foo:int:int"}, alwaysInvalid) + require.Error(t, err) +} + +func TestParseFields1(t *testing.T) { + name1, err := multiformatname.NewName("foo") + require.NoError(t, err) + name2, err := multiformatname.NewName("fooBar") + require.NoError(t, err) + name3, err := multiformatname.NewName("bar-foo") + require.NoError(t, err) + name4, err := multiformatname.NewName("foo_foo") + require.NoError(t, err) + + tests := []struct { + name string + fields []string + want Fields + err error + }{ + { + name: "test string types", + fields: []string{ + name1.Original, + name2.Original + ":string", + }, + want: Fields{ + { + Name: name1, + DatatypeName: datatype.String, + }, + { + Name: name2, + DatatypeName: datatype.String, + }, + }, + }, + { + name: "test number types", + fields: []string{ + name1.Original + ":uint", + name2.Original + ":int", + name3.Original + ":bool", + }, + want: Fields{ + { + Name: name1, + DatatypeName: datatype.Uint, + }, + { + Name: name2, + DatatypeName: datatype.Int, + }, + { + Name: name3, + DatatypeName: datatype.Bool, + }, + }, + }, + { + name: "test list types", + fields: []string{ + name1.Original + ":array.uint", + name2.Original + ":array.int", + name3.Original + ":array.string", + }, + want: Fields{ + { + Name: name1, + DatatypeName: datatype.UintSlice, + }, + { + Name: name2, + DatatypeName: datatype.IntSlice, + }, + { + Name: name3, + DatatypeName: datatype.StringSlice, + }, + }, + }, + { + name: "test mixed types", + fields: []string{ + name1.Original + ":uint", + name2.Original + ":array.coin", + name3.Original, + name4.Original + ":strings", + }, + want: Fields{ + { + Name: name1, + DatatypeName: datatype.Uint, + }, + { + Name: name2, + DatatypeName: datatype.Coins, + }, + { + Name: name3, + DatatypeName: datatype.String, + }, + { + Name: name4, + DatatypeName: datatype.StringSliceAlias, + }, + }, + }, + { + name: "test custom types", + fields: []string{ + name1.Original + ":Bla", + name2.Original + ":Test", + name3.Original, + }, + want: Fields{ + { + Name: name1, + DatatypeName: datatype.Custom, + Datatype: "Bla", + }, + { + Name: name2, + DatatypeName: datatype.Custom, + Datatype: "Test", + }, + { + Name: name3, + DatatypeName: datatype.String, + }, + }, + }, + { + name: "test sdk.Coin types", + fields: []string{ + name1.Original + ":coin", + name2.Original + ":array.coin", + }, + want: Fields{ + { + Name: name1, + DatatypeName: datatype.Coin, + }, + { + Name: name2, + DatatypeName: datatype.Coins, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseFields(tt.fields, noCheck) + if tt.err != nil { + require.ErrorIs(t, err, tt.err) + return + } + require.NoError(t, err) + require.EqualValues(t, tt.want, got) + }) + } +} diff --git a/starport/templates/field/plushhelpers/plushhelpers.go b/starport/templates/field/plushhelpers/plushhelpers.go new file mode 100644 index 0000000000..f357b5693f --- /dev/null +++ b/starport/templates/field/plushhelpers/plushhelpers.go @@ -0,0 +1,62 @@ +package plushhelpers + +import ( + "strings" + + "github.com/gobuffalo/plush" + "github.com/tendermint/starport/starport/templates/field" + "github.com/tendermint/starport/starport/templates/field/datatype" +) + +// ExtendPlushContext sets available field helpers on the provided context. +func ExtendPlushContext(ctx *plush.Context) { + ctx.Set("mergeGoImports", mergeGoImports) + ctx.Set("mergeProtoImports", mergeProtoImports) + ctx.Set("mergeCustomImports", mergeCustomImports) + ctx.Set("title", strings.Title) +} + +func mergeCustomImports(fields ...field.Fields) []string { + allImports := make([]string, 0) + exist := make(map[string]struct{}) + for _, fields := range fields { + for _, customImport := range fields.Custom() { + if _, ok := exist[customImport]; ok { + continue + } + exist[customImport] = struct{}{} + allImports = append(allImports, customImport) + } + } + return allImports +} + +func mergeGoImports(fields ...field.Fields) []datatype.GoImport { + allImports := make([]datatype.GoImport, 0) + exist := make(map[string]struct{}) + for _, fields := range fields { + for _, goImport := range fields.GoCLIImports() { + if _, ok := exist[goImport.Name]; ok { + continue + } + exist[goImport.Name] = struct{}{} + allImports = append(allImports, goImport) + } + } + return allImports +} + +func mergeProtoImports(fields ...field.Fields) []string { + allImports := make([]string, 0) + exist := make(map[string]struct{}) + for _, fields := range fields { + for _, protoImport := range fields.ProtoImports() { + if _, ok := exist[protoImport]; ok { + continue + } + exist[protoImport] = struct{}{} + allImports = append(allImports, protoImport) + } + } + return allImports +} diff --git a/starport/templates/ibc/oracle.go b/starport/templates/ibc/oracle.go index 7199d98fa5..01d08b0193 100644 --- a/starport/templates/ibc/oracle.go +++ b/starport/templates/ibc/oracle.go @@ -3,7 +3,6 @@ package ibc import ( "embed" "fmt" - "os" "path/filepath" "strings" @@ -12,18 +11,15 @@ import ( "github.com/gobuffalo/plushgen" "github.com/tendermint/starport/starport/pkg/multiformatname" "github.com/tendermint/starport/starport/pkg/placeholder" - "github.com/tendermint/starport/starport/pkg/plushhelpers" "github.com/tendermint/starport/starport/pkg/xgenny" "github.com/tendermint/starport/starport/pkg/xstrings" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" "github.com/tendermint/starport/starport/templates/testutil" ) var ( - //go:embed oracle/static/* oracle/static/**/* - fsOracleStatic embed.FS - - //go:embed oracle/dynamic/* oracle/dynamic/**/* - fsOracleDynamic embed.FS + //go:embed oracle/* oracle/**/* + fsOracle embed.FS ) // OracleOptions are options to scaffold an oracle query in a IBC module @@ -41,6 +37,8 @@ type OracleOptions struct { func NewOracle(replacer placeholder.Replacer, opts *OracleOptions) (*genny.Generator, error) { g := genny.New() + template := xgenny.NewEmbedWalker(fsOracle, "oracle/", opts.AppPath) + g.RunFn(moduleOracleModify(replacer, opts)) g.RunFn(protoQueryOracleModify(replacer, opts)) g.RunFn(protoTxOracleModify(replacer, opts)) @@ -49,12 +47,6 @@ func NewOracle(replacer placeholder.Replacer, opts *OracleOptions) (*genny.Gener g.RunFn(clientCliTxOracleModify(replacer, opts)) g.RunFn(codecOracleModify(replacer, opts)) - err := box(g, opts) - if err != nil { - return g, err - } - g.RunFn(packetHandlerOracleModify(replacer, opts)) - ctx := plush.NewContext() ctx.Set("moduleName", opts.ModuleName) ctx.Set("ModulePath", opts.ModulePath) @@ -66,41 +58,23 @@ func NewOracle(replacer placeholder.Replacer, opts *OracleOptions) (*genny.Gener // Used for proto package name ctx.Set("formatOwnerName", xstrings.FormatUsername) - // Create the 'testutil' package with the test helpers - if err := testutil.Register(ctx, g, opts.AppPath); err != nil { - return g, err - } - plushhelpers.ExtendPlushContext(ctx) g.Transformer(plushgen.Transformer(ctx)) g.Transformer(genny.Replace("{{moduleName}}", opts.ModuleName)) g.Transformer(genny.Replace("{{queryName}}", opts.QueryName.Snake)) - return g, nil -} -func box(g *genny.Generator, opts *OracleOptions) error { - var ( - gs = genny.New() - path = filepath.Join(opts.AppPath, "x", opts.ModuleName, "oracle.go") + // Create the 'testutil' package with the test helpers + if err := testutil.Register(g, opts.AppPath); err != nil { + return g, err + } - staticTemplate = xgenny.NewEmbedWalker( - fsOracleStatic, - "oracle/static/", - opts.AppPath, - ) - dynamicTemplate = xgenny.NewEmbedWalker( - fsOracleDynamic, - "oracle/dynamic/", - opts.AppPath, - ) - ) - if _, err := os.Stat(path); os.IsNotExist(err) { - if err := gs.Box(staticTemplate); err != nil { - return err - } + if err := xgenny.Box(g, template); err != nil { + return g, err } - g.Merge(gs) - return g.Box(dynamicTemplate) + + g.RunFn(packetHandlerOracleModify(replacer, opts)) + + return g, nil } func moduleOracleModify(replacer placeholder.Replacer, opts *OracleOptions) genny.RunFn { @@ -158,19 +132,19 @@ func protoQueryOracleModify(replacer placeholder.Replacer, opts *OracleOptions) templateService := ` // %[2]vResult defines a rpc handler method for Msg%[2]vData. rpc %[2]vResult(Query%[2]vRequest) returns (Query%[2]vResponse) { - option (google.api.http).get = "/%[4]v/%[5]v/%[3]v_result/{request_id}"; + option (google.api.http).get = "/%[3]v/%[4]v/%[5]v_result/{request_id}"; } // Last%[2]vId query the last %[2]v result id rpc Last%[2]vId(QueryLast%[2]vIdRequest) returns (QueryLast%[2]vIdResponse) { - option (google.api.http).get = "/%[4]v/%[5]v/last_%[3]v_id"; + option (google.api.http).get = "/%[3]v/%[4]v/last_%[5]v_id"; } %[1]v` replacementService := fmt.Sprintf(templateService, Placeholder2, opts.QueryName.UpperCamel, - opts.QueryName.Snake, opts.AppName, opts.ModuleName, + opts.QueryName.Snake, ) content = replacer.Replace(content, Placeholder2, replacementService) @@ -235,10 +209,9 @@ import "%[2]v/%[3]v.proto"; (gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" ]; - string request_key = 8; - uint64 prepare_gas = 9; - uint64 execute_gas = 10; - string client_id = 11 [(gogoproto.customname) = "ClientID"]; + uint64 prepare_gas = 8; + uint64 execute_gas = 9; + string client_id = 10 [(gogoproto.customname) = "ClientID"]; } message Msg%[2]vDataResponse { diff --git a/starport/templates/ibc/oracle/dynamic/proto/{{moduleName}}/{{queryName}}.proto.plush b/starport/templates/ibc/oracle/proto/{{moduleName}}/{{queryName}}.proto.plush similarity index 100% rename from starport/templates/ibc/oracle/dynamic/proto/{{moduleName}}/{{queryName}}.proto.plush rename to starport/templates/ibc/oracle/proto/{{moduleName}}/{{queryName}}.proto.plush diff --git a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/client/cli/query_{{queryName}}.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/client/cli/query_{{queryName}}.go.plush similarity index 100% rename from starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/client/cli/query_{{queryName}}.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/client/cli/query_{{queryName}}.go.plush diff --git a/starport/templates/ibc/oracle/static/x/{{moduleName}}/client/cli/tx_oracle.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/client/cli/tx_oracle.go.plush similarity index 86% rename from starport/templates/ibc/oracle/static/x/{{moduleName}}/client/cli/tx_oracle.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/client/cli/tx_oracle.go.plush index af5f3bb4db..85ee6da6d2 100644 --- a/starport/templates/ibc/oracle/static/x/{{moduleName}}/client/cli/tx_oracle.go.plush +++ b/starport/templates/ibc/oracle/x/{{moduleName}}/client/cli/tx_oracle.go.plush @@ -5,7 +5,6 @@ const ( flagSymbols = "symbols" flagMultiplier = "multiplier" flagFeeLimit = "fee-limit" - flagRequestkey = "request-key" flagPrepareGas = "prepare-gas" flagExecuteGas = "execute-gas" ) diff --git a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/client/cli/tx_{{queryName}}.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/client/cli/tx_{{queryName}}.go.plush similarity index 92% rename from starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/client/cli/tx_{{queryName}}.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/client/cli/tx_{{queryName}}.go.plush index b94041da20..931529fa57 100644 --- a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/client/cli/tx_{{queryName}}.go.plush +++ b/starport/templates/ibc/oracle/x/{{moduleName}}/client/cli/tx_{{queryName}}.go.plush @@ -70,12 +70,6 @@ func CmdRequest<%= queryName.UpperCamel %>Data() *cobra.Command { return err } - // retrieve the request key corresponding to the pool account (used to pay fee) on BandChain. - requestKey, err := cmd.Flags().GetString(flagRequestkey) - if err != nil { - return err - } - // retrieve the amount of gas allowed for the prepare step of the oracle script. prepareGas, err := cmd.Flags().GetUint64(flagPrepareGas) if err != nil { @@ -101,7 +95,6 @@ func CmdRequest<%= queryName.UpperCamel %>Data() *cobra.Command { askCount, minCount, feeLimit, - requestKey, prepareGas, executeGas, ) @@ -117,7 +110,6 @@ func CmdRequest<%= queryName.UpperCamel %>Data() *cobra.Command { cmd.Flags().StringSlice(flagSymbols, nil, "Symbols used in calling the oracle script") cmd.Flags().Uint64(flagMultiplier, 1000000, "Multiplier used in calling the oracle script") cmd.Flags().String(flagFeeLimit, "", "the maximum tokens that will be paid to all data source providers") - cmd.Flags().String(flagRequestkey, "", "Key for generating escrow address") cmd.Flags().Uint64(flagPrepareGas, 200000, "Prepare gas used in fee counting for prepare request") cmd.Flags().Uint64(flagExecuteGas, 200000, "Execute gas used in fee counting for execute request") flags.AddTxFlagsToCmd(cmd) diff --git a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/keeper/grpc_query_{{queryName}}.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/keeper/grpc_query_{{queryName}}.go.plush similarity index 100% rename from starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/keeper/grpc_query_{{queryName}}.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/keeper/grpc_query_{{queryName}}.go.plush diff --git a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/keeper/msg_{{queryName}}.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/keeper/msg_{{queryName}}.go.plush similarity index 89% rename from starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/keeper/msg_{{queryName}}.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/keeper/msg_{{queryName}}.go.plush index 3ec23730be..d7cf1d9ded 100644 --- a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/keeper/msg_{{queryName}}.go.plush +++ b/starport/templates/ibc/oracle/x/{{moduleName}}/keeper/msg_{{queryName}}.go.plush @@ -20,7 +20,7 @@ func (k msgServer) <%= queryName.UpperCamel %>Data(goCtx context.Context, msg *t ctx := sdk.UnwrapSDKContext(goCtx) sourcePort := types.PortID - sourceChannelEnd, found := k.channelKeeper.GetChannel(ctx, sourcePort, msg.SourceChannel) + sourceChannelEnd, found := k.ChannelKeeper.GetChannel(ctx, sourcePort, msg.SourceChannel) if !found { return nil, sdkerrors.Wrapf( sdkerrors.ErrUnknownRequest, @@ -33,14 +33,14 @@ func (k msgServer) <%= queryName.UpperCamel %>Data(goCtx context.Context, msg *t destinationChannel := sourceChannelEnd.GetCounterparty().GetChannelID() // get the next sequence - sequence, found := k.channelKeeper.GetNextSequenceSend(ctx, sourcePort, msg.SourceChannel) + sequence, found := k.ChannelKeeper.GetNextSequenceSend(ctx, sourcePort, msg.SourceChannel) if !found { return nil, sdkerrors.Wrapf( channeltypes.ErrSequenceSendNotFound, "source port: %s, source channel: %s", sourcePort, msg.SourceChannel) } - channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, msg.SourceChannel)) + channelCap, ok := k.ScopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, msg.SourceChannel)) if !ok { return nil, sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability") @@ -58,7 +58,7 @@ func (k msgServer) <%= queryName.UpperCamel %>Data(goCtx context.Context, msg *t msg.ExecuteGas, ) - err := k.channelKeeper.SendPacket(ctx, channelCap, channeltypes.NewPacket( + err := k.ChannelKeeper.SendPacket(ctx, channelCap, channeltypes.NewPacket( packetData.GetBytes(), sequence, sourcePort, diff --git a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/keeper/{{queryName}}.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/keeper/{{queryName}}.go.plush similarity index 100% rename from starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/keeper/{{queryName}}.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/keeper/{{queryName}}.go.plush diff --git a/starport/templates/ibc/oracle/static/x/{{moduleName}}/oracle.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/oracle.go.plush similarity index 100% rename from starport/templates/ibc/oracle/static/x/{{moduleName}}/oracle.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/oracle.go.plush diff --git a/starport/templates/ibc/oracle/static/x/{{moduleName}}/types/oracle.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/types/oracle.go.plush similarity index 100% rename from starport/templates/ibc/oracle/static/x/{{moduleName}}/types/oracle.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/types/oracle.go.plush diff --git a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/types/{{queryName}}.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/types/{{queryName}}.go.plush similarity index 95% rename from starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/types/{{queryName}}.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/types/{{queryName}}.go.plush index 2f06cf1ec0..e7cadf0f32 100644 --- a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/types/{{queryName}}.go.plush +++ b/starport/templates/ibc/oracle/x/{{moduleName}}/types/{{queryName}}.go.plush @@ -5,6 +5,8 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) +const TypeMsg<%= queryName.UpperCamel %>Data = "<%= queryName.Snake %>_data" + var ( _ sdk.Msg = &Msg<%= queryName.UpperCamel %>Data{} @@ -27,7 +29,6 @@ func NewMsg<%= queryName.UpperCamel %>Data( askCount uint64, minCount uint64, feeLimit sdk.Coins, - requestKey string, prepareGas uint64, executeGas uint64, ) *Msg<%= queryName.UpperCamel %>Data { @@ -40,7 +41,6 @@ func NewMsg<%= queryName.UpperCamel %>Data( AskCount: askCount, MinCount: minCount, FeeLimit: feeLimit, - RequestKey: requestKey, PrepareGas: prepareGas, ExecuteGas: executeGas, } @@ -53,7 +53,7 @@ func (m *Msg<%= queryName.UpperCamel %>Data) Route() string { // Type returns the message type func (m *Msg<%= queryName.UpperCamel %>Data) Type() string { - return "<%= queryName.UpperCamel %>Data" + return TypeMsg<%= queryName.UpperCamel %>Data } // GetSigners returns the message signers diff --git a/starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/types/{{queryName}}_test.go.plush b/starport/templates/ibc/oracle/x/{{moduleName}}/types/{{queryName}}_test.go.plush similarity index 100% rename from starport/templates/ibc/oracle/dynamic/x/{{moduleName}}/types/{{queryName}}_test.go.plush rename to starport/templates/ibc/oracle/x/{{moduleName}}/types/{{queryName}}_test.go.plush diff --git a/starport/templates/ibc/packet.go b/starport/templates/ibc/packet.go index 999470479c..1c0e7be60b 100644 --- a/starport/templates/ibc/packet.go +++ b/starport/templates/ibc/packet.go @@ -9,11 +9,11 @@ import ( "github.com/gobuffalo/genny" "github.com/gobuffalo/plush" "github.com/gobuffalo/plushgen" - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/multiformatname" "github.com/tendermint/starport/starport/pkg/placeholder" - "github.com/tendermint/starport/starport/pkg/plushhelpers" "github.com/tendermint/starport/starport/pkg/xgenny" + "github.com/tendermint/starport/starport/templates/field" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" "github.com/tendermint/starport/starport/templates/module" "github.com/tendermint/starport/starport/templates/testutil" ) @@ -86,15 +86,16 @@ func NewPacket(replacer placeholder.Replacer, opts *PacketOptions) (*genny.Gener ctx.Set("fields", opts.Fields) ctx.Set("ackFields", opts.AckFields) - // Create the 'testutil' package with the test helpers - if err := testutil.Register(ctx, g, opts.AppPath); err != nil { - return g, err - } - plushhelpers.ExtendPlushContext(ctx) g.Transformer(plushgen.Transformer(ctx)) g.Transformer(genny.Replace("{{moduleName}}", opts.ModuleName)) g.Transformer(genny.Replace("{{packetName}}", opts.PacketName.Snake)) + + // Create the 'testutil' package with the test helpers + if err := testutil.Register(g, opts.AppPath); err != nil { + return g, err + } + return g, nil } @@ -198,19 +199,25 @@ func protoModify(replacer placeholder.Replacer, opts *PacketOptions) genny.RunFn // Add the message definition for packet and acknowledgment var packetFields string for i, field := range opts.Fields { - packetFields += fmt.Sprintf(" %s %s = %d;\n", field.Datatype, field.Name.LowerCamel, i+1) + packetFields += fmt.Sprintf(" %s;\n", field.ProtoType(i+1)) } var ackFields string for i, field := range opts.AckFields { - ackFields += fmt.Sprintf(" %s %s = %d;\n", field.Datatype, field.Name.LowerCamel, i+1) + ackFields += fmt.Sprintf(" %s;\n", field.ProtoType(i+1)) } // Ensure custom types are imported + protoImports := append(opts.Fields.ProtoImports(), opts.AckFields.ProtoImports()...) customFields := append(opts.Fields.Custom(), opts.AckFields.Custom()...) for _, f := range customFields { + protoImports = append(protoImports, + fmt.Sprintf("%[1]v/%[2]v.proto", opts.ModuleName, f), + ) + } + for _, f := range protoImports { importModule := fmt.Sprintf(` -import "%[1]v/%[2]v.proto";`, opts.ModuleName, f) +import "%[1]v";`, f) content = strings.ReplaceAll(content, importModule, "") replacementImport := fmt.Sprintf("%[1]v%[2]v", PlaceholderProtoPacketImport, importModule) @@ -282,13 +289,19 @@ func protoTxModify(replacer placeholder.Replacer, opts *PacketOptions) genny.Run var sendFields string for i, field := range opts.Fields { - sendFields += fmt.Sprintf(" %s %s = %d;\n", field.Datatype, field.Name.LowerCamel, i+5) + sendFields += fmt.Sprintf(" %s;\n", field.ProtoType(i+5)) } // Ensure custom types are imported + protoImports := opts.Fields.ProtoImports() for _, f := range opts.Fields.Custom() { + protoImports = append(protoImports, + fmt.Sprintf("%[1]v/%[2]v.proto", opts.ModuleName, f), + ) + } + for _, f := range protoImports { importModule := fmt.Sprintf(` -import "%[1]v/%[2]v.proto";`, opts.ModuleName, f) +import "%[1]v";`, f) content = strings.ReplaceAll(content, importModule, "") replacementImport := fmt.Sprintf("%[1]v%[2]v", PlaceholderProtoTxImport, importModule) diff --git a/starport/templates/ibc/packet/component/x/{{moduleName}}/keeper/{{packetName}}.go.plush b/starport/templates/ibc/packet/component/x/{{moduleName}}/keeper/{{packetName}}.go.plush index 809b165aa6..3fd84166ab 100644 --- a/starport/templates/ibc/packet/component/x/{{moduleName}}/keeper/{{packetName}}.go.plush +++ b/starport/templates/ibc/packet/component/x/{{moduleName}}/keeper/{{packetName}}.go.plush @@ -21,7 +21,7 @@ func (k Keeper) Transmit<%= packetName.UpperCamel %>Packet( timeoutTimestamp uint64, ) error { - sourceChannelEnd, found := k.channelKeeper.GetChannel(ctx, sourcePort, sourceChannel) + sourceChannelEnd, found := k.ChannelKeeper.GetChannel(ctx, sourcePort, sourceChannel) if !found { return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "port ID (%s) channel ID (%s)", sourcePort, sourceChannel) } @@ -30,7 +30,7 @@ func (k Keeper) Transmit<%= packetName.UpperCamel %>Packet( destinationChannel := sourceChannelEnd.GetCounterparty().GetChannelID() // get the next sequence - sequence, found := k.channelKeeper.GetNextSequenceSend(ctx, sourcePort, sourceChannel) + sequence, found := k.ChannelKeeper.GetNextSequenceSend(ctx, sourcePort, sourceChannel) if !found { return sdkerrors.Wrapf( channeltypes.ErrSequenceSendNotFound, @@ -38,7 +38,7 @@ func (k Keeper) Transmit<%= packetName.UpperCamel %>Packet( ) } - channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, sourceChannel)) + channelCap, ok := k.ScopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(sourcePort, sourceChannel)) if !ok { return sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability") } @@ -59,7 +59,7 @@ func (k Keeper) Transmit<%= packetName.UpperCamel %>Packet( timeoutTimestamp, ) - if err := k.channelKeeper.SendPacket(ctx, channelCap, packet); err != nil { + if err := k.ChannelKeeper.SendPacket(ctx, channelCap, packet); err != nil { return err } diff --git a/starport/templates/ibc/packet/messages/x/{{moduleName}}/client/cli/tx_{{packetName}}.go.plush b/starport/templates/ibc/packet/messages/x/{{moduleName}}/client/cli/tx_{{packetName}}.go.plush index 4c2b9d5b97..59181c3ef4 100644 --- a/starport/templates/ibc/packet/messages/x/{{moduleName}}/client/cli/tx_{{packetName}}.go.plush +++ b/starport/templates/ibc/packet/messages/x/{{moduleName}}/client/cli/tx_{{packetName}}.go.plush @@ -2,11 +2,9 @@ package cli import ( "strconv" - <%= if (fields.IsComplex()) { %> "encoding/json" <% } %> - - <%= if (fields.NeedCastImport()) { %> "github.com/spf13/cast" <% } %> + <%= for (goImport) in mergeGoImports(fields) { %> + <%= goImport.Alias %> "<%= goImport.Name %>"<% } %> "github.com/spf13/cobra" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -31,7 +29,7 @@ func CmdSend<%= packetName.UpperCamel %>() *cobra.Command { srcPort := args[0] srcChannel := args[1] - <%= for (i, field) in fields { %> <%= castArg("arg", field, i+2) %> + <%= for (i, field) in fields { %> <%= field.CLIArgs("arg", i+2) %> <% } %> // Get the relative timeout timestamp diff --git a/starport/templates/ibc/packet/messages/x/{{moduleName}}/types/messages_{{packetName}}.go.plush b/starport/templates/ibc/packet/messages/x/{{moduleName}}/types/messages_{{packetName}}.go.plush index bca3d10ca5..d101f3d288 100644 --- a/starport/templates/ibc/packet/messages/x/{{moduleName}}/types/messages_{{packetName}}.go.plush +++ b/starport/templates/ibc/packet/messages/x/{{moduleName}}/types/messages_{{packetName}}.go.plush @@ -5,6 +5,8 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) +const TypeMsgSend<%= packetName.UpperCamel %> = "send_<%= packetName.Snake %>" + var _ sdk.Msg = &MsgSend<%= packetName.UpperCamel %>{} func NewMsgSend<%= packetName.UpperCamel %>( @@ -12,7 +14,7 @@ func NewMsgSend<%= packetName.UpperCamel %>( port string, channelID string, timeoutTimestamp uint64,<%= for (field) in fields { %> - <%= field.Name.LowerCamel %> <%= field.GetDatatype() %>,<% } %> + <%= field.Name.LowerCamel %> <%= field.DataType() %>,<% } %> ) *MsgSend<%= packetName.UpperCamel %> { return &MsgSend<%= packetName.UpperCamel %>{ <%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>, @@ -28,7 +30,7 @@ func (msg *MsgSend<%= packetName.UpperCamel %>) Route() string { } func (msg *MsgSend<%= packetName.UpperCamel %>) Type() string { - return "Send<%= packetName.UpperCamel %>" + return TypeMsgSend<%= packetName.UpperCamel %> } func (msg *MsgSend<%= packetName.UpperCamel %>) GetSigners() []sdk.AccAddress { diff --git a/starport/templates/message/message.go b/starport/templates/message/message.go index ca00c9823c..48daf0423e 100644 --- a/starport/templates/message/message.go +++ b/starport/templates/message/message.go @@ -7,7 +7,7 @@ import ( "github.com/gobuffalo/packd" "github.com/gobuffalo/plush" "github.com/gobuffalo/plushgen" - "github.com/tendermint/starport/starport/pkg/plushhelpers" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" "github.com/tendermint/starport/starport/templates/testutil" ) @@ -31,14 +31,15 @@ func Box(box packd.Walker, opts *Options, g *genny.Generator) error { ctx.Set("Fields", opts.Fields) ctx.Set("ResFields", opts.ResFields) - // Create the 'testutil' package with the test helpers - if err := testutil.Register(ctx, g, opts.AppPath); err != nil { - return err - } - plushhelpers.ExtendPlushContext(ctx) g.Transformer(plushgen.Transformer(ctx)) g.Transformer(genny.Replace("{{moduleName}}", opts.ModuleName)) g.Transformer(genny.Replace("{{msgName}}", opts.MsgName.Snake)) + + // Create the 'testutil' package with the test helpers + if err := testutil.Register(g, opts.AppPath); err != nil { + return err + } + return nil } diff --git a/starport/templates/message/options.go b/starport/templates/message/options.go index acd7888784..c5328ac289 100644 --- a/starport/templates/message/options.go +++ b/starport/templates/message/options.go @@ -1,8 +1,8 @@ package message import ( - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/multiformatname" + "github.com/tendermint/starport/starport/templates/field" ) // Options ... diff --git a/starport/templates/message/stargate.go b/starport/templates/message/stargate.go index 1348da7209..a3120acd93 100644 --- a/starport/templates/message/stargate.go +++ b/starport/templates/message/stargate.go @@ -20,6 +20,7 @@ func NewStargate(replacer placeholder.Replacer, opts *Options) (*genny.Generator g.RunFn(protoTxMessageModify(replacer, opts)) g.RunFn(typesCodecModify(replacer, opts)) g.RunFn(clientCliTxModify(replacer, opts)) + g.RunFn(moduleSimulationModify(replacer, opts)) template := xgenny.NewEmbedWalker( fsStargate, @@ -83,11 +84,11 @@ func protoTxMessageModify(replacer placeholder.Replacer, opts *Options) genny.Ru var msgFields string for i, field := range opts.Fields { - msgFields += fmt.Sprintf(" %s %s = %d;\n", field.Datatype, field.Name.LowerCamel, i+2) + msgFields += fmt.Sprintf(" %s;\n", field.ProtoType(i+2)) } var resFields string for i, field := range opts.ResFields { - resFields += fmt.Sprintf(" %s %s = %d;\n", field.Datatype, field.Name.LowerCamel, i+1) + resFields += fmt.Sprintf(" %s;\n", field.ProtoType(i+1)) } template := `message Msg%[2]v { @@ -108,10 +109,16 @@ message Msg%[2]vResponse { content := replacer.Replace(f.String(), PlaceholderProtoTxMessage, replacement) // Ensure custom types are imported + protoImports := append(opts.ResFields.ProtoImports(), opts.Fields.ProtoImports()...) customFields := append(opts.ResFields.Custom(), opts.Fields.Custom()...) for _, f := range customFields { + protoImports = append(protoImports, + fmt.Sprintf("%[1]v/%[2]v.proto", opts.ModuleName, f), + ) + } + for _, f := range protoImports { importModule := fmt.Sprintf(` -import "%[1]v/%[2]v.proto";`, opts.ModuleName, f) +import "%[1]v";`, f) content = strings.ReplaceAll(content, importModule, "") replacementImport := fmt.Sprintf("%[1]v%[2]v", typed.PlaceholderProtoTxImport, importModule) @@ -174,3 +181,23 @@ func clientCliTxModify(replacer placeholder.Replacer, opts *Options) genny.RunFn return r.File(newFile) } } + +func moduleSimulationModify(replacer placeholder.Replacer, opts *Options) genny.RunFn { + return func(r *genny.Runner) error { + path := filepath.Join(opts.AppPath, "x", opts.ModuleName, "module_simulation.go") + f, err := r.Disk.Find(path) + if err != nil { + return err + } + + content := typed.ModuleSimulationMsgModify( + replacer, + f.String(), + opts.ModuleName, + opts.MsgName, + ) + + newFile := genny.NewFileS(path, content) + return r.File(newFile) + } +} diff --git a/starport/templates/message/stargate/x/{{moduleName}}/client/cli/tx_{{msgName}}.go.plush b/starport/templates/message/stargate/x/{{moduleName}}/client/cli/tx_{{msgName}}.go.plush index c50bb0bf0c..304dfee081 100644 --- a/starport/templates/message/stargate/x/{{moduleName}}/client/cli/tx_{{msgName}}.go.plush +++ b/starport/templates/message/stargate/x/{{moduleName}}/client/cli/tx_{{msgName}}.go.plush @@ -2,11 +2,9 @@ package cli import ( "strconv" - <%= if (Fields.IsComplex()) { %> "encoding/json" <% } %> - - <%= if (Fields.NeedCastImport()) { %> "github.com/spf13/cast" <% } %> + <%= for (goImport) in mergeGoImports(Fields) { %> + <%= goImport.Alias %> "<%= goImport.Name %>"<% } %> "github.com/spf13/cobra" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -21,7 +19,7 @@ func Cmd<%= MsgName.UpperCamel %>() *cobra.Command { Short: "<%= MsgDesc %>", Args: cobra.ExactArgs(<%= len(Fields) %>), RunE: func(cmd *cobra.Command, args []string) (err error) { - <%= for (i, field) in Fields { %> <%= castArg("arg", field, i) %> + <%= for (i, field) in Fields { %> <%= field.CLIArgs("arg", i) %> <% } %> clientCtx, err := client.GetClientTxContext(cmd) if err != nil { diff --git a/starport/templates/message/stargate/x/{{moduleName}}/simulation/{{msgName}}.go.plush b/starport/templates/message/stargate/x/{{moduleName}}/simulation/{{msgName}}.go.plush new file mode 100644 index 0000000000..6a59e12888 --- /dev/null +++ b/starport/templates/message/stargate/x/{{moduleName}}/simulation/{{msgName}}.go.plush @@ -0,0 +1,29 @@ +package simulation + +import ( + "math/rand" + + "<%= ModulePath %>/x/<%= ModuleName %>/keeper" + "<%= ModulePath %>/x/<%= ModuleName %>/types" + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" +) + +func SimulateMsg<%= MsgName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + msg := &types.Msg<%= MsgName.UpperCamel %>{ + <%= MsgSigner.UpperCamel %>: simAccount.Address.String(), + } + + // TODO: Handling the <%= MsgName.UpperCamel %> simulation + + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= MsgName.UpperCamel %> simulation not implemented"), nil, nil + } +} diff --git a/starport/templates/message/stargate/x/{{moduleName}}/types/message_{{msgName}}.go.plush b/starport/templates/message/stargate/x/{{moduleName}}/types/message_{{msgName}}.go.plush index c4d02424a8..2a1919596d 100644 --- a/starport/templates/message/stargate/x/{{moduleName}}/types/message_{{msgName}}.go.plush +++ b/starport/templates/message/stargate/x/{{moduleName}}/types/message_{{msgName}}.go.plush @@ -5,9 +5,11 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) +const TypeMsg<%= MsgName.UpperCamel %> = "<%= MsgName.Snake %>" + var _ sdk.Msg = &Msg<%= MsgName.UpperCamel %>{} -func NewMsg<%= MsgName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.GetDatatype() %><% } %>) *Msg<%= MsgName.UpperCamel %> { +func NewMsg<%= MsgName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.DataType() %><% } %>) *Msg<%= MsgName.UpperCamel %> { return &Msg<%= MsgName.UpperCamel %>{ <%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>,<%= for (field) in Fields { %> <%= field.Name.UpperCamel %>: <%= field.Name.LowerCamel %>,<% } %> @@ -19,7 +21,7 @@ func (msg *Msg<%= MsgName.UpperCamel %>) Route() string { } func (msg *Msg<%= MsgName.UpperCamel %>) Type() string { - return "<%= MsgName.UpperCamel %>" + return TypeMsg<%= MsgName.UpperCamel %> } func (msg *Msg<%= MsgName.UpperCamel %>) GetSigners() []sdk.AccAddress { diff --git a/starport/templates/module/create/genesistest.go b/starport/templates/module/create/genesistest.go index ef8951828f..820023f48f 100644 --- a/starport/templates/module/create/genesistest.go +++ b/starport/templates/module/create/genesistest.go @@ -1,43 +1,36 @@ package modulecreate import ( + "strings" + "github.com/gobuffalo/genny" "github.com/gobuffalo/plush" "github.com/gobuffalo/plushgen" - "github.com/tendermint/starport/starport/pkg/plushhelpers" "github.com/tendermint/starport/starport/pkg/xgenny" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" ) -// genesisTestCtx returns the generator to generate genesis_test.go -func genesisTestCtx(appName, modulePath, moduleName string) *genny.Generator { - g := genny.New() +// AddGenesisTest returns the generator to generate genesis_test.go files +func AddGenesisTest(appPath, appName, modulePath, moduleName string, isIBC bool) (*genny.Generator, error) { + var ( + g = genny.New() + template = xgenny.NewEmbedWalker(fsGenesisTest, "genesistest/", appPath) + ) + ctx := plush.NewContext() ctx.Set("moduleName", moduleName) ctx.Set("modulePath", modulePath) ctx.Set("appName", appName) + ctx.Set("isIBC", isIBC) + ctx.Set("title", strings.Title) plushhelpers.ExtendPlushContext(ctx) g.Transformer(plushgen.Transformer(ctx)) g.Transformer(genny.Replace("{{moduleName}}", moduleName)) - return g -} -// AddGenesisModuleTest returns the generator to generate genesis_test.go -func AddGenesisModuleTest(appPath, appName, modulePath, moduleName string) (*genny.Generator, error) { - g := genesisTestCtx(appName, modulePath, moduleName) - return g, g.Box(xgenny.NewEmbedWalker( - fsGenesisModuleTest, - "genesistest/module/", - appPath, - )) -} + if err := xgenny.Box(g, template); err != nil { + return nil, err + } -// AddGenesisTypesTest returns the generator to generate types/genesis_test.go -func AddGenesisTypesTest(appPath, appName, modulePath, moduleName string) (*genny.Generator, error) { - g := genesisTestCtx(appName, modulePath, moduleName) - return g, g.Box(xgenny.NewEmbedWalker( - fsGenesisTypesTest, - "genesistest/types/", - appPath, - )) + return g, nil } diff --git a/starport/templates/module/create/genesistest/module/x/{{moduleName}}/genesis_test.go.plush b/starport/templates/module/create/genesistest/x/{{moduleName}}/genesis_test.go.plush similarity index 71% rename from starport/templates/module/create/genesistest/module/x/{{moduleName}}/genesis_test.go.plush rename to starport/templates/module/create/genesistest/x/{{moduleName}}/genesis_test.go.plush index 9f0c4ebe87..74ca73c7f9 100644 --- a/starport/templates/module/create/genesistest/module/x/{{moduleName}}/genesis_test.go.plush +++ b/starport/templates/module/create/genesistest/x/{{moduleName}}/genesis_test.go.plush @@ -4,6 +4,7 @@ import ( "testing" keepertest "<%= modulePath %>/testutil/keeper" + "<%= modulePath %>/testutil/nullify" "<%= modulePath %>/x/<%= moduleName %>" "<%= modulePath %>/x/<%= moduleName %>/types" "github.com/stretchr/testify/require" @@ -11,6 +12,8 @@ import ( func TestGenesis(t *testing.T) { genesisState := types.GenesisState{ + Params: types.DefaultParams(), + <%= if (isIBC) { %>PortId: types.PortID,<% } %> // this line is used by starport scaffolding # genesis/test/state } @@ -19,5 +22,10 @@ func TestGenesis(t *testing.T) { got := <%= moduleName %>.ExportGenesis(ctx, *k) require.NotNil(t, got) + nullify.Fill(&genesisState) + nullify.Fill(got) + + <%= if (isIBC) { %>require.Equal(t, genesisState.PortId, got.PortId)<% } %> + // this line is used by starport scaffolding # genesis/test/assert } diff --git a/starport/templates/module/create/genesistest/types/x/{{moduleName}}/types/genesis_test.go.plush b/starport/templates/module/create/genesistest/x/{{moduleName}}/types/genesis_test.go.plush similarity index 94% rename from starport/templates/module/create/genesistest/types/x/{{moduleName}}/types/genesis_test.go.plush rename to starport/templates/module/create/genesistest/x/{{moduleName}}/types/genesis_test.go.plush index ddb0950c38..b0b19c0b00 100644 --- a/starport/templates/module/create/genesistest/types/x/{{moduleName}}/types/genesis_test.go.plush +++ b/starport/templates/module/create/genesistest/x/{{moduleName}}/types/genesis_test.go.plush @@ -21,6 +21,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "valid genesis state", genState: &types.GenesisState{ + <%= if (isIBC) { %>PortId: types.PortID,<% } %> // this line is used by starport scaffolding # types/genesis/validField }, valid: true, diff --git a/starport/templates/module/create/ibc.go b/starport/templates/module/create/ibc.go index 1bf8fd4832..35d7b69e4c 100644 --- a/starport/templates/module/create/ibc.go +++ b/starport/templates/module/create/ibc.go @@ -9,9 +9,9 @@ import ( "github.com/gobuffalo/plush" "github.com/gobuffalo/plushgen" "github.com/tendermint/starport/starport/pkg/placeholder" - "github.com/tendermint/starport/starport/pkg/plushhelpers" "github.com/tendermint/starport/starport/pkg/xgenny" "github.com/tendermint/starport/starport/pkg/xstrings" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" "github.com/tendermint/starport/starport/templates/module" "github.com/tendermint/starport/starport/templates/typed" ) @@ -26,8 +26,6 @@ func NewIBC(replacer placeholder.Replacer, opts *CreateOptions) (*genny.Generato g.RunFn(genesisModify(replacer, opts)) g.RunFn(genesisTypesModify(replacer, opts)) g.RunFn(genesisProtoModify(replacer, opts)) - g.RunFn(genesisTestsModify(replacer, opts)) - g.RunFn(genesisTypesTestsModify(replacer, opts)) g.RunFn(keysModify(replacer, opts)) if err := g.Box(template); err != nil { @@ -130,7 +128,7 @@ func genesisProtoModify(replacer placeholder.Replacer, opts *CreateOptions) genn // Determine the new field number content := f.String() - template := `string port_id = 1; + template := `string port_id = 2; %s` replacement := fmt.Sprintf(template, typed.PlaceholderGenesisProtoState) content = replacer.Replace(content, typed.PlaceholderGenesisProtoState, replacement) @@ -140,41 +138,6 @@ func genesisProtoModify(replacer placeholder.Replacer, opts *CreateOptions) genn } } -func genesisTestsModify(replacer placeholder.Replacer, opts *CreateOptions) genny.RunFn { - return func(r *genny.Runner) error { - path := filepath.Join(opts.AppPath, "x", opts.ModuleName, "genesis_test.go") - f, err := r.Disk.Find(path) - if err != nil { - return err - } - - replacementState := fmt.Sprintf("PortId: types.PortID,\n%s", module.PlaceholderGenesisTestState) - content := replacer.Replace(f.String(), module.PlaceholderGenesisTestState, replacementState) - - replacementAssert := fmt.Sprintf("require.Equal(t, genesisState.PortId, got.PortId)\n%s", module.PlaceholderGenesisTestAssert) - content = replacer.Replace(content, module.PlaceholderGenesisTestAssert, replacementAssert) - - newFile := genny.NewFileS(path, content) - return r.File(newFile) - } -} - -func genesisTypesTestsModify(replacer placeholder.Replacer, opts *CreateOptions) genny.RunFn { - return func(r *genny.Runner) error { - path := filepath.Join(opts.AppPath, "x", opts.ModuleName, "types/genesis_test.go") - f, err := r.Disk.Find(path) - if err != nil { - return err - } - - replacement := fmt.Sprintf("PortId: types.PortID,\n%s", module.PlaceholderTypesGenesisValidField) - content := replacer.Replace(f.String(), module.PlaceholderTypesGenesisValidField, replacement) - - newFile := genny.NewFileS(path, content) - return r.File(newFile) - } -} - func keysModify(replacer placeholder.Replacer, opts *CreateOptions) genny.RunFn { return func(r *genny.Runner) error { path := filepath.Join(opts.AppPath, "x", opts.ModuleName, "types/keys.go") diff --git a/starport/templates/module/create/ibc/testutil/keeper/{{moduleName}}.go.plush b/starport/templates/module/create/ibc/testutil/keeper/{{moduleName}}.go.plush index 27fe7fdc00..8a18cfbb4d 100644 --- a/starport/templates/module/create/ibc/testutil/keeper/{{moduleName}}.go.plush +++ b/starport/templates/module/create/ibc/testutil/keeper/{{moduleName}}.go.plush @@ -35,9 +35,8 @@ func <%= title(moduleName) %>Keeper(t testing.TB) (*keeper.Keeper, sdk.Context) appCodec := codec.NewProtoCodec(registry) capabilityKeeper := capabilitykeeper.NewKeeper(appCodec, storeKey, memStoreKey) - amino := codec.NewLegacyAmino() ss := typesparams.NewSubspace(appCodec, - amino, + types.Amino, storeKey, memStoreKey, "<%= title(moduleName) %>SubSpace", @@ -51,10 +50,17 @@ func <%= title(moduleName) %>Keeper(t testing.TB) (*keeper.Keeper, sdk.Context) capabilityKeeper.ScopeToModule("<%= title(moduleName) %>IBCKeeper"), ) + paramsSubspace := typesparams.NewSubspace(appCodec, + types.Amino, + storeKey, + memStoreKey, + "<%= title(moduleName) %>Params", + ) k := keeper.NewKeeper( - codec.NewProtoCodec(registry), + appCodec, storeKey, memStoreKey, + paramsSubspace, IBCKeeper.ChannelKeeper, &IBCKeeper.PortKeeper, capabilityKeeper.ScopeToModule("<%= title(moduleName) %>ScopedKeeper"),<%= for (dependency) in dependencies { %> @@ -62,5 +68,9 @@ func <%= title(moduleName) %>Keeper(t testing.TB) (*keeper.Keeper, sdk.Context) ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, logger) + + // Initialize params + k.SetParams(ctx, types.DefaultParams()) + return k, ctx } diff --git a/starport/templates/module/create/ibc/x/{{moduleName}}/keeper/ibc.go.plush b/starport/templates/module/create/ibc/x/{{moduleName}}/keeper/ibc.go.plush deleted file mode 100644 index 457eca7182..0000000000 --- a/starport/templates/module/create/ibc/x/{{moduleName}}/keeper/ibc.go.plush +++ /dev/null @@ -1,55 +0,0 @@ -package keeper - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" - "<%= modulePath %>/x/<%= moduleName %>/types" - channeltypes "github.com/cosmos/ibc-go/modules/core/04-channel/types" - host "github.com/cosmos/ibc-go/modules/core/24-host" -) - -// ChanCloseInit defines a wrapper function for the channel Keeper's function -func (k Keeper) ChanCloseInit(ctx sdk.Context, portID, channelID string) error { - capName := host.ChannelCapabilityPath(portID, channelID) - chanCap, ok := k.scopedKeeper.GetCapability(ctx, capName) - if !ok { - return sdkerrors.Wrapf(channeltypes.ErrChannelCapabilityNotFound, "could not retrieve channel capability at: %s", capName) - } - return k.channelKeeper.ChanCloseInit(ctx, portID, channelID, chanCap) -} - -// IsBound checks if the module is already bound to the desired port -func (k Keeper) IsBound(ctx sdk.Context, portID string) bool { - _, ok := k.scopedKeeper.GetCapability(ctx, host.PortPath(portID)) - return ok -} - -// BindPort defines a wrapper function for the ort Keeper's function in -// order to expose it to module's InitGenesis function -func (k Keeper) BindPort(ctx sdk.Context, portID string) error { - cap := k.portKeeper.BindPort(ctx, portID) - return k.ClaimCapability(ctx, cap, host.PortPath(portID)) -} - -// GetPort returns the portID for the module. Used in ExportGenesis -func (k Keeper) GetPort(ctx sdk.Context) string { - store := ctx.KVStore(k.storeKey) - return string(store.Get(types.PortKey)) -} - -// SetPort sets the portID for the module. Used in InitGenesis -func (k Keeper) SetPort(ctx sdk.Context, portID string) { - store := ctx.KVStore(k.storeKey) - store.Set(types.PortKey, []byte(portID)) -} - -// AuthenticateCapability wraps the scopedKeeper's AuthenticateCapability function -func (k Keeper) AuthenticateCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) bool { - return k.scopedKeeper.AuthenticateCapability(ctx, cap, name) -} - -// ClaimCapability allows the module that can claim a capability that IBC module passes to it -func (k Keeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error { - return k.scopedKeeper.ClaimCapability(ctx, cap, name) -} \ No newline at end of file diff --git a/starport/templates/module/create/ibc/x/{{moduleName}}/types/expected_keeper_ibc.go.plush b/starport/templates/module/create/ibc/x/{{moduleName}}/types/expected_keeper_ibc.go.plush deleted file mode 100644 index 223f840c9a..0000000000 --- a/starport/templates/module/create/ibc/x/{{moduleName}}/types/expected_keeper_ibc.go.plush +++ /dev/null @@ -1,28 +0,0 @@ -package types - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" - channeltypes "github.com/cosmos/ibc-go/modules/core/04-channel/types" - ibcexported "github.com/cosmos/ibc-go/modules/core/exported" -) - -// ChannelKeeper defines the expected IBC channel keeper -type ChannelKeeper interface { - GetChannel(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) - GetNextSequenceSend(ctx sdk.Context, portID, channelID string) (uint64, bool) - SendPacket(ctx sdk.Context, channelCap *capabilitytypes.Capability, packet ibcexported.PacketI) error - ChanCloseInit(ctx sdk.Context, portID, channelID string, chanCap *capabilitytypes.Capability) error -} - -// PortKeeper defines the expected IBC port keeper -type PortKeeper interface { - BindPort(ctx sdk.Context, portID string) *capabilitytypes.Capability -} - -// ScopedKeeper defines the expected IBC scoped keeper -type ScopedKeeper interface { - GetCapability(ctx sdk.Context, name string) (*capabilitytypes.Capability, bool) - AuthenticateCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) bool - ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capability, name string) error -} diff --git a/starport/templates/module/create/msgserver.go b/starport/templates/module/create/msgserver.go index df2af487a2..9d9f9afe05 100644 --- a/starport/templates/module/create/msgserver.go +++ b/starport/templates/module/create/msgserver.go @@ -8,9 +8,9 @@ import ( "github.com/gobuffalo/plush" "github.com/gobuffalo/plushgen" "github.com/tendermint/starport/starport/pkg/placeholder" - "github.com/tendermint/starport/starport/pkg/plushhelpers" "github.com/tendermint/starport/starport/pkg/xgenny" "github.com/tendermint/starport/starport/pkg/xstrings" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" "github.com/tendermint/starport/starport/templates/module" "github.com/tendermint/starport/starport/templates/typed" ) diff --git a/starport/templates/module/create/options.go b/starport/templates/module/create/options.go index d9ddd9d693..4c568f37db 100644 --- a/starport/templates/module/create/options.go +++ b/starport/templates/module/create/options.go @@ -3,6 +3,8 @@ package modulecreate import ( "fmt" "strings" + + "github.com/tendermint/starport/starport/templates/field" ) // CreateOptions represents the options to scaffold a Cosmos SDK module @@ -12,6 +14,7 @@ type CreateOptions struct { AppName string AppPath string OwnerName string + Params field.Fields // True if the module should implement the IBC module interface IsIBC bool diff --git a/starport/templates/module/create/simapp/x/{{moduleName}}/module_simulation.go.plush b/starport/templates/module/create/simapp/x/{{moduleName}}/module_simulation.go.plush new file mode 100644 index 0000000000..64ad604fb0 --- /dev/null +++ b/starport/templates/module/create/simapp/x/{{moduleName}}/module_simulation.go.plush @@ -0,0 +1,67 @@ +package <%= moduleName %> + +import ( + "math/rand" + + "<%= modulePath %>/testutil/sample" + <%= moduleName %>simulation "<%= modulePath %>/x/<%= moduleName %>/simulation" + "<%= modulePath %>/x/<%= moduleName %>/types" + "github.com/cosmos/cosmos-sdk/baseapp" + simappparams "github.com/cosmos/cosmos-sdk/simapp/params" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +// avoid unused import issue +var ( + _ = sample.AccAddress + _ = <%= moduleName %>simulation.FindAccount + _ = simappparams.StakePerAccount + _ = simulation.MsgEntryKind + _ = baseapp.Paramspace +) + +const ( + // this line is used by starport scaffolding # simapp/module/const +) + +// GenerateGenesisState creates a randomized GenState of the module +func (AppModule) GenerateGenesisState(simState *module.SimulationState) { + accs := make([]string, len(simState.Accounts)) + for i, acc := range simState.Accounts { + accs[i] = acc.Address.String() + } + <%= moduleName %>Genesis := types.GenesisState{ + // this line is used by starport scaffolding # simapp/module/genesisState + } + simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&<%= moduleName %>Genesis) +} + +// ProposalContents doesn't return any content functions for governance proposals +func (AppModule) ProposalContents(_ module.SimulationState) []simtypes.WeightedProposalContent { + return nil +} + +// RandomizedParams creates randomized param changes for the simulator +func (am AppModule) RandomizedParams(_ *rand.Rand) []simtypes.ParamChange { + <%= if (len(params) > 0) { %><%= moduleName %>Params := types.DefaultParams()<% } %> + return []simtypes.ParamChange{<%= for (param) in params { %> + simulation.NewSimParamChange(types.ModuleName, string(types.Key<%= param.Name.UpperCamel %>), func(r *rand.Rand) string { + return string(types.Amino.MustMarshalJSON(<%= moduleName %>Params.<%= param.Name.UpperCamel %>)) + }),<% } %> + } +} + +// RegisterStoreDecoder registers a decoder +func (am AppModule) RegisterStoreDecoder(_ sdk.StoreDecoderRegistry) {} + +// WeightedOperations returns the all the gov module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + operations := make([]simtypes.WeightedOperation, 0) + + // this line is used by starport scaffolding # simapp/module/operation + + return operations +} diff --git a/starport/templates/module/create/simapp/x/{{moduleName}}/simulation/simap.go.plush b/starport/templates/module/create/simapp/x/{{moduleName}}/simulation/simap.go.plush new file mode 100644 index 0000000000..92c437c0d1 --- /dev/null +++ b/starport/templates/module/create/simapp/x/{{moduleName}}/simulation/simap.go.plush @@ -0,0 +1,15 @@ +package simulation + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" +) + +// FindAccount find a specific address from an account list +func FindAccount(accs []simtypes.Account, address string) (simtypes.Account, bool) { + creator, err := sdk.AccAddressFromBech32(address) + if err != nil { + panic(err) + } + return simtypes.FindAccount(accs, creator) +} diff --git a/starport/templates/module/create/simulation.go b/starport/templates/module/create/simulation.go new file mode 100644 index 0000000000..be1465484b --- /dev/null +++ b/starport/templates/module/create/simulation.go @@ -0,0 +1,33 @@ +package modulecreate + +import ( + "github.com/gobuffalo/genny" + "github.com/gobuffalo/plush" + "github.com/gobuffalo/plushgen" + "github.com/tendermint/starport/starport/pkg/xgenny" + "github.com/tendermint/starport/starport/templates/field" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" +) + +// AddSimulation returns the generator to generate module_simulation.go file +func AddSimulation(appPath, modulePath, moduleName string, params ...field.Field) (*genny.Generator, error) { + var ( + g = genny.New() + template = xgenny.NewEmbedWalker(fsSimapp, "simapp/", appPath) + ) + + ctx := plush.NewContext() + ctx.Set("moduleName", moduleName) + ctx.Set("modulePath", modulePath) + ctx.Set("params", params) + + plushhelpers.ExtendPlushContext(ctx) + g.Transformer(genny.Replace("{{moduleName}}", moduleName)) + + if err := xgenny.Box(g, template); err != nil { + return nil, err + } + + g.Transformer(plushgen.Transformer(ctx)) + return g, nil +} diff --git a/starport/templates/module/create/stargate.go b/starport/templates/module/create/stargate.go index ad12746a51..ee0b72489c 100644 --- a/starport/templates/module/create/stargate.go +++ b/starport/templates/module/create/stargate.go @@ -9,9 +9,9 @@ import ( "github.com/gobuffalo/plush" "github.com/gobuffalo/plushgen" "github.com/tendermint/starport/starport/pkg/placeholder" - "github.com/tendermint/starport/starport/pkg/plushhelpers" "github.com/tendermint/starport/starport/pkg/xgenny" "github.com/tendermint/starport/starport/pkg/xstrings" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" "github.com/tendermint/starport/starport/templates/module" ) @@ -25,14 +25,9 @@ func NewStargate(opts *CreateOptions) (*genny.Generator, error) { "msgserver/", opts.AppPath, ) - genesisModuleTestTemplate = xgenny.NewEmbedWalker( - fsGenesisModuleTest, - "genesistest/module/", - opts.AppPath, - ) - genesisTypesTestTemplate = xgenny.NewEmbedWalker( - fsGenesisTypesTest, - "genesistest/types/", + genesisTestTemplate = xgenny.NewEmbedWalker( + fsGenesisTest, + "genesistest/", opts.AppPath, ) stargateTemplate = xgenny.NewEmbedWalker( @@ -41,13 +36,11 @@ func NewStargate(opts *CreateOptions) (*genny.Generator, error) { opts.AppPath, ) ) + if err := g.Box(msgServerTemplate); err != nil { return g, err } - if err := g.Box(genesisModuleTestTemplate); err != nil { - return g, err - } - if err := g.Box(genesisTypesTestTemplate); err != nil { + if err := g.Box(genesisTestTemplate); err != nil { return g, err } if err := g.Box(stargateTemplate); err != nil { @@ -59,6 +52,7 @@ func NewStargate(opts *CreateOptions) (*genny.Generator, error) { ctx.Set("appName", opts.AppName) ctx.Set("ownerName", opts.OwnerName) ctx.Set("dependencies", opts.Dependencies) + ctx.Set("params", opts.Params) ctx.Set("isIBC", opts.IsIBC) // Used for proto package name @@ -67,6 +61,13 @@ func NewStargate(opts *CreateOptions) (*genny.Generator, error) { plushhelpers.ExtendPlushContext(ctx) g.Transformer(plushgen.Transformer(ctx)) g.Transformer(genny.Replace("{{moduleName}}", opts.ModuleName)) + + gSimapp, err := AddSimulation(opts.AppPath, opts.ModulePath, opts.ModuleName, opts.Params...) + if err != nil { + return g, err + } + g.Merge(gSimapp) + return g, nil } @@ -161,9 +162,10 @@ func appModifyStargate(replacer placeholder.Replacer, opts *CreateOptions) genny appCodec, keys[%[2]vmoduletypes.StoreKey], keys[%[2]vmoduletypes.MemStoreKey], + app.GetSubspace(%[2]vmoduletypes.ModuleName), %[4]v %[6]v) - %[2]vModule := %[2]vmodule.NewAppModule(appCodec, app.%[5]vKeeper) + %[2]vModule := %[2]vmodule.NewAppModule(appCodec, app.%[5]vKeeper, app.AccountKeeper, app.BankKeeper) %[1]v` replacement = fmt.Sprintf( @@ -181,7 +183,7 @@ func appModifyStargate(replacer placeholder.Replacer, opts *CreateOptions) genny template = `%[2]vModule, %[1]v` replacement = fmt.Sprintf(template, module.PlaceholderSgAppAppModule, opts.ModuleName) - content = replacer.Replace(content, module.PlaceholderSgAppAppModule, replacement) + content = replacer.ReplaceAll(content, module.PlaceholderSgAppAppModule, replacement) // Init genesis template = `%[2]vmoduletypes.ModuleName, diff --git a/starport/templates/module/create/stargate/proto/{{moduleName}}/genesis.proto.plush b/starport/templates/module/create/stargate/proto/{{moduleName}}/genesis.proto.plush index f942a69c57..3f7e69e177 100644 --- a/starport/templates/module/create/stargate/proto/{{moduleName}}/genesis.proto.plush +++ b/starport/templates/module/create/stargate/proto/{{moduleName}}/genesis.proto.plush @@ -1,11 +1,14 @@ syntax = "proto3"; package <%= formatOwnerName(ownerName) %>.<%= appName %>.<%= moduleName %>; +import "gogoproto/gogo.proto"; +import "<%= moduleName %>/params.proto"; // this line is used by starport scaffolding # genesis/proto/import option go_package = "<%= modulePath %>/x/<%= moduleName %>/types"; // GenesisState defines the <%= moduleName %> module's genesis state. message GenesisState { - // this line is used by starport scaffolding # genesis/proto/state + Params params = 1 [(gogoproto.nullable) = false]; + // this line is used by starport scaffolding # genesis/proto/state } diff --git a/starport/templates/module/create/stargate/proto/{{moduleName}}/params.proto.plush b/starport/templates/module/create/stargate/proto/{{moduleName}}/params.proto.plush new file mode 100644 index 0000000000..bf3f025f15 --- /dev/null +++ b/starport/templates/module/create/stargate/proto/{{moduleName}}/params.proto.plush @@ -0,0 +1,13 @@ +syntax = "proto3"; +package <%= formatOwnerName(ownerName) %>.<%= appName %>.<%= moduleName %>; + +import "gogoproto/gogo.proto"; + +option go_package = "<%= modulePath %>/x/<%= moduleName %>/types"; + +// Params defines the parameters for the module. +message Params { + option (gogoproto.goproto_stringer) = false; + <%= for (i, param) in params { %> + <%= param.ProtoType(i+1) %> [(gogoproto.moretags) = "yaml:\"<%= param.Name.Snake %>\""];<% } %> +} \ No newline at end of file diff --git a/starport/templates/module/create/stargate/proto/{{moduleName}}/query.proto.plush b/starport/templates/module/create/stargate/proto/{{moduleName}}/query.proto.plush index 131f0a343b..83e8d6b3c2 100644 --- a/starport/templates/module/create/stargate/proto/{{moduleName}}/query.proto.plush +++ b/starport/templates/module/create/stargate/proto/{{moduleName}}/query.proto.plush @@ -1,15 +1,30 @@ syntax = "proto3"; package <%= formatOwnerName(ownerName) %>.<%= appName %>.<%= moduleName %>; +import "gogoproto/gogo.proto"; import "google/api/annotations.proto"; import "cosmos/base/query/v1beta1/pagination.proto"; +import "<%= moduleName %>/params.proto"; // this line is used by starport scaffolding # 1 option go_package = "<%= modulePath %>/x/<%= moduleName %>/types"; // Query defines the gRPC querier service. service Query { - // this line is used by starport scaffolding # 2 + // Parameters queries the parameters of the module. + rpc Params(QueryParamsRequest) returns (QueryParamsResponse) { + option (google.api.http).get = "/<%= formatOwnerName(ownerName) %>/<%= appName %>/<%= moduleName %>/params"; + } + // this line is used by starport scaffolding # 2 +} + +// QueryParamsRequest is request type for the Query/Params RPC method. +message QueryParamsRequest {} + +// QueryParamsResponse is response type for the Query/Params RPC method. +message QueryParamsResponse { + // params holds all the parameters of this module. + Params params = 1 [(gogoproto.nullable) = false]; } // this line is used by starport scaffolding # 3 diff --git a/starport/templates/module/create/stargate/testutil/keeper/{{moduleName}}.go.plush b/starport/templates/module/create/stargate/testutil/keeper/{{moduleName}}.go.plush index 0dd7235031..06e411158f 100644 --- a/starport/templates/module/create/stargate/testutil/keeper/{{moduleName}}.go.plush +++ b/starport/templates/module/create/stargate/testutil/keeper/{{moduleName}}.go.plush @@ -10,6 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/store" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" + typesparams "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/libs/log" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" @@ -27,13 +28,26 @@ func <%= title(moduleName) %>Keeper(t testing.TB) (*keeper.Keeper, sdk.Context) require.NoError(t, stateStore.LoadLatestVersion()) registry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(registry) + + paramsSubspace := typesparams.NewSubspace(cdc, + types.Amino, + storeKey, + memStoreKey, + "<%= title(moduleName) %>Params", + ) k := keeper.NewKeeper( - codec.NewProtoCodec(registry), + cdc, storeKey, - memStoreKey,<%= for (dependency) in dependencies { %> + memStoreKey, + paramsSubspace, <%= for (dependency) in dependencies { %> nil,<% } %> ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) + + // Initialize params + k.SetParams(ctx, types.DefaultParams()) + return k, ctx } diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/query.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/query.go.plush index 9fe0e47ccb..4d2674d7f7 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/query.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/query.go.plush @@ -24,6 +24,7 @@ func GetQueryCmd(queryRoute string) *cobra.Command { RunE: client.ValidateCmd, } + cmd.AddCommand(CmdQueryParams()) // this line is used by starport scaffolding # 1 return cmd diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/query_params.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/query_params.go.plush new file mode 100644 index 0000000000..612273344f --- /dev/null +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/query_params.go.plush @@ -0,0 +1,34 @@ +package cli + +import ( + "context" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/spf13/cobra" + "<%= modulePath %>/x/<%= moduleName %>/types" +) + +func CmdQueryParams() *cobra.Command { + cmd := &cobra.Command{ + Use: "params", + Short: "shows the parameters of the module", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx := client.GetClientContextFromCmd(cmd) + + queryClient := types.NewQueryClient(clientCtx) + + res, err := queryClient.Params(context.Background(), &types.QueryParamsRequest{}) + if err != nil { + return err + } + + return clientCtx.PrintProto(res) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/tx.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/tx.go.plush index 91c6b08cba..8f0d2003fc 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/tx.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/client/cli/tx.go.plush @@ -17,9 +17,9 @@ var ( const ( flagPacketTimeoutTimestamp = "packet-timeout-timestamp" + listSeparator = "," ) - // GetTxCmd returns the transaction commands for this module func GetTxCmd() *cobra.Command { cmd := &cobra.Command{ diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/genesis.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/genesis.go.plush index b9ee3ac61f..0f2555e734 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/genesis.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/genesis.go.plush @@ -10,11 +10,13 @@ import ( // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { // this line is used by starport scaffolding # genesis/module/init + k.SetParams(ctx, genState.Params) } // ExportGenesis returns the capability module's exported genesis. func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis := types.DefaultGenesis() + genesis.Params = k.GetParams(ctx) // this line is used by starport scaffolding # genesis/module/export diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/grpc_query_params.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/grpc_query_params.go.plush new file mode 100644 index 0000000000..04ea1dc541 --- /dev/null +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/grpc_query_params.go.plush @@ -0,0 +1,19 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + "<%= modulePath %>/x/<%= moduleName %>/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (k Keeper) Params(c context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + ctx := sdk.UnwrapSDKContext(c) + + return &types.QueryParamsResponse{Params: k.GetParams(ctx)}, nil +} diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/grpc_query_params_test.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/grpc_query_params_test.go.plush new file mode 100644 index 0000000000..9b4585f88e --- /dev/null +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/grpc_query_params_test.go.plush @@ -0,0 +1,21 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + testkeeper "<%= modulePath %>/testutil/keeper" + "<%= modulePath %>/x/<%= moduleName %>/types" +) + +func TestParamsQuery(t *testing.T) { + keeper, ctx := testkeeper.<%= title(moduleName) %>Keeper(t) + wctx := sdk.WrapSDKContext(ctx) + params := types.DefaultParams() + keeper.SetParams(ctx, params) + + response, err := keeper.Params(wctx, &types.QueryParamsRequest{}) + require.NoError(t, err) + require.Equal(t, &types.QueryParamsResponse{Params: params}, response) +} diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper.go.plush index 32b540274b..b60de1ec48 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/keeper.go.plush @@ -7,17 +7,18 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "<%= modulePath %>/x/<%= moduleName %>/types" + <%= if (isIBC) { %>"github.com/tendermint/spm/ibckeeper"<% } %> ) type ( Keeper struct { - cdc codec.BinaryCodec - storeKey sdk.StoreKey - memKey sdk.StoreKey - <%= if (isIBC) { %>channelKeeper types.ChannelKeeper - portKeeper types.PortKeeper - scopedKeeper types.ScopedKeeper<% } %> + <%= if (isIBC) { %>*ibckeeper.Keeper<% } %> + cdc codec.BinaryCodec + storeKey sdk.StoreKey + memKey sdk.StoreKey + paramstore paramtypes.Subspace <%= for (dependency) in dependencies { %> <%= dependency.Name %>Keeper types.<%= title(dependency.Name) %>Keeper<% } %> } @@ -27,18 +28,29 @@ func NewKeeper( cdc codec.BinaryCodec, storeKey, memKey sdk.StoreKey, - <%= if (isIBC) { %>channelKeeper types.ChannelKeeper, - portKeeper types.PortKeeper, - scopedKeeper types.ScopedKeeper,<% } %> + ps paramtypes.Subspace, + <%= if (isIBC) { %>channelKeeper ibckeeper.ChannelKeeper, + portKeeper ibckeeper.PortKeeper, + scopedKeeper ibckeeper.ScopedKeeper,<% } %> <%= for (dependency) in dependencies { %><%= dependency.Name %>Keeper types.<%= title(dependency.Name) %>Keeper,<% } %> ) *Keeper { + // set KeyTable if it has not already been set + if !ps.HasKeyTable() { + ps = ps.WithKeyTable(types.ParamKeyTable()) + } + return &Keeper{ - cdc: cdc, - storeKey: storeKey, - memKey: memKey, - <%= if (isIBC) { %>channelKeeper: channelKeeper, - portKeeper: portKeeper, - scopedKeeper: scopedKeeper,<% } %> + <%= if (isIBC) { %>Keeper: ibckeeper.NewKeeper( + types.PortKey, + storeKey, + channelKeeper, + portKeeper, + scopedKeeper, + ),<% } %> + cdc: cdc, + storeKey: storeKey, + memKey: memKey, + paramstore: ps, <%= for (dependency) in dependencies { %><%= dependency.Name %>Keeper: <%= dependency.Name %>Keeper,<% } %> } } diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/params.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/params.go.plush new file mode 100644 index 0000000000..b02a89b0f9 --- /dev/null +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/params.go.plush @@ -0,0 +1,26 @@ +package keeper + +import ( + "<%= modulePath %>/x/<%= moduleName %>/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// GetParams get all parameters as types.Params +func (k Keeper) GetParams(ctx sdk.Context) types.Params { + return types.NewParams(<%= for (param) in params { %> + k.<%= param.Name.UpperCamel %>(ctx),<% } %> + ) +} + +// SetParams set the params +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramstore.SetParamSet(ctx, ¶ms) +} + +<%= for (param) in params { %> +// <%= param.Name.UpperCamel %> returns the <%= param.Name.UpperCamel %> param +func (k Keeper) <%= param.Name.UpperCamel %>(ctx sdk.Context) (res <%= param.DataType() %>) { + k.paramstore.Get(ctx, types.Key<%= param.Name.UpperCamel %>, &res) + return +} +<% } %> \ No newline at end of file diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/params_test.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/params_test.go.plush new file mode 100644 index 0000000000..28b33a9609 --- /dev/null +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/keeper/params_test.go.plush @@ -0,0 +1,19 @@ +package keeper_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + testkeeper "<%= modulePath %>/testutil/keeper" + "<%= modulePath %>/x/<%= moduleName %>/types" +) + +func TestGetParams(t *testing.T) { + k, ctx := testkeeper.<%= title(moduleName) %>Keeper(t) + params := types.DefaultParams() + + k.SetParams(ctx, params) + + require.EqualValues(t, params, k.GetParams(ctx))<%= for (param) in params { %> + require.EqualValues(t, params.<%= param.Name.UpperCamel %>, k.<%= param.Name.UpperCamel %>(ctx))<% } %> +} diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/module.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/module.go.plush index e81118f7b8..819ae60646 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/module.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/module.go.plush @@ -100,13 +100,22 @@ func (AppModuleBasic) GetQueryCmd() *cobra.Command { type AppModule struct { AppModuleBasic - keeper keeper.Keeper + keeper keeper.Keeper + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper } -func NewAppModule(cdc codec.Codec, keeper keeper.Keeper) AppModule { +func NewAppModule( + cdc codec.Codec, + keeper keeper.Keeper, + accountKeeper types.AccountKeeper, + bankKeeper types.BankKeeper, +) AppModule { return AppModule{ AppModuleBasic: NewAppModuleBasic(cdc), keeper: keeper, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, } } diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/types/codec.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/types/codec.go.plush index 5eae940178..fb6871d22c 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/types/codec.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/types/codec.go.plush @@ -18,6 +18,6 @@ func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { } var ( - amino = codec.NewLegacyAmino() + Amino = codec.NewLegacyAmino() ModuleCdc = codec.NewProtoCodec(cdctypes.NewInterfaceRegistry()) ) diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/types/expected_keepers.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/types/expected_keepers.go.plush index 74e14e8e3c..1d6213dfbf 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/types/expected_keepers.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/types/expected_keepers.go.plush @@ -1,7 +1,26 @@ package types +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/types" +) + <%= for (dependency) in dependencies { %> +<%= if (dependency.Name != "bank" && dependency.Name != "account") { %> type <%= title(dependency.Name) %>Keeper interface { // Methods imported from <%= dependency.Name %> should be defined here } -<% } %> \ No newline at end of file +<% } %> +<% } %> + +// AccountKeeper defines the expected account keeper used for simulations (noalias) +type AccountKeeper interface { + GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI + // Methods imported from account should be defined here +} + +// BankKeeper defines the expected interface needed to retrieve account balances. +type BankKeeper interface { + SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + // Methods imported from bank should be defined here +} \ No newline at end of file diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/types/genesis.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/types/genesis.go.plush index 21701257c1..aa82115f83 100644 --- a/starport/templates/module/create/stargate/x/{{moduleName}}/types/genesis.go.plush +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/types/genesis.go.plush @@ -11,6 +11,7 @@ const DefaultIndex uint64 = 1 func DefaultGenesis() *GenesisState { return &GenesisState{ // this line is used by starport scaffolding # genesis/types/default + Params: DefaultParams(), } } @@ -19,5 +20,5 @@ func DefaultGenesis() *GenesisState { func (gs GenesisState) Validate() error { // this line is used by starport scaffolding # genesis/types/validate - return nil + return gs.Params.Validate() } diff --git a/starport/templates/module/create/stargate/x/{{moduleName}}/types/params.go.plush b/starport/templates/module/create/stargate/x/{{moduleName}}/types/params.go.plush new file mode 100644 index 0000000000..c31e01b2c2 --- /dev/null +++ b/starport/templates/module/create/stargate/x/{{moduleName}}/types/params.go.plush @@ -0,0 +1,77 @@ +package types + +import ( + <%= if (len(params) > 0) { %>"fmt"<% } %> + + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + "gopkg.in/yaml.v2" +) + +var _ paramtypes.ParamSet = (*Params)(nil) + +<%= for (param) in params { %> +var ( + Key<%= param.Name.UpperCamel %> = []byte("<%= param.Name.UpperCamel %>")<%= if (param.DataType() == "string") { %> + // TODO: Determine the default value + Default<%= param.Name.UpperCamel %> <%= param.DataType() %> = "<%= param.Name.Snake %>"<% } else { %> + // TODO: Determine the default value + Default<%= param.Name.UpperCamel %> <%= param.DataType() %> = <%= param.ValueIndex() %><% } %> +) +<% } %> + +// ParamKeyTable the param key table for launch module +func ParamKeyTable() paramtypes.KeyTable { + return paramtypes.NewKeyTable().RegisterParamSet(&Params{}) +} + +// NewParams creates a new Params instance +func NewParams(<%= for (param) in params { %> + <%= param.Name.LowerCamel %> <%= param.DataType() %>,<% } %> +) Params { + return Params{<%= for (param) in params { %> + <%= param.Name.UpperCamel %>: <%= param.Name.LowerCamel %>,<% } %> + } +} + +// DefaultParams returns a default set of parameters +func DefaultParams() Params { + return NewParams(<%= for (param) in params { %> + Default<%= param.Name.UpperCamel %>,<% } %> + ) +} + +// ParamSetPairs get the params.ParamSet +func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { + return paramtypes.ParamSetPairs{<%= for (param) in params { %> + paramtypes.NewParamSetPair(Key<%= param.Name.UpperCamel %>, &p.<%= param.Name.UpperCamel %>, validate<%= param.Name.UpperCamel %>),<% } %> + } +} + +// Validate validates the set of params +func (p Params) Validate() error {<%= for (param) in params { %> + if err := validate<%= param.Name.UpperCamel %>(p.<%= param.Name.UpperCamel %>); err != nil { + return err + } + <% } %> + return nil +} + +// String implements the Stringer interface. +func (p Params) String() string { + out, _ := yaml.Marshal(p) + return string(out) +} +<%= for (param) in params { %> +// validate<%= param.Name.UpperCamel %> validates the <%= param.Name.UpperCamel %> param +func validate<%= param.Name.UpperCamel %>(v interface{}) error { + <%= param.Name.LowerCamel %>, ok := v.(<%= param.DataType() %>) + if !ok { + return fmt.Errorf("invalid parameter type: %T", v) + } + + // TODO implement validation + _ = <%= param.Name.LowerCamel %> + + return nil +} +<% } %> \ No newline at end of file diff --git a/starport/templates/module/create/templates.go b/starport/templates/module/create/templates.go index a070bc977c..450c5d07ed 100644 --- a/starport/templates/module/create/templates.go +++ b/starport/templates/module/create/templates.go @@ -14,9 +14,9 @@ var ( //go:embed msgserver/* msgserver/**/* fsMsgServer embed.FS - //go:embed genesistest/module/* genesistest/module/**/* - fsGenesisModuleTest embed.FS + //go:embed genesistest/* genesistest/**/* + fsGenesisTest embed.FS - //go:embed genesistest/types/* genesistest/types/**/* - fsGenesisTypesTest embed.FS + //go:embed simapp/* simapp/**/* + fsSimapp embed.FS ) diff --git a/starport/templates/module/import/stargate.go b/starport/templates/module/import/stargate.go index e7f9bc24f1..b2dc128248 100644 --- a/starport/templates/module/import/stargate.go +++ b/starport/templates/module/import/stargate.go @@ -7,7 +7,7 @@ import ( "github.com/gobuffalo/genny" "github.com/gobuffalo/plush" "github.com/tendermint/starport/starport/pkg/placeholder" - "github.com/tendermint/starport/starport/pkg/plushhelpers" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" "github.com/tendermint/starport/starport/templates/module" ) diff --git a/starport/templates/query/options.go b/starport/templates/query/options.go index 6c1e836035..806e04f259 100644 --- a/starport/templates/query/options.go +++ b/starport/templates/query/options.go @@ -1,8 +1,8 @@ package query import ( - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/multiformatname" + "github.com/tendermint/starport/starport/templates/field" ) // Options ... diff --git a/starport/templates/query/query.go b/starport/templates/query/query.go index 6781ee2eaa..e121a3def3 100644 --- a/starport/templates/query/query.go +++ b/starport/templates/query/query.go @@ -7,7 +7,7 @@ import ( "github.com/gobuffalo/packd" "github.com/gobuffalo/plush" "github.com/gobuffalo/plushgen" - "github.com/tendermint/starport/starport/pkg/plushhelpers" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" ) var ( diff --git a/starport/templates/query/stargate.go b/starport/templates/query/stargate.go index 087613cf4e..ea8895093a 100644 --- a/starport/templates/query/stargate.go +++ b/starport/templates/query/stargate.go @@ -35,10 +35,17 @@ func protoQueryModify(replacer placeholder.Replacer, opts *Options) genny.RunFn return err } + // if the query has request fields, they are appended to the rpc query + var reqPath string + for _, field := range opts.ReqFields { + reqPath += "/" + reqPath = filepath.Join(reqPath, fmt.Sprintf("{%s}", field.ProtoFieldName())) + } + // RPC service - templateRPC := `// Queries a list of %[3]v items. + templateRPC := `// Queries a list of %[2]v items. rpc %[2]v(Query%[2]vRequest) returns (Query%[2]vResponse) { - option (google.api.http).get = "/%[4]v/%[5]v/%[6]v/%[3]v"; + option (google.api.http).get = "/%[3]v/%[4]v/%[5]v/%[6]v%[7]v"; } %[1]v` @@ -46,17 +53,18 @@ func protoQueryModify(replacer placeholder.Replacer, opts *Options) genny.RunFn templateRPC, Placeholder2, opts.QueryName.UpperCamel, - opts.QueryName.LowerCamel, opts.OwnerName, opts.AppName, opts.ModuleName, + opts.QueryName.Snake, + reqPath, ) content := replacer.Replace(f.String(), Placeholder2, replacementRPC) // Fields for request var reqFields string for i, field := range opts.ReqFields { - reqFields += fmt.Sprintf(" %s %s = %d;\n", field.Datatype, field.Name.LowerCamel, i+1) + reqFields += fmt.Sprintf(" %s;\n", field.ProtoType(i+1)) } if opts.Paginated { reqFields += fmt.Sprintf("cosmos.base.query.v1beta1.PageRequest pagination = %d;\n", len(opts.ReqFields)+1) @@ -65,17 +73,23 @@ func protoQueryModify(replacer placeholder.Replacer, opts *Options) genny.RunFn // Fields for response var resFields string for i, field := range opts.ResFields { - resFields += fmt.Sprintf(" %s %s = %d;\n", field.Datatype, field.Name.LowerCamel, i+1) + resFields += fmt.Sprintf(" %s;\n", field.ProtoType(i+1)) } if opts.Paginated { resFields += fmt.Sprintf("cosmos.base.query.v1beta1.PageResponse pagination = %d;\n", len(opts.ResFields)+1) } // Ensure custom types are imported + protoImports := append(opts.ResFields.ProtoImports(), opts.ReqFields.ProtoImports()...) customFields := append(opts.ResFields.Custom(), opts.ReqFields.Custom()...) for _, f := range customFields { + protoImports = append(protoImports, + fmt.Sprintf("%[1]v/%[2]v.proto", opts.ModuleName, f), + ) + } + for _, f := range protoImports { importModule := fmt.Sprintf(` -import "%[1]v/%[2]v.proto";`, opts.ModuleName, f) +import "%[1]v";`, f) content = strings.ReplaceAll(content, importModule, "") replacementImport := fmt.Sprintf("%[1]v%[2]v", Placeholder, importModule) diff --git a/starport/templates/query/stargate/x/{{moduleName}}/client/cli/query_{{queryName}}.go.plush b/starport/templates/query/stargate/x/{{moduleName}}/client/cli/query_{{queryName}}.go.plush index 6040ec7767..e1a8030975 100644 --- a/starport/templates/query/stargate/x/{{moduleName}}/client/cli/query_{{queryName}}.go.plush +++ b/starport/templates/query/stargate/x/{{moduleName}}/client/cli/query_{{queryName}}.go.plush @@ -2,11 +2,9 @@ package cli import ( "strconv" - <%= if (ReqFields.IsComplex()) { %> "encoding/json" <% } %> - - <%= if (ReqFields.NeedCastImport()) { %> "github.com/spf13/cast" <% } %> + <%= for (goImport) in mergeGoImports(ReqFields) { %> + <%= goImport.Alias %> "<%= goImport.Name %>"<% } %> "github.com/spf13/cobra" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "<%= ModulePath %>/x/<%= ModuleName %>/types" @@ -20,7 +18,7 @@ func Cmd<%= QueryName.UpperCamel %>() *cobra.Command { Short: "<%= Description %>", Args: cobra.ExactArgs(<%= len(ReqFields) %>), RunE: func(cmd *cobra.Command, args []string) (err error) { - <%= for (i, field) in ReqFields { %> <%= castArg("req", field, i) %> + <%= for (i, field) in ReqFields { %> <%= field.CLIArgs("req", i) %> <% } %> clientCtx, err := client.GetClientTxContext(cmd) if err != nil { diff --git a/starport/templates/testutil/register.go b/starport/templates/testutil/register.go index d9c4b184de..b269565f55 100644 --- a/starport/templates/testutil/register.go +++ b/starport/templates/testutil/register.go @@ -2,49 +2,18 @@ package testutil import ( "embed" - "fmt" - "os" - "path/filepath" "github.com/gobuffalo/genny" - "github.com/gobuffalo/plush" "github.com/tendermint/starport/starport/pkg/xgenny" ) -const ( - modulePathKey = "ModulePath" - testUtilDir = "testutil" - sampleDir = "sample" -) - var ( //go:embed stargate/* stargate/**/* fsStargate embed.FS - //go:embed stargate/testutil/sample/* - fsSample embed.FS ) // Register testutil template using existing generator. // Register is meant to be used by modules that depend on this module. -//nolint:interfacer -func Register(ctx *plush.Context, gen *genny.Generator, appPath string) error { - if !ctx.Has(modulePathKey) { - return fmt.Errorf("ctx is missing value for the key %s", modulePathKey) - } - // Check if the testutil folder already exists - path := filepath.Join(appPath, testUtilDir) - if _, err := os.Stat(path); os.IsNotExist(err) { - // if not, box the entire testutil folder - return gen.Box(xgenny.NewEmbedWalker(fsStargate, "stargate/", appPath)) - } else if err != nil { - return err - } - - // if yes, only box the sample folder - path = filepath.Join(path, sampleDir) - _, err := os.Stat(path) - if os.IsNotExist(err) { - return gen.Box(xgenny.NewEmbedWalker(fsSample, "stargate/", appPath)) - } - return err +func Register(gen *genny.Generator, appPath string) error { + return xgenny.Box(gen, xgenny.NewEmbedWalker(fsStargate, "stargate/", appPath)) } diff --git a/starport/templates/testutil/stargate/testutil/nullify/nullify.go.plush b/starport/templates/testutil/stargate/testutil/nullify/nullify.go.plush new file mode 100644 index 0000000000..3b968c09c1 --- /dev/null +++ b/starport/templates/testutil/stargate/testutil/nullify/nullify.go.plush @@ -0,0 +1,57 @@ +// Package nullify provides methods to init nil values structs for test assertion. +package nullify + +import ( + "reflect" + "unsafe" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var ( + coinType = reflect.TypeOf(sdk.Coin{}) + coinsType = reflect.TypeOf(sdk.Coins{}) +) + +// Fill analyze all struct fields and slices with +// reflection and initialize the nil and empty slices, +// structs, and pointers. +func Fill(x interface{}) interface{} { + v := reflect.Indirect(reflect.ValueOf(x)) + switch v.Kind() { + case reflect.Slice: + for i := 0; i < v.Len(); i++ { + obj := v.Index(i) + objPt := reflect.NewAt(obj.Type(), unsafe.Pointer(obj.UnsafeAddr())).Interface() + objPt = Fill(objPt) + obj.Set(reflect.ValueOf(objPt)) + } + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + f := reflect.Indirect(v.Field(i)) + if !f.CanSet() { + continue + } + switch f.Kind() { + case reflect.Slice: + f.Set(reflect.MakeSlice(f.Type(), 0, 0)) + case reflect.Struct: + switch f.Type() { + case coinType: + coin := reflect.New(coinType).Interface() + s := reflect.ValueOf(coin).Elem() + f.Set(s) + case coinsType: + coins := reflect.New(coinsType).Interface() + s := reflect.ValueOf(coins).Elem() + f.Set(s) + default: + objPt := reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Interface() + s := Fill(objPt) + f.Set(reflect.ValueOf(s)) + } + } + } + } + return reflect.Indirect(v).Interface() +} diff --git a/starport/templates/testutil/stargate/testutil/simapp/simapp.go.plush b/starport/templates/testutil/stargate/testutil/simapp/simapp.go.plush deleted file mode 100644 index d3b0789f27..0000000000 --- a/starport/templates/testutil/stargate/testutil/simapp/simapp.go.plush +++ /dev/null @@ -1,49 +0,0 @@ -package simapp - -import ( - "time" - - "github.com/cosmos/cosmos-sdk/simapp" - "github.com/tendermint/spm/cosmoscmd" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/log" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - tmtypes "github.com/tendermint/tendermint/types" - tmdb "github.com/tendermint/tm-db" - - "<%= ModulePath %>/app" -) - -// New creates application instance with in-memory database and disabled logging. -func New(dir string) cosmoscmd.App { - db := tmdb.NewMemDB() - logger := log.NewNopLogger() - - encoding := cosmoscmd.MakeEncodingConfig(app.ModuleBasics) - - a := app.New(logger, db, nil, true, map[int64]bool{}, dir, 0, encoding, - simapp.EmptyAppOptions{}) - // InitChain updates deliverState which is required when app.NewContext is called - a.InitChain(abci.RequestInitChain{ - ConsensusParams: defaultConsensusParams, - AppStateBytes: []byte("{}"), - }) - return a -} - -var defaultConsensusParams = &abci.ConsensusParams{ - Block: &abci.BlockParams{ - MaxBytes: 200000, - MaxGas: 2000000, - }, - Evidence: &tmproto.EvidenceParams{ - MaxAgeNumBlocks: 302400, - MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration - MaxBytes: 10000, - }, - Validator: &tmproto.ValidatorParams{ - PubKeyTypes: []string{ - tmtypes.ABCIPubKeyTypeEd25519, - }, - }, -} diff --git a/starport/templates/typed/dry/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush b/starport/templates/typed/dry/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush index 41d549203e..c59cc818fa 100644 --- a/starport/templates/typed/dry/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush +++ b/starport/templates/typed/dry/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush @@ -1,10 +1,11 @@ syntax = "proto3"; package <%= formatOwnerName(OwnerName) %>.<%= AppName %>.<%= ModuleName %>; -option go_package = "<%= ModulePath %>/x/<%= ModuleName %>/types"; -<%= for (i, importName) in Fields.Custom() { %>import "<%= ModuleName %>/<%= importName %>.proto"; <% } %> +option go_package = "<%= ModulePath %>/x/<%= ModuleName %>/types";<%= for (importName) in mergeCustomImports(Fields) { %> +import "<%= ModuleName %>/<%= importName %>.proto"; <% } %><%= for (importName) in mergeProtoImports(Fields) { %> +import "<%= importName %>"; <% } %> message <%= TypeName.UpperCamel %> { <%= for (i, field) in Fields { %> - <%= field.GetProtoDatatype() %> <%= field.Name.LowerCamel %> = <%= i+1 %>; <% } %> + <%= field.ProtoType(i+1) %>; <% } %> } diff --git a/starport/templates/typed/list/genesis.go b/starport/templates/typed/list/genesis.go index 8ecc1e42de..2e587f697f 100644 --- a/starport/templates/typed/list/genesis.go +++ b/starport/templates/typed/list/genesis.go @@ -160,15 +160,15 @@ func genesisTestsModify(replacer placeholder.Replacer, opts *typed.Options) genn } templateState := `%[2]vList: []types.%[2]v{ - { - Id: 0, - }, - { - Id: 1, + { + Id: 0, + }, + { + Id: 1, + }, }, -}, -%[2]vCount: 2, -%[1]v` + %[2]vCount: 2, + %[1]v` replacementValid := fmt.Sprintf( templateState, module.PlaceholderGenesisTestState, @@ -176,8 +176,7 @@ func genesisTestsModify(replacer placeholder.Replacer, opts *typed.Options) genn ) content := replacer.Replace(f.String(), module.PlaceholderGenesisTestState, replacementValid) - templateAssert := `require.Len(t, got.%[2]vList, len(genesisState.%[2]vList)) -require.Subset(t, genesisState.%[2]vList, got.%[2]vList) + templateAssert := `require.ElementsMatch(t, genesisState.%[2]vList, got.%[2]vList) require.Equal(t, genesisState.%[2]vCount, got.%[2]vCount) %[1]v` replacementTests := fmt.Sprintf( diff --git a/starport/templates/typed/list/simulation.go b/starport/templates/typed/list/simulation.go new file mode 100644 index 0000000000..8c3382dee4 --- /dev/null +++ b/starport/templates/typed/list/simulation.go @@ -0,0 +1,55 @@ +package list + +import ( + "fmt" + "path/filepath" + + "github.com/gobuffalo/genny" + "github.com/tendermint/starport/starport/pkg/placeholder" + "github.com/tendermint/starport/starport/templates/typed" +) + +func moduleSimulationModify(replacer placeholder.Replacer, opts *typed.Options) genny.RunFn { + return func(r *genny.Runner) error { + path := filepath.Join(opts.AppPath, "x", opts.ModuleName, "module_simulation.go") + f, err := r.Disk.Find(path) + if err != nil { + return err + } + + // Create a list of two different indexes and fields to use as sample + msgField := fmt.Sprintf("%s: sample.AccAddress(),\n", opts.MsgSigner.UpperCamel) + + // simulation genesis state + templateGs := ` %[2]vList: []types.%[2]v{ + { + Id: 0, + %[3]v + }, + { + Id: 1, + %[3]v + }, + }, + %[2]vCount: 2, + %[1]v` + replacementGs := fmt.Sprintf( + templateGs, + typed.PlaceholderSimappGenesisState, + opts.TypeName.UpperCamel, + msgField, + ) + content := replacer.Replace(f.String(), typed.PlaceholderSimappGenesisState, replacementGs) + + content = typed.ModuleSimulationMsgModify( + replacer, + content, + opts.ModuleName, + opts.TypeName, + "Create", "Update", "Delete", + ) + + newFile := genny.NewFileS(path, content) + return r.File(newFile) + } +} diff --git a/starport/templates/typed/list/stargate.go b/starport/templates/typed/list/stargate.go index 40a860edc4..86ed3bb3cd 100644 --- a/starport/templates/typed/list/stargate.go +++ b/starport/templates/typed/list/stargate.go @@ -52,6 +52,7 @@ func NewStargate(replacer placeholder.Replacer, opts *typed.Options) (*genny.Gen g.RunFn(protoTxModify(replacer, opts)) g.RunFn(typesCodecModify(replacer, opts)) g.RunFn(clientCliTxModify(replacer, opts)) + g.RunFn(moduleSimulationModify(replacer, opts)) // Messages template if err := typed.Box(messagesTemplate, opts, g); err != nil { @@ -130,17 +131,23 @@ func protoTxModify(replacer placeholder.Replacer, opts *typed.Options) genny.Run // Messages var createFields string for i, field := range opts.Fields { - createFields += fmt.Sprintf(" %s %s = %d;\n", field.Datatype, field.Name.LowerCamel, i+2) + createFields += fmt.Sprintf(" %s;\n", field.ProtoType(i+2)) } var updateFields string for i, field := range opts.Fields { - updateFields += fmt.Sprintf(" %s %s = %d;\n", field.Datatype, field.Name.LowerCamel, i+3) + updateFields += fmt.Sprintf(" %s;\n", field.ProtoType(i+3)) } // Ensure custom types are imported + protoImports := opts.Fields.ProtoImports() for _, f := range opts.Fields.Custom() { + protoImports = append(protoImports, + fmt.Sprintf("%[1]v/%[2]v.proto", opts.ModuleName, f), + ) + } + for _, f := range protoImports { importModule := fmt.Sprintf(` -import "%[1]v/%[2]v.proto";`, opts.ModuleName, f) +import "%[1]v";`, f) content = strings.ReplaceAll(content, importModule, "") replacementImport := fmt.Sprintf("%[1]v%[2]v", typed.PlaceholderProtoTxImport, importModule) @@ -206,23 +213,23 @@ func protoQueryModify(replacer placeholder.Replacer, opts *typed.Options) genny. content = replacer.Replace(content, typed.Placeholder, replacementGogoImport) // RPC service - templateRPC := `// Queries a %[3]v by id. + templateRPC := `// Queries a %[2]v by id. rpc %[2]v(QueryGet%[2]vRequest) returns (QueryGet%[2]vResponse) { - option (google.api.http).get = "/%[4]v/%[5]v/%[6]v/%[3]v/{id}"; + option (google.api.http).get = "/%[3]v/%[4]v/%[5]v/%[6]v/{id}"; } - // Queries a list of %[3]v items. + // Queries a list of %[2]v items. rpc %[2]vAll(QueryAll%[2]vRequest) returns (QueryAll%[2]vResponse) { - option (google.api.http).get = "/%[4]v/%[5]v/%[6]v/%[3]v"; + option (google.api.http).get = "/%[3]v/%[4]v/%[5]v/%[6]v"; } %[1]v` replacementRPC := fmt.Sprintf(templateRPC, typed.Placeholder2, opts.TypeName.UpperCamel, - opts.TypeName.LowerCamel, opts.OwnerName, opts.AppName, opts.ModuleName, + opts.TypeName.Snake, ) content = replacer.Replace(content, typed.Placeholder2, replacementRPC) diff --git a/starport/templates/typed/list/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush b/starport/templates/typed/list/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush index fc2a97bab5..92ebe08e0d 100644 --- a/starport/templates/typed/list/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush +++ b/starport/templates/typed/list/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush @@ -1,13 +1,12 @@ syntax = "proto3"; package <%= formatOwnerName(OwnerName) %>.<%= AppName %>.<%= ModuleName %>; -option go_package = "<%= ModulePath %>/x/<%= ModuleName %>/types"; - -import "gogoproto/gogo.proto"; -<%= for (i, importName) in Fields.Custom() { %>import "<%= ModuleName %>/<%= importName %>.proto"; <% } %> +option go_package = "<%= ModulePath %>/x/<%= ModuleName %>/types";<%= for (importName) in mergeCustomImports(Fields) { %> +import "<%= ModuleName %>/<%= importName %>.proto"; <% } %><%= for (importName) in mergeProtoImports(Fields) { %> +import "<%= importName %>"; <% } %> message <%= TypeName.UpperCamel %> { uint64 id = 1;<%= for (i, field) in Fields { %> - <%= field.GetProtoDatatype() %> <%= field.Name.LowerCamel %> = <%= i+2 %>; <% } %> + <%= field.ProtoType(i+2) %>; <% } %> <%= if (!NoMessage) { %>string <%= MsgSigner.LowerCamel %> = <%= len(Fields)+2 %>;<% } %> } \ No newline at end of file diff --git a/starport/templates/typed/list/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush b/starport/templates/typed/list/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush index b70ace8682..82e9911683 100644 --- a/starport/templates/typed/list/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush +++ b/starport/templates/typed/list/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush @@ -12,6 +12,7 @@ import ( "google.golang.org/grpc/status" "<%= ModulePath %>/testutil/network" + "<%= ModulePath %>/testutil/nullify" "<%= ModulePath %>/x/<%= ModuleName %>/client/cli" "<%= ModulePath %>/x/<%= ModuleName %>/types" ) @@ -23,9 +24,11 @@ func networkWith<%= TypeName.UpperCamel %>Objects(t *testing.T, n int) (*network require.NoError(t, cfg.Codec.UnmarshalJSON(cfg.GenesisState[types.ModuleName], &state)) for i := 0; i < n; i++ { - state.<%= TypeName.UpperCamel %>List = append(state.<%= TypeName.UpperCamel %>List, types.<%= TypeName.UpperCamel %>{ - Id: uint64(i), - }) + <%= TypeName.LowerCamel %> := types.<%= TypeName.UpperCamel %>{ + Id: uint64(i), + } + nullify.Fill(&<%= TypeName.LowerCamel %>) + state.<%= TypeName.UpperCamel %>List = append(state.<%= TypeName.UpperCamel %>List, <%= TypeName.LowerCamel %>) } buf, err := cfg.Codec.MarshalJSON(&state) require.NoError(t, err) @@ -74,7 +77,10 @@ func TestShow<%= TypeName.UpperCamel %>(t *testing.T) { var resp types.QueryGet<%= TypeName.UpperCamel %>Response require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.NotNil(t, resp.<%= TypeName.UpperCamel %>) - require.Equal(t, tc.obj, resp.<%= TypeName.UpperCamel %>) + require.Equal(t, + nullify.Fill(&tc.obj), + nullify.Fill(&resp.<%= TypeName.UpperCamel %>), + ) } }) } @@ -108,7 +114,10 @@ func TestList<%= TypeName.UpperCamel %>(t *testing.T) { var resp types.QueryAll<%= TypeName.UpperCamel %>Response require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.LessOrEqual(t, len(resp.<%= TypeName.UpperCamel %>), step) - require.Subset(t, objs, resp.<%= TypeName.UpperCamel %>) + require.Subset(t, + nullify.Fill(objs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) } }) t.Run("ByKey", func(t *testing.T) { @@ -121,7 +130,10 @@ func TestList<%= TypeName.UpperCamel %>(t *testing.T) { var resp types.QueryAll<%= TypeName.UpperCamel %>Response require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.LessOrEqual(t, len(resp.<%= TypeName.UpperCamel %>), step) - require.Subset(t, objs, resp.<%= TypeName.UpperCamel %>) + require.Subset(t, + nullify.Fill(objs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) next = resp.Pagination.NextKey } }) @@ -133,6 +145,9 @@ func TestList<%= TypeName.UpperCamel %>(t *testing.T) { require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.NoError(t, err) require.Equal(t, len(objs), int(resp.Pagination.Total)) - require.Equal(t, objs, resp.<%= TypeName.UpperCamel %>) + require.ElementsMatch(t, + nullify.Fill(objs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) }) } diff --git a/starport/templates/typed/list/stargate/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush b/starport/templates/typed/list/stargate/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush index fcc169e358..8fa8586115 100644 --- a/starport/templates/typed/list/stargate/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush +++ b/starport/templates/typed/list/stargate/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush @@ -11,6 +11,7 @@ import ( "google.golang.org/grpc/status" "<%= ModulePath %>/x/<%= ModuleName %>/types" + "<%= ModulePath %>/testutil/nullify" keepertest "<%= ModulePath %>/testutil/keeper" ) @@ -50,7 +51,10 @@ func Test<%= TypeName.UpperCamel %>QuerySingle(t *testing.T) { require.ErrorIs(t, err, tc.err) } else { require.NoError(t, err) - require.Equal(t, tc.response, response) + require.Equal(t, + nullify.Fill(tc.response), + nullify.Fill(response), + ) } }) } @@ -77,7 +81,10 @@ func Test<%= TypeName.UpperCamel %>QueryPaginated(t *testing.T) { resp, err := keeper.<%= TypeName.UpperCamel %>All(wctx, request(nil, uint64(i), uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.<%= TypeName.UpperCamel %>), step) - require.Subset(t, msgs, resp.<%= TypeName.UpperCamel %>) + require.Subset(t, + nullify.Fill(msgs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) } }) t.Run("ByKey", func(t *testing.T) { @@ -87,7 +94,10 @@ func Test<%= TypeName.UpperCamel %>QueryPaginated(t *testing.T) { resp, err := keeper.<%= TypeName.UpperCamel %>All(wctx, request(next, 0, uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.<%= TypeName.UpperCamel %>), step) - require.Subset(t, msgs, resp.<%= TypeName.UpperCamel %>) + require.Subset(t, + nullify.Fill(msgs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) next = resp.Pagination.NextKey } }) @@ -95,6 +105,10 @@ func Test<%= TypeName.UpperCamel %>QueryPaginated(t *testing.T) { resp, err := keeper.<%= TypeName.UpperCamel %>All(wctx, request(nil, 0, 0, true)) require.NoError(t, err) require.Equal(t, len(msgs), int(resp.Pagination.Total)) + require.ElementsMatch(t, + nullify.Fill(msgs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) }) t.Run("InvalidRequest", func(t *testing.T) { _, err := keeper.<%= TypeName.UpperCamel %>All(wctx, nil) diff --git a/starport/templates/typed/list/stargate/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush b/starport/templates/typed/list/stargate/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush index dd8bd74568..f47551db82 100644 --- a/starport/templates/typed/list/stargate/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush +++ b/starport/templates/typed/list/stargate/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush @@ -3,10 +3,11 @@ package keeper_test import ( "testing" - keepertest "<%= ModulePath %>/testutil/keeper" "<%= ModulePath %>/x/<%= ModuleName %>/keeper" "<%= ModulePath %>/x/<%= ModuleName %>/types" sdk "github.com/cosmos/cosmos-sdk/types" + keepertest "<%= ModulePath %>/testutil/keeper" + "<%= ModulePath %>/testutil/nullify" "github.com/stretchr/testify/require" ) @@ -24,7 +25,10 @@ func Test<%= TypeName.UpperCamel %>Get(t *testing.T) { for _, item := range items { got, found := keeper.Get<%= TypeName.UpperCamel %>(ctx, item.Id) require.True(t, found) - require.Equal(t, item, got) + require.Equal(t, + nullify.Fill(&item), + nullify.Fill(&got), + ) } } @@ -41,7 +45,10 @@ func Test<%= TypeName.UpperCamel %>Remove(t *testing.T) { func Test<%= TypeName.UpperCamel %>GetAll(t *testing.T) { keeper, ctx := keepertest.<%= title(ModuleName) %>Keeper(t) items := createN<%= TypeName.UpperCamel %>(keeper, ctx, 10) - require.ElementsMatch(t, items, keeper.GetAll<%= TypeName.UpperCamel %>(ctx)) + require.ElementsMatch(t, + nullify.Fill(items), + nullify.Fill(keeper.GetAll<%= TypeName.UpperCamel %>(ctx)), + ) } func Test<%= TypeName.UpperCamel %>Count(t *testing.T) { diff --git a/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush b/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush index b4f52ae436..c2dde5b285 100644 --- a/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush +++ b/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush @@ -1,12 +1,10 @@ package cli import ( - <%= if (Fields.IsComplex()) { %> "encoding/json" <% } %> "strconv" - + <%= for (goImport) in mergeGoImports(Fields) { %> + <%= goImport.Alias %> "<%= goImport.Name %>"<% } %> "github.com/spf13/cobra" - <%= if (Fields.NeedCastImport()) { %> "github.com/spf13/cast" <% } %> - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -19,7 +17,7 @@ func CmdCreate<%= TypeName.UpperCamel %>() *cobra.Command { Short: "Create a new <%= TypeName.Original %>", Args: cobra.ExactArgs(<%= len(Fields) %>), RunE: func(cmd *cobra.Command, args []string) (err error) { - <%= for (i, field) in Fields { %> <%= castArg("arg", field, i) %> + <%= for (i, field) in Fields { %> <%= field.CLIArgs("arg", i) %> <% } %> clientCtx, err := client.GetClientTxContext(cmd) if err != nil { @@ -51,7 +49,7 @@ func CmdUpdate<%= TypeName.UpperCamel %>() *cobra.Command { } <%= for (i, field) in Fields { %> - <%= castArg("arg", field, i+1) %> + <%= field.CLIArgs("arg", i+1) %> <% } %> clientCtx, err := client.GetClientTxContext(cmd) if err != nil { diff --git a/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush b/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush index f645a8e8a2..61f7c60227 100644 --- a/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush +++ b/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush @@ -19,7 +19,7 @@ func TestCreate<%= TypeName.UpperCamel %>(t *testing.T) { val := net.Validators[0] ctx := val.ClientCtx - fields := []string{<%= for (field) in Fields { %> "<%= genValidArg(field.DatatypeName) %>", <% } %>} + fields := []string{<%= for (field) in Fields { %> "<%= field.DefaultTestValue() %>", <% } %>} for _, tc := range []struct { desc string args []string @@ -47,7 +47,7 @@ func TestCreate<%= TypeName.UpperCamel %>(t *testing.T) { } else { require.NoError(t, err) var resp sdk.TxResponse - require.NoError(t, ctx.JSONCodec.UnmarshalJSON(out.Bytes(), &resp)) + require.NoError(t, ctx.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.Equal(t, tc.code, resp.Code) } }) @@ -60,7 +60,7 @@ func TestUpdate<%= TypeName.UpperCamel %>(t *testing.T) { val := net.Validators[0] ctx := val.ClientCtx - fields := []string{<%= for (field) in Fields { %> "<%= genValidArg(field.DatatypeName) %>", <% } %>} + fields := []string{<%= for (field) in Fields { %> "<%= field.DefaultTestValue() %>", <% } %>} common := []string{ fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), @@ -103,7 +103,7 @@ func TestUpdate<%= TypeName.UpperCamel %>(t *testing.T) { } else { require.NoError(t, err) var resp sdk.TxResponse - require.NoError(t, ctx.JSONCodec.UnmarshalJSON(out.Bytes(), &resp)) + require.NoError(t, ctx.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.Equal(t, tc.code, resp.Code) } }) @@ -116,7 +116,7 @@ func TestDelete<%= TypeName.UpperCamel %>(t *testing.T) { val := net.Validators[0] ctx := val.ClientCtx - fields := []string{<%= for (field) in Fields { %> "<%= genValidArg(field.DatatypeName) %>", <% } %>} + fields := []string{<%= for (field) in Fields { %> "<%= field.DefaultTestValue() %>", <% } %>} common := []string{ fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), @@ -156,7 +156,7 @@ func TestDelete<%= TypeName.UpperCamel %>(t *testing.T) { } else { require.NoError(t, err) var resp sdk.TxResponse - require.NoError(t, ctx.JSONCodec.UnmarshalJSON(out.Bytes(), &resp)) + require.NoError(t, ctx.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.Equal(t, tc.code, resp.Code) } }) diff --git a/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/simulation/{{typeName}}.go.plush b/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/simulation/{{typeName}}.go.plush new file mode 100644 index 0000000000..4d7fba6a24 --- /dev/null +++ b/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/simulation/{{typeName}}.go.plush @@ -0,0 +1,134 @@ +package simulation + +import ( + "math/rand" + + "<%= ModulePath %>/x/<%= ModuleName %>/keeper" + "<%= ModulePath %>/x/<%= ModuleName %>/types" + "github.com/cosmos/cosmos-sdk/baseapp" + simappparams "github.com/cosmos/cosmos-sdk/simapp/params" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +func SimulateMsgCreate<%= TypeName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + + msg := &types.MsgCreate<%= TypeName.UpperCamel %>{ + <%= MsgSigner.UpperCamel %>: simAccount.Address.String(), + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(), + AccountKeeper: ak, + Bankkeeper: bk, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +func SimulateMsgUpdate<%= TypeName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var ( + simAccount = simtypes.Account{} + <%= TypeName.LowerCamel %> = types.<%= TypeName.UpperCamel %>{} + msg = &types.MsgUpdate<%= TypeName.UpperCamel %>{} + all<%= TypeName.UpperCamel %> = k.GetAll<%= TypeName.UpperCamel %>(ctx) + found = false + ) + for _, obj := range all<%= TypeName.UpperCamel %> { + simAccount, found = FindAccount(accs, obj.<%= MsgSigner.UpperCamel %>) + if found { + <%= TypeName.LowerCamel %> = obj + break + } + } + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.LowerCamel %> <%= MsgSigner.LowerCamel %> not found"), nil, nil + } + msg.<%= MsgSigner.UpperCamel %> = simAccount.Address.String() + msg.Id = <%= TypeName.LowerCamel %>.Id + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(), + AccountKeeper: ak, + Bankkeeper: bk, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +func SimulateMsgDelete<%= TypeName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var ( + simAccount = simtypes.Account{} + <%= TypeName.LowerCamel %> = types.<%= TypeName.UpperCamel %>{} + msg = &types.MsgUpdate<%= TypeName.UpperCamel %>{} + all<%= TypeName.UpperCamel %> = k.GetAll<%= TypeName.UpperCamel %>(ctx) + found = false + ) + for _, obj := range all<%= TypeName.UpperCamel %> { + simAccount, found = FindAccount(accs, obj.<%= MsgSigner.UpperCamel %>) + if found { + <%= TypeName.LowerCamel %> = obj + break + } + } + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.LowerCamel %> <%= MsgSigner.LowerCamel %> not found"), nil, nil + } + msg.<%= MsgSigner.UpperCamel %> = simAccount.Address.String() + msg.Id = <%= TypeName.LowerCamel %>.Id + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(), + AccountKeeper: ak, + Bankkeeper: bk, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush b/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush index dd22b0df42..40abe4aad0 100644 --- a/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush +++ b/starport/templates/typed/list/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush @@ -5,9 +5,15 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) +const ( + TypeMsgCreate<%= TypeName.UpperCamel %> = "create_<%= TypeName.Snake %>" + TypeMsgUpdate<%= TypeName.UpperCamel %> = "update_<%= TypeName.Snake %>" + TypeMsgDelete<%= TypeName.UpperCamel %> = "delete_<%= TypeName.Snake %>" +) + var _ sdk.Msg = &MsgCreate<%= TypeName.UpperCamel %>{} -func NewMsgCreate<%= TypeName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.GetDatatype() %><% } %>) *MsgCreate<%= TypeName.UpperCamel %> { +func NewMsgCreate<%= TypeName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.DataType() %><% } %>) *MsgCreate<%= TypeName.UpperCamel %> { return &MsgCreate<%= TypeName.UpperCamel %>{ <%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>,<%= for (field) in Fields { %> <%= field.Name.UpperCamel %>: <%= field.Name.LowerCamel %>,<% } %> @@ -19,7 +25,7 @@ func (msg *MsgCreate<%= TypeName.UpperCamel %>) Route() string { } func (msg *MsgCreate<%= TypeName.UpperCamel %>) Type() string { - return "Create<%= TypeName.UpperCamel %>" + return TypeMsgCreate<%= TypeName.UpperCamel %> } func (msg *MsgCreate<%= TypeName.UpperCamel %>) GetSigners() []sdk.AccAddress { @@ -45,7 +51,7 @@ func (msg *MsgCreate<%= TypeName.UpperCamel %>) ValidateBasic() error { var _ sdk.Msg = &MsgUpdate<%= TypeName.UpperCamel %>{} -func NewMsgUpdate<%= TypeName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string, id uint64<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.GetDatatype() %><% } %>) *MsgUpdate<%= TypeName.UpperCamel %> { +func NewMsgUpdate<%= TypeName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string, id uint64<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.DataType() %><% } %>) *MsgUpdate<%= TypeName.UpperCamel %> { return &MsgUpdate<%= TypeName.UpperCamel%>{ Id: id, <%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>,<%= for (field) in Fields { %> @@ -58,7 +64,7 @@ func (msg *MsgUpdate<%= TypeName.UpperCamel %>) Route() string { } func (msg *MsgUpdate<%= TypeName.UpperCamel %>) Type() string { - return "Update<%= TypeName.UpperCamel %>" + return TypeMsgUpdate<%= TypeName.UpperCamel %> } func (msg *MsgUpdate<%= TypeName.UpperCamel %>) GetSigners() []sdk.AccAddress { @@ -95,7 +101,7 @@ func (msg *MsgDelete<%= TypeName.UpperCamel %>) Route() string { } func (msg *MsgDelete<%= TypeName.UpperCamel %>) Type() string { - return "Delete<%= TypeName.UpperCamel %>" + return TypeMsgDelete<%= TypeName.UpperCamel %> } func (msg *MsgDelete<%= TypeName.UpperCamel %>) GetSigners() []sdk.AccAddress { diff --git a/starport/templates/typed/map/simulation.go b/starport/templates/typed/map/simulation.go new file mode 100644 index 0000000000..18308d5982 --- /dev/null +++ b/starport/templates/typed/map/simulation.go @@ -0,0 +1,57 @@ +package maptype + +import ( + "fmt" + "path/filepath" + + "github.com/gobuffalo/genny" + "github.com/tendermint/starport/starport/pkg/placeholder" + "github.com/tendermint/starport/starport/templates/typed" +) + +func moduleSimulationModify(replacer placeholder.Replacer, opts *typed.Options) genny.RunFn { + return func(r *genny.Runner) error { + path := filepath.Join(opts.AppPath, "x", opts.ModuleName, "module_simulation.go") + f, err := r.Disk.Find(path) + if err != nil { + return err + } + + // Create a list of two different indexes and fields to use as sample + sampleIndexes := make([]string, 2) + for i := 0; i < 2; i++ { + sampleIndexes[i] = fmt.Sprintf("%s: sample.AccAddress(),\n", opts.MsgSigner.UpperCamel) + for _, index := range opts.Indexes { + sampleIndexes[i] += index.GenesisArgs(i) + } + } + + // simulation genesis state + templateGs := `%[2]vList: []types.%[2]v{ + { + %[3]v}, + { + %[4]v}, + }, + %[1]v` + replacementGs := fmt.Sprintf( + templateGs, + typed.PlaceholderSimappGenesisState, + opts.TypeName.UpperCamel, + sampleIndexes[0], + sampleIndexes[1], + ) + content := replacer.Replace(f.String(), typed.PlaceholderSimappGenesisState, replacementGs) + + content = typed.ModuleSimulationMsgModify( + replacer, + content, + opts.ModuleName, + opts.TypeName, + "Create", "Update", "Delete", + ) + + newFile := genny.NewFileS(path, content) + return r.File(newFile) + } +} diff --git a/starport/templates/typed/map/stargate.go b/starport/templates/typed/map/stargate.go index 2a26bc6b2d..272d7924bf 100644 --- a/starport/templates/typed/map/stargate.go +++ b/starport/templates/typed/map/stargate.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/gobuffalo/genny" - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/placeholder" "github.com/tendermint/starport/starport/pkg/xgenny" + "github.com/tendermint/starport/starport/templates/field/datatype" "github.com/tendermint/starport/starport/templates/module" "github.com/tendermint/starport/starport/templates/typed" ) @@ -34,7 +34,7 @@ func NewStargate(replacer placeholder.Replacer, opts *typed.Options) (*genny.Gen // because we can't generate reliable tests for this type var generateTest bool for _, index := range opts.Indexes { - if index.DatatypeName != field.TypeBool { + if index.DatatypeName != datatype.Bool { generateTest = true } } @@ -79,6 +79,7 @@ func NewStargate(replacer placeholder.Replacer, opts *typed.Options) (*genny.Gen g.RunFn(handlerModify(replacer, opts)) g.RunFn(clientCliTxModify(replacer, opts)) g.RunFn(typesCodecModify(replacer, opts)) + g.RunFn(moduleSimulationModify(replacer, opts)) if err := typed.Box(messagesTemplate, opts, g); err != nil { return nil, err @@ -120,30 +121,31 @@ func protoRPCModify(replacer placeholder.Replacer, opts *typed.Options) genny.Ru replacementGogoImport := typed.EnsureGogoProtoImported(path, typed.Placeholder) content = replacer.Replace(content, typed.Placeholder, replacementGogoImport) - var lowerCamelIndexes []string + var protoIndexes []string for _, index := range opts.Indexes { - lowerCamelIndexes = append(lowerCamelIndexes, fmt.Sprintf("{%s}", index.Name.LowerCamel)) + protoIndexes = append(protoIndexes, fmt.Sprintf("{%s}", index.ProtoFieldName())) } - indexPath := strings.Join(lowerCamelIndexes, "/") + indexPath := strings.Join(protoIndexes, "/") // Add the service - templateService := `// Queries a %[3]v by index. + templateService := `// Queries a %[2]v by index. rpc %[2]v(QueryGet%[2]vRequest) returns (QueryGet%[2]vResponse) { - option (google.api.http).get = "/%[4]v/%[5]v/%[6]v/%[3]v/%[7]v"; + option (google.api.http).get = "/%[3]v/%[4]v/%[5]v/%[6]v/%[7]v"; } - // Queries a list of %[3]v items. + // Queries a list of %[2]v items. rpc %[2]vAll(QueryAll%[2]vRequest) returns (QueryAll%[2]vResponse) { - option (google.api.http).get = "/%[4]v/%[5]v/%[6]v/%[3]v"; + option (google.api.http).get = "/%[3]v/%[4]v/%[5]v/%[6]v"; } %[1]v` - replacementService := fmt.Sprintf(templateService, typed.Placeholder2, + replacementService := fmt.Sprintf(templateService, + typed.Placeholder2, opts.TypeName.UpperCamel, - opts.TypeName.LowerCamel, opts.OwnerName, opts.AppName, opts.ModuleName, + opts.TypeName.Snake, indexPath, ) content = replacer.Replace(content, typed.Placeholder2, replacementService) @@ -151,22 +153,22 @@ func protoRPCModify(replacer placeholder.Replacer, opts *typed.Options) genny.Ru // Add the service messages var queryIndexFields string for i, index := range opts.Indexes { - queryIndexFields += fmt.Sprintf( - " %s %s = %d;\n", - index.Datatype, - index.Name.LowerCamel, - i+1, - ) + queryIndexFields += fmt.Sprintf(" %s;\n", index.ProtoType(i+1)) } // Ensure custom types are imported - for _, f := range opts.Indexes.Custom() { + protoImports := opts.Fields.ProtoImports() + for _, f := range opts.Fields.Custom() { + protoImports = append(protoImports, + fmt.Sprintf("%[1]v/%[2]v.proto", opts.ModuleName, f), + ) + } + for _, f := range protoImports { importModule := fmt.Sprintf(` -import "%[1]v/%[2]v.proto";`, opts.ModuleName, f) +import "%[1]v";`, f) content = strings.ReplaceAll(content, importModule, "") - - replacementImport := fmt.Sprintf("%[1]v%[2]v", typed.PlaceholderProtoTxImport, importModule) - content = replacer.Replace(content, typed.PlaceholderProtoTxImport, replacementImport) + replacementImport := fmt.Sprintf("%[1]v%[2]v", typed.Placeholder, importModule) + content = replacer.Replace(content, typed.Placeholder, replacementImport) } templateMessage := `message QueryGet%[2]vRequest { @@ -383,24 +385,17 @@ func genesisTestsModify(replacer placeholder.Replacer, opts *typed.Options) genn sampleIndexes := make([]string, 2) for i := 0; i < 2; i++ { for _, index := range opts.Indexes { - switch index.DatatypeName { - case field.TypeString: - sampleIndexes[i] += fmt.Sprintf("%s: \"%d\",\n", index.Name.UpperCamel, i) - case field.TypeInt, field.TypeUint: - sampleIndexes[i] += fmt.Sprintf("%s: %d,\n", index.Name.UpperCamel, i) - case field.TypeBool: - sampleIndexes[i] += fmt.Sprintf("%s: %t,\n", index.Name.UpperCamel, i%2 == 0) - } + sampleIndexes[i] += index.GenesisArgs(i) } } templateState := `%[2]vList: []types.%[2]v{ - { - %[3]v}, - { - %[4]v}, -}, -%[1]v` + { + %[3]v}, + { + %[4]v}, + }, + %[1]v` replacementState := fmt.Sprintf( templateState, module.PlaceholderGenesisTestState, @@ -410,8 +405,7 @@ func genesisTestsModify(replacer placeholder.Replacer, opts *typed.Options) genn ) content := replacer.Replace(f.String(), module.PlaceholderGenesisTestState, replacementState) - templateAssert := `require.Len(t, got.%[2]vList, len(genesisState.%[2]vList)) -require.Subset(t, genesisState.%[2]vList, got.%[2]vList) + templateAssert := `require.ElementsMatch(t, genesisState.%[2]vList, got.%[2]vList) %[1]v` replacementTests := fmt.Sprintf( templateAssert, @@ -437,14 +431,7 @@ func genesisTypesTestsModify(replacer placeholder.Replacer, opts *typed.Options) sampleIndexes := make([]string, 2) for i := 0; i < 2; i++ { for _, index := range opts.Indexes { - switch index.DatatypeName { - case field.TypeString: - sampleIndexes[i] += fmt.Sprintf("%s: \"%d\",\n", index.Name.UpperCamel, i) - case field.TypeInt, field.TypeUint: - sampleIndexes[i] += fmt.Sprintf("%s: %d,\n", index.Name.UpperCamel, i) - case field.TypeBool: - sampleIndexes[i] += fmt.Sprintf("%s: %t,\n", index.Name.UpperCamel, i != 0) - } + sampleIndexes[i] += index.GenesisArgs(i) } } @@ -522,29 +509,25 @@ func protoTxModify(replacer placeholder.Replacer, opts *typed.Options) genny.Run // Messages var indexes string for i, index := range opts.Indexes { - indexes += fmt.Sprintf( - " %s %s = %d;\n", - index.Datatype, - index.Name.LowerCamel, - i+2, - ) + indexes += fmt.Sprintf(" %s;\n", index.ProtoType(i+2)) } var fields string - for i, field := range opts.Fields { - fields += fmt.Sprintf( - " %s %s = %d;\n", - field.Datatype, - field.Name.LowerCamel, - i+2+len(opts.Indexes), - ) + for i, f := range opts.Fields { + fields += fmt.Sprintf(" %s;\n", f.ProtoType(i+2+len(opts.Indexes))) } // Ensure custom types are imported + protoImports := append(opts.Fields.ProtoImports(), opts.Indexes.ProtoImports()...) customFields := append(opts.Fields.Custom(), opts.Indexes.Custom()...) for _, f := range customFields { + protoImports = append(protoImports, + fmt.Sprintf("%[1]v/%[2]v.proto", opts.ModuleName, f), + ) + } + for _, f := range protoImports { importModule := fmt.Sprintf(` -import "%[1]v/%[2]v.proto";`, opts.ModuleName, f) +import "%[1]v";`, f) content = strings.ReplaceAll(content, importModule, "") replacementImport := fmt.Sprintf("%[1]v%[2]v", typed.PlaceholderProtoTxImport, importModule) diff --git a/starport/templates/typed/map/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush b/starport/templates/typed/map/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush index 1f664b6d8c..8e34e7e04a 100644 --- a/starport/templates/typed/map/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush +++ b/starport/templates/typed/map/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush @@ -1,13 +1,13 @@ syntax = "proto3"; package <%= formatOwnerName(OwnerName) %>.<%= AppName %>.<%= ModuleName %>; -option go_package = "<%= ModulePath %>/x/<%= ModuleName %>/types"; -<%= for (i, importName) in Indexes.Custom() { %>import "<%= ModuleName %>/<%= importName %>.proto"; <% } %> -<%= for (i, importName) in Fields.Custom() { %>import "<%= ModuleName %>/<%= importName %>.proto"; <% } %> +option go_package = "<%= ModulePath %>/x/<%= ModuleName %>/types";<%= for (importName) in mergeCustomImports(Fields, Indexes) { %> +import "<%= ModuleName %>/<%= importName %>.proto"; <% } %><%= for (importName) in mergeProtoImports(Fields, Indexes) { %> +import "<%= importName %>"; <% } %> message <%= TypeName.UpperCamel %> {<%= for (i, index) in Indexes { %> - <%= index.GetProtoDatatype() %> <%= index.Name.LowerCamel %> = <%= i+1 %>; <% } %><%= for (i, field) in Fields { %> - <%= field.GetProtoDatatype() %> <%= field.Name.LowerCamel %> = <%= i+1+len(Indexes) %>; <% } %> + <%= index.ProtoType(i+1) %>; <% } %><%= for (i, field) in Fields { %> + <%= field.ProtoType(i+1+len(Indexes)) %>; <% } %> <%= if (!NoMessage) { %>string <%= MsgSigner.LowerCamel %> = <%= len(Fields)+len(Indexes)+1 %>;<% } %> } diff --git a/starport/templates/typed/map/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}.go.plush b/starport/templates/typed/map/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}.go.plush index 82f54d6fc5..96afb9b5bb 100644 --- a/starport/templates/typed/map/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}.go.plush +++ b/starport/templates/typed/map/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}.go.plush @@ -2,11 +2,9 @@ package cli import ( "context" - <%= if (Indexes.IsComplex()) { %> "encoding/json" <% } %> - - <%= if (Indexes.NeedCastImport()) { %> "github.com/spf13/cast" <% } %> + <%= for (goImport) in mergeGoImports(Indexes) { %> + <%= goImport.Alias %> "<%= goImport.Name %>"<% } %> "github.com/spf13/cobra" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "<%= ModulePath %>/x/<%= ModuleName %>/types" @@ -55,7 +53,7 @@ func CmdShow<%= TypeName.UpperCamel %>() *cobra.Command { queryClient := types.NewQueryClient(clientCtx) - <%= for (i, index) in Indexes { %> <%= castArg("arg", index, i) %> + <%= for (i, field) in Indexes { %> <%= field.CLIArgs("arg", i) %> <% } %> params := &types.QueryGet<%= TypeName.UpperCamel %>Request{ <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: arg<%= index.Name.UpperCamel %>, diff --git a/starport/templates/typed/map/stargate/component/x/{{moduleName}}/keeper/{{typeName}}.go.plush b/starport/templates/typed/map/stargate/component/x/{{moduleName}}/keeper/{{typeName}}.go.plush index 8ce004f014..c55424bd18 100644 --- a/starport/templates/typed/map/stargate/component/x/{{moduleName}}/keeper/{{typeName}}.go.plush +++ b/starport/templates/typed/map/stargate/component/x/{{moduleName}}/keeper/{{typeName}}.go.plush @@ -18,7 +18,7 @@ func (k Keeper) Set<%= TypeName.UpperCamel %>(ctx sdk.Context, <%= TypeName.Lowe // Get<%= TypeName.UpperCamel %> returns a <%= TypeName.LowerCamel %> from its index func (k Keeper) Get<%= TypeName.UpperCamel %>( ctx sdk.Context, - <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.GetDatatype() %>, + <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.DataType() %>, <% } %> ) (val types.<%= TypeName.UpperCamel %>, found bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.<%= TypeName.UpperCamel %>KeyPrefix)) @@ -37,7 +37,7 @@ func (k Keeper) Get<%= TypeName.UpperCamel %>( // Remove<%= TypeName.UpperCamel %> removes a <%= TypeName.LowerCamel %> from the store func (k Keeper) Remove<%= TypeName.UpperCamel %>( ctx sdk.Context, - <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.GetDatatype() %>, + <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.DataType() %>, <% } %> ) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.<%= TypeName.UpperCamel %>KeyPrefix)) diff --git a/starport/templates/typed/map/stargate/component/x/{{moduleName}}/types/key_{{typeName}}.go.plush b/starport/templates/typed/map/stargate/component/x/{{moduleName}}/types/key_{{typeName}}.go.plush index f3a13fb593..36e0b5b619 100644 --- a/starport/templates/typed/map/stargate/component/x/{{moduleName}}/types/key_{{typeName}}.go.plush +++ b/starport/templates/typed/map/stargate/component/x/{{moduleName}}/types/key_{{typeName}}.go.plush @@ -11,11 +11,11 @@ const ( // <%= TypeName.UpperCamel %>Key returns the store key to retrieve a <%= TypeName.UpperCamel %> from the index fields func <%= TypeName.UpperCamel %>Key( -<%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.GetDatatype() %>, +<%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.DataType() %>, <% } %>) []byte { var key []byte <%= for (i, index) in Indexes { %> - <%= castToBytes(index.Name.LowerCamel, index.DatatypeName) %> + <%= index.ToBytes(index.Name.LowerCamel) %> key = append(key, <%= index.Name.LowerCamel %>Bytes...) key = append(key, []byte("/")...) <% } %> diff --git a/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush b/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush index 683270a5c1..94feb6425f 100644 --- a/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush +++ b/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush @@ -1,11 +1,9 @@ package cli import ( - <%= if (Indexes.IsComplex() || Fields.IsComplex()) { %> "encoding/json" <% } %> - + <%= for (goImport) in mergeGoImports(Indexes, Fields) { %> + <%= goImport.Alias %> "<%= goImport.Name %>"<% } %> "github.com/spf13/cobra" - <%= if (Indexes.NeedCastImport() || Fields.NeedCastImport()) { %> "github.com/spf13/cast" <% } %> - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -19,10 +17,10 @@ func CmdCreate<%= TypeName.UpperCamel %>() *cobra.Command { Args: cobra.ExactArgs(<%= len(Fields) + len(Indexes) %>), RunE: func(cmd *cobra.Command, args []string) (err error) { // Get indexes - <%= for (i, index) in Indexes { %> <%= castArg("index", index, i) %> + <%= for (i, field) in Indexes { %> <%= field.CLIArgs("index", i) %> <% } %> // Get value arguments - <%= for (i, field) in Fields { %> <%= castArg("arg", field, i+len(Indexes)) %> + <%= for (i, field) in Fields { %> <%= field.CLIArgs("arg", i+len(Indexes)) %> <% } %> clientCtx, err := client.GetClientTxContext(cmd) if err != nil { @@ -53,10 +51,10 @@ func CmdUpdate<%= TypeName.UpperCamel %>() *cobra.Command { Args: cobra.ExactArgs(<%= len(Fields) + len(Indexes) %>), RunE: func(cmd *cobra.Command, args []string) (err error) { // Get indexes - <%= for (i, index) in Indexes { %> <%= castArg("index", index, i) %> + <%= for (i, field) in Indexes { %> <%= field.CLIArgs("index", i) %> <% } %> // Get value arguments - <%= for (i, field) in Fields { %> <%= castArg("arg", field, i+len(Indexes)) %> + <%= for (i, field) in Fields { %> <%= field.CLIArgs("arg", i+len(Indexes)) %> <% } %> clientCtx, err := client.GetClientTxContext(cmd) if err != nil { @@ -86,7 +84,7 @@ func CmdDelete<%= TypeName.UpperCamel %>() *cobra.Command { Short: "Delete a <%= TypeName.Original %>", Args: cobra.ExactArgs(<%= len(Indexes) %>), RunE: func(cmd *cobra.Command, args []string) (err error) { - <%= for (i, index) in Indexes { %> <%= castArg("index", index, i) %> + <%= for (i, field) in Indexes { %> <%= field.CLIArgs("index", i) %> <% } %> clientCtx, err := client.GetClientTxContext(cmd) if err != nil { diff --git a/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/simulation/{{typeName}}.go.plush b/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/simulation/{{typeName}}.go.plush new file mode 100644 index 0000000000..72e60e65d1 --- /dev/null +++ b/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/simulation/{{typeName}}.go.plush @@ -0,0 +1,147 @@ +package simulation + +import ( + "math/rand" + "strconv" + + "<%= ModulePath %>/x/<%= ModuleName %>/keeper" + "<%= ModulePath %>/x/<%= ModuleName %>/types" + "github.com/cosmos/cosmos-sdk/baseapp" + simappparams "github.com/cosmos/cosmos-sdk/simapp/params" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +// Prevent strconv unused error +var _ = strconv.IntSize + +func SimulateMsgCreate<%= TypeName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + + i := r.Int() + msg := &types.MsgCreate<%= TypeName.UpperCamel %>{ + <%= MsgSigner.UpperCamel %>: simAccount.Address.String(),<%= for (i, index) in Indexes { %> + <%= index.Name.UpperCamel %>: <%= index.ValueLoop() %>,<% } %> + } + + _, found := k.Get<%= TypeName.UpperCamel %>(ctx <%= for (index) in Indexes { %>, msg.<%= index.Name.UpperCamel %><% } %>) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.UpperCamel %> already exist"), nil, nil + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(), + AccountKeeper: ak, + Bankkeeper: bk, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +func SimulateMsgUpdate<%= TypeName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var ( + simAccount = simtypes.Account{} + <%= TypeName.LowerCamel %> = types.<%= TypeName.UpperCamel %>{} + msg = &types.MsgUpdate<%= TypeName.UpperCamel %>{} + all<%= TypeName.UpperCamel %> = k.GetAll<%= TypeName.UpperCamel %>(ctx) + found = false + ) + for _, obj := range all<%= TypeName.UpperCamel %> { + simAccount, found = FindAccount(accs, obj.<%= MsgSigner.UpperCamel %>) + if found { + <%= TypeName.LowerCamel %> = obj + break + } + } + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.LowerCamel %> <%= MsgSigner.LowerCamel %> not found"), nil, nil + } + msg.<%= MsgSigner.UpperCamel %> = simAccount.Address.String() + <%= for (i, index) in Indexes { %> + msg.<%= index.Name.UpperCamel %> = <%= TypeName.LowerCamel %>.<%= index.Name.UpperCamel %><% } %> + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(), + AccountKeeper: ak, + Bankkeeper: bk, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +func SimulateMsgDelete<%= TypeName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var ( + simAccount = simtypes.Account{} + <%= TypeName.LowerCamel %> = types.<%= TypeName.UpperCamel %>{} + msg = &types.MsgUpdate<%= TypeName.UpperCamel %>{} + all<%= TypeName.UpperCamel %> = k.GetAll<%= TypeName.UpperCamel %>(ctx) + found = false + ) + for _, obj := range all<%= TypeName.UpperCamel %> { + simAccount, found = FindAccount(accs, obj.<%= MsgSigner.UpperCamel %>) + if found { + <%= TypeName.LowerCamel %> = obj + break + } + } + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.LowerCamel %> <%= MsgSigner.LowerCamel %> not found"), nil, nil + } + msg.<%= MsgSigner.UpperCamel %> = simAccount.Address.String() + <%= for (i, index) in Indexes { %> + msg.<%= index.Name.UpperCamel %> = <%= TypeName.LowerCamel %>.<%= index.Name.UpperCamel %><% } %> + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(), + AccountKeeper: ak, + Bankkeeper: bk, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush b/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush index 2ecc45f7c7..4473f011a7 100644 --- a/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush +++ b/starport/templates/typed/map/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush @@ -5,12 +5,18 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) +const ( + TypeMsgCreate<%= TypeName.UpperCamel %> = "create_<%= TypeName.Snake %>" + TypeMsgUpdate<%= TypeName.UpperCamel %> = "update_<%= TypeName.Snake %>" + TypeMsgDelete<%= TypeName.UpperCamel %> = "delete_<%= TypeName.Snake %>" +) + var _ sdk.Msg = &MsgCreate<%= TypeName.UpperCamel %>{} func NewMsgCreate<%= TypeName.UpperCamel %>( <%= MsgSigner.LowerCamel %> string, - <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.GetDatatype() %>, - <% } %><%= for (field) in Fields { %><%= field.Name.LowerCamel %> <%= field.GetDatatype() %>, + <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.DataType() %>, + <% } %><%= for (field) in Fields { %><%= field.Name.LowerCamel %> <%= field.DataType() %>, <% } %> ) *MsgCreate<%= TypeName.UpperCamel %> { return &MsgCreate<%= TypeName.UpperCamel %>{ @@ -26,7 +32,7 @@ func (msg *MsgCreate<%= TypeName.UpperCamel %>) Route() string { } func (msg *MsgCreate<%= TypeName.UpperCamel %>) Type() string { - return "Create<%= TypeName.UpperCamel %>" + return TypeMsgCreate<%= TypeName.UpperCamel %> } func (msg *MsgCreate<%= TypeName.UpperCamel %>) GetSigners() []sdk.AccAddress { @@ -54,8 +60,8 @@ var _ sdk.Msg = &MsgUpdate<%= TypeName.UpperCamel %>{} func NewMsgUpdate<%= TypeName.UpperCamel %>( <%= MsgSigner.LowerCamel %> string, - <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.GetDatatype() %>, - <% } %><%= for (field) in Fields { %><%= field.Name.LowerCamel %> <%= field.GetDatatype() %>, + <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.DataType() %>, + <% } %><%= for (field) in Fields { %><%= field.Name.LowerCamel %> <%= field.DataType() %>, <% } %> ) *MsgUpdate<%= TypeName.UpperCamel %> { return &MsgUpdate<%= TypeName.UpperCamel %>{ @@ -71,7 +77,7 @@ func (msg *MsgUpdate<%= TypeName.UpperCamel %>) Route() string { } func (msg *MsgUpdate<%= TypeName.UpperCamel %>) Type() string { - return "Update<%= TypeName.UpperCamel %>" + return TypeMsgUpdate<%= TypeName.UpperCamel %> } func (msg *MsgUpdate<%= TypeName.UpperCamel %>) GetSigners() []sdk.AccAddress { @@ -99,7 +105,7 @@ var _ sdk.Msg = &MsgDelete<%= TypeName.UpperCamel %>{} func NewMsgDelete<%= TypeName.UpperCamel %>( <%= MsgSigner.LowerCamel %> string, - <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.GetDatatype() %>, + <%= for (i, index) in Indexes { %><%= index.Name.LowerCamel %> <%= index.DataType() %>, <% } %> ) *MsgDelete<%= TypeName.UpperCamel %> { return &MsgDelete<%= TypeName.UpperCamel %>{ @@ -113,7 +119,7 @@ func (msg *MsgDelete<%= TypeName.UpperCamel %>) Route() string { } func (msg *MsgDelete<%= TypeName.UpperCamel %>) Type() string { - return "Delete<%= TypeName.UpperCamel %>" + return TypeMsgDelete<%= TypeName.UpperCamel %> } func (msg *MsgDelete<%= TypeName.UpperCamel %>) GetSigners() []sdk.AccAddress { diff --git a/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush b/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush index 3cb4a2ca76..ad182969c3 100644 --- a/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush +++ b/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush @@ -13,6 +13,7 @@ import ( "google.golang.org/grpc/status" "<%= ModulePath %>/testutil/network" + "<%= ModulePath %>/testutil/nullify" "<%= ModulePath %>/x/<%= ModuleName %>/client/cli" "<%= ModulePath %>/x/<%= ModuleName %>/types" ) @@ -27,10 +28,12 @@ func networkWith<%= TypeName.UpperCamel %>Objects(t *testing.T, n int) (*network require.NoError(t, cfg.Codec.UnmarshalJSON(cfg.GenesisState[types.ModuleName], &state)) for i := 0; i < n; i++ { - state.<%= TypeName.UpperCamel %>List = append(state.<%= TypeName.UpperCamel %>List, types.<%= TypeName.UpperCamel %>{ - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genUniqueArg(index.DatatypeName) %>, - <% } %> - }) + <%= TypeName.LowerCamel %> := types.<%= TypeName.UpperCamel %>{ + <%= for (index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueLoop() %>, + <% } %> + } + nullify.Fill(&<%= TypeName.LowerCamel %>) + state.<%= TypeName.UpperCamel %>List = append(state.<%= TypeName.UpperCamel %>List, <%= TypeName.LowerCamel %>) } buf, err := cfg.Codec.MarshalJSON(&state) require.NoError(t, err) @@ -47,7 +50,7 @@ func TestShow<%= TypeName.UpperCamel %>(t *testing.T) { } for _, tc := range []struct { desc string - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %> <%= index.GetDatatype() %> + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %> <%= index.DataType() %> <% } %> args []string err error @@ -62,7 +65,7 @@ func TestShow<%= TypeName.UpperCamel %>(t *testing.T) { }, { desc: "not found", - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= genNotFoundIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= index.ValueInvalidIndex() %>, <% } %> args: common, err: status.Error(codes.InvalidArgument, "not found"), @@ -71,7 +74,7 @@ func TestShow<%= TypeName.UpperCamel %>(t *testing.T) { tc := tc t.Run(tc.desc, func(t *testing.T) { args := []string{ - <%= for (i, index) in Indexes { %><%= castToString("tc.id" + index.Name.UpperCamel, index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.ToString("tc.id" + index.Name.UpperCamel) %>, <% } %> } args = append(args, tc.args...) @@ -85,7 +88,10 @@ func TestShow<%= TypeName.UpperCamel %>(t *testing.T) { var resp types.QueryGet<%= TypeName.UpperCamel %>Response require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.NotNil(t, resp.<%= TypeName.UpperCamel %>) - require.Equal(t, tc.obj, resp.<%= TypeName.UpperCamel %>) + require.Equal(t, + nullify.Fill(&tc.obj), + nullify.Fill(&resp.<%= TypeName.UpperCamel %>), + ) } }) } @@ -119,7 +125,10 @@ func TestList<%= TypeName.UpperCamel %>(t *testing.T) { var resp types.QueryAll<%= TypeName.UpperCamel %>Response require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.LessOrEqual(t, len(resp.<%= TypeName.UpperCamel %>), step) - require.Subset(t, objs, resp.<%= TypeName.UpperCamel %>) + require.Subset(t, + nullify.Fill(objs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) } }) t.Run("ByKey", func(t *testing.T) { @@ -132,7 +141,10 @@ func TestList<%= TypeName.UpperCamel %>(t *testing.T) { var resp types.QueryAll<%= TypeName.UpperCamel %>Response require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.LessOrEqual(t, len(resp.<%= TypeName.UpperCamel %>), step) - require.Subset(t, objs, resp.<%= TypeName.UpperCamel %>) + require.Subset(t, + nullify.Fill(objs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) next = resp.Pagination.NextKey } }) @@ -144,6 +156,9 @@ func TestList<%= TypeName.UpperCamel %>(t *testing.T) { require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.NoError(t, err) require.Equal(t, len(objs), int(resp.Pagination.Total)) - require.Equal(t, objs, resp.<%= TypeName.UpperCamel %>) + require.ElementsMatch(t, + nullify.Fill(objs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) }) } diff --git a/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush b/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush index 8b407660fa..af78e9e90a 100644 --- a/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush +++ b/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush @@ -11,6 +11,7 @@ import ( "google.golang.org/grpc/status" "<%= ModulePath %>/x/<%= ModuleName %>/types" + "<%= ModulePath %>/testutil/nullify" keepertest "<%= ModulePath %>/testutil/keeper" ) @@ -46,7 +47,7 @@ func Test<%= TypeName.UpperCamel %>QuerySingle(t *testing.T) { { desc: "KeyNotFound", request: &types.QueryGet<%= TypeName.UpperCamel %>Request{ - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>:<%= genNotFoundIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>:<%= index.ValueInvalidIndex() %>, <% } %> }, err: status.Error(codes.InvalidArgument, "not found"), @@ -61,7 +62,11 @@ func Test<%= TypeName.UpperCamel %>QuerySingle(t *testing.T) { if tc.err != nil { require.ErrorIs(t, err, tc.err) } else { - require.Equal(t, tc.response, response) + require.NoError(t, err) + require.Equal(t, + nullify.Fill(tc.response), + nullify.Fill(response), + ) } }) } @@ -88,7 +93,10 @@ func Test<%= TypeName.UpperCamel %>QueryPaginated(t *testing.T) { resp, err := keeper.<%= TypeName.UpperCamel %>All(wctx, request(nil, uint64(i), uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.<%= TypeName.UpperCamel %>), step) - require.Subset(t, msgs, resp.<%= TypeName.UpperCamel %>) + require.Subset(t, + nullify.Fill(msgs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) } }) t.Run("ByKey", func(t *testing.T) { @@ -98,7 +106,10 @@ func Test<%= TypeName.UpperCamel %>QueryPaginated(t *testing.T) { resp, err := keeper.<%= TypeName.UpperCamel %>All(wctx, request(next, 0, uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.<%= TypeName.UpperCamel %>), step) - require.Subset(t, msgs, resp.<%= TypeName.UpperCamel %>) + require.Subset(t, + nullify.Fill(msgs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) next = resp.Pagination.NextKey } }) @@ -106,6 +117,10 @@ func Test<%= TypeName.UpperCamel %>QueryPaginated(t *testing.T) { resp, err := keeper.<%= TypeName.UpperCamel %>All(wctx, request(nil, 0, 0, true)) require.NoError(t, err) require.Equal(t, len(msgs), int(resp.Pagination.Total)) + require.ElementsMatch(t, + nullify.Fill(msgs), + nullify.Fill(resp.<%= TypeName.UpperCamel %>), + ) }) t.Run("InvalidRequest", func(t *testing.T) { _, err := keeper.<%= TypeName.UpperCamel %>All(wctx, nil) diff --git a/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush b/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush index abda55d7d6..9b56ca574d 100644 --- a/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush +++ b/starport/templates/typed/map/stargate/tests/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush @@ -4,9 +4,10 @@ import ( "strconv" "testing" - keepertest "<%= ModulePath %>/testutil/keeper" "<%= ModulePath %>/x/<%= ModuleName %>/keeper" "<%= ModulePath %>/x/<%= ModuleName %>/types" + keepertest "<%= ModulePath %>/testutil/keeper" + "<%= ModulePath %>/testutil/nullify" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" ) @@ -17,7 +18,7 @@ var _ = strconv.IntSize func createN<%= TypeName.UpperCamel %>(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.<%= TypeName.UpperCamel %> { items := make([]types.<%= TypeName.UpperCamel %>, n) for i := range items { - <%= for (i, index) in Indexes { %>items[i].<%= index.Name.UpperCamel %> = <%= genUniqueArg(index.DatatypeName) %> + <%= for (i, index) in Indexes { %>items[i].<%= index.Name.UpperCamel %> = <%= index.ValueLoop() %> <% } %> keeper.Set<%= TypeName.UpperCamel %>(ctx, items[i]) } @@ -33,7 +34,10 @@ func Test<%= TypeName.UpperCamel %>Get(t *testing.T) { <% } %> ) require.True(t, found) - require.Equal(t, item, rst) + require.Equal(t, + nullify.Fill(&item), + nullify.Fill(&rst), + ) } } func Test<%= TypeName.UpperCamel %>Remove(t *testing.T) { @@ -55,5 +59,8 @@ func Test<%= TypeName.UpperCamel %>Remove(t *testing.T) { func Test<%= TypeName.UpperCamel %>GetAll(t *testing.T) { keeper, ctx := keepertest.<%= title(ModuleName) %>Keeper(t) items := createN<%= TypeName.UpperCamel %>(keeper, ctx, 10) - require.ElementsMatch(t, items, keeper.GetAll<%= TypeName.UpperCamel %>(ctx)) + require.ElementsMatch(t, + nullify.Fill(items), + nullify.Fill(keeper.GetAll<%= TypeName.UpperCamel %>(ctx)), + ) } diff --git a/starport/templates/typed/map/stargate/tests/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush b/starport/templates/typed/map/stargate/tests/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush index b7a79fbc51..0a3ceb1021 100644 --- a/starport/templates/typed/map/stargate/tests/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush +++ b/starport/templates/typed/map/stargate/tests/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush @@ -23,17 +23,17 @@ func TestCreate<%= TypeName.UpperCamel %>(t *testing.T) { val := net.Validators[0] ctx := val.ClientCtx - fields := []string{<%= for (field) in Fields { %> "<%= genValidArg(field.DatatypeName) %>", <% } %>} + fields := []string{<%= for (field) in Fields { %> "<%= field.DefaultTestValue() %>", <% } %>} for _, tc := range []struct { desc string - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %> <%= index.GetDatatype() %> + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %> <%= index.DataType() %> <% } %> args []string err error code uint32 }{ { - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= genValidIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= index.ValueIndex() %>, <% } %> desc: "valid", args: []string{ @@ -47,7 +47,7 @@ func TestCreate<%= TypeName.UpperCamel %>(t *testing.T) { tc := tc t.Run(tc.desc, func(t *testing.T) { args := []string{ - <%= for (i, index) in Indexes { %><%= castToString("tc.id" + index.Name.UpperCamel, index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.ToString("tc.id" + index.Name.UpperCamel) %>, <% } %> } args = append(args, fields...) @@ -58,7 +58,7 @@ func TestCreate<%= TypeName.UpperCamel %>(t *testing.T) { } else { require.NoError(t, err) var resp sdk.TxResponse - require.NoError(t, ctx.JSONCodec.UnmarshalJSON(out.Bytes(), &resp)) + require.NoError(t, ctx.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.Equal(t, tc.code, resp.Code) } }) @@ -70,7 +70,7 @@ func TestUpdate<%= TypeName.UpperCamel %>(t *testing.T) { val := net.Validators[0] ctx := val.ClientCtx - fields := []string{<%= for (field) in Fields { %> "<%= genValidArg(field.DatatypeName) %>", <% } %>} + fields := []string{<%= for (field) in Fields { %> "<%= field.DefaultTestValue() %>", <% } %>} common := []string{ fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), @@ -88,7 +88,7 @@ func TestUpdate<%= TypeName.UpperCamel %>(t *testing.T) { for _, tc := range []struct { desc string - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %> <%= index.GetDatatype() %> + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %> <%= index.DataType() %> <% } %> args []string code uint32 @@ -96,13 +96,13 @@ func TestUpdate<%= TypeName.UpperCamel %>(t *testing.T) { }{ { desc: "valid", - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= genValidIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= index.ValueIndex() %>, <% } %> args: common, }, { desc: "key not found", - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= genNotFoundIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= index.ValueInvalidIndex() %>, <% } %> args: common, code: sdkerrors.ErrKeyNotFound.ABCICode(), @@ -111,7 +111,7 @@ func TestUpdate<%= TypeName.UpperCamel %>(t *testing.T) { tc := tc t.Run(tc.desc, func(t *testing.T) { args := []string{ - <%= for (i, index) in Indexes { %><%= castToString("tc.id" + index.Name.UpperCamel, index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.ToString("tc.id" + index.Name.UpperCamel) %>, <% } %> } args = append(args, fields...) @@ -122,7 +122,7 @@ func TestUpdate<%= TypeName.UpperCamel %>(t *testing.T) { } else { require.NoError(t, err) var resp sdk.TxResponse - require.NoError(t, ctx.JSONCodec.UnmarshalJSON(out.Bytes(), &resp)) + require.NoError(t, ctx.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.Equal(t, tc.code, resp.Code) } }) @@ -135,7 +135,7 @@ func TestDelete<%= TypeName.UpperCamel %>(t *testing.T) { val := net.Validators[0] ctx := val.ClientCtx - fields := []string{<%= for (field) in Fields { %> "<%= genValidArg(field.DatatypeName) %>", <% } %>} + fields := []string{<%= for (field) in Fields { %> "<%= field.DefaultTestValue() %>", <% } %>} common := []string{ fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), @@ -153,7 +153,7 @@ func TestDelete<%= TypeName.UpperCamel %>(t *testing.T) { for _, tc := range []struct { desc string - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %> <%= index.GetDatatype() %> + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %> <%= index.DataType() %> <% } %> args []string code uint32 @@ -161,13 +161,13 @@ func TestDelete<%= TypeName.UpperCamel %>(t *testing.T) { }{ { desc: "valid", - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= genValidIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= index.ValueIndex() %>, <% } %> args: common, }, { desc: "key not found", - <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= genNotFoundIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %>id<%= index.Name.UpperCamel %>: <%= index.ValueInvalidIndex() %>, <% } %> args: common, code: sdkerrors.ErrKeyNotFound.ABCICode(), @@ -176,7 +176,7 @@ func TestDelete<%= TypeName.UpperCamel %>(t *testing.T) { tc := tc t.Run(tc.desc, func(t *testing.T) { args := []string{ - <%= for (i, index) in Indexes { %><%= castToString("tc.id" + index.Name.UpperCamel, index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.ToString("tc.id" + index.Name.UpperCamel) %>, <% } %> } args = append(args, tc.args...) @@ -186,7 +186,7 @@ func TestDelete<%= TypeName.UpperCamel %>(t *testing.T) { } else { require.NoError(t, err) var resp sdk.TxResponse - require.NoError(t, ctx.JSONCodec.UnmarshalJSON(out.Bytes(), &resp)) + require.NoError(t, ctx.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.Equal(t, tc.code, resp.Code) } }) diff --git a/starport/templates/typed/map/stargate/tests/messages/x/{{moduleName}}/keeper/msg_server_{{typeName}}_test.go.plush b/starport/templates/typed/map/stargate/tests/messages/x/{{moduleName}}/keeper/msg_server_{{typeName}}_test.go.plush index 7389d089e5..ca9d3eae85 100644 --- a/starport/templates/typed/map/stargate/tests/messages/x/{{moduleName}}/keeper/msg_server_{{typeName}}_test.go.plush +++ b/starport/templates/typed/map/stargate/tests/messages/x/{{moduleName}}/keeper/msg_server_{{typeName}}_test.go.plush @@ -23,7 +23,7 @@ func Test<%= TypeName.UpperCamel %>MsgServerCreate(t *testing.T) { <%= MsgSigner.LowerCamel %> := "A" for i := 0; i < 5; i++ { expected := &types.MsgCreate<%= TypeName.UpperCamel %>{<%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>, - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genUniqueArg(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueLoop() %>, <% } %> } _, err := srv.Create<%= TypeName.UpperCamel %>(wctx, expected) @@ -48,14 +48,14 @@ func Test<%= TypeName.UpperCamel %>MsgServerUpdate(t *testing.T) { { desc: "Completed", request: &types.MsgUpdate<%= TypeName.UpperCamel %>{<%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>, - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genValidIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueIndex() %>, <% } %> }, }, { desc: "Unauthorized", request: &types.MsgUpdate<%= TypeName.UpperCamel %>{<%= MsgSigner.UpperCamel %>: "B", - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genValidIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueIndex() %>, <% } %> }, err: sdkerrors.ErrUnauthorized, @@ -63,7 +63,7 @@ func Test<%= TypeName.UpperCamel %>MsgServerUpdate(t *testing.T) { { desc: "KeyNotFound", request: &types.MsgUpdate<%= TypeName.UpperCamel %>{<%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>, - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genNotFoundIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueInvalidIndex() %>, <% } %> }, err: sdkerrors.ErrKeyNotFound, @@ -74,7 +74,7 @@ func Test<%= TypeName.UpperCamel %>MsgServerUpdate(t *testing.T) { srv := keeper.NewMsgServerImpl(*k) wctx := sdk.WrapSDKContext(ctx) expected := &types.MsgCreate<%= TypeName.UpperCamel %>{<%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>, - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genValidIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueIndex() %>, <% } %> } _, err := srv.Create<%= TypeName.UpperCamel %>(wctx, expected) @@ -107,14 +107,14 @@ func Test<%= TypeName.UpperCamel %>MsgServerDelete(t *testing.T) { { desc: "Completed", request: &types.MsgDelete<%= TypeName.UpperCamel %>{<%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>, - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genValidIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueIndex() %>, <% } %> }, }, { desc: "Unauthorized", request: &types.MsgDelete<%= TypeName.UpperCamel %>{<%= MsgSigner.UpperCamel %>: "B", - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genValidIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueIndex() %>, <% } %> }, err: sdkerrors.ErrUnauthorized, @@ -122,7 +122,7 @@ func Test<%= TypeName.UpperCamel %>MsgServerDelete(t *testing.T) { { desc: "KeyNotFound", request: &types.MsgDelete<%= TypeName.UpperCamel %>{<%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>, - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genNotFoundIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueInvalidIndex() %>, <% } %> }, err: sdkerrors.ErrKeyNotFound, @@ -134,7 +134,7 @@ func Test<%= TypeName.UpperCamel %>MsgServerDelete(t *testing.T) { wctx := sdk.WrapSDKContext(ctx) _, err := srv.Create<%= TypeName.UpperCamel %>(wctx, &types.MsgCreate<%= TypeName.UpperCamel %>{<%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>, - <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= genValidIndex(index.DatatypeName) %>, + <%= for (i, index) in Indexes { %><%= index.Name.UpperCamel %>: <%= index.ValueIndex() %>, <% } %> }) require.NoError(t, err) diff --git a/starport/templates/typed/options.go b/starport/templates/typed/options.go index 92c2b843a2..1a7355b05f 100644 --- a/starport/templates/typed/options.go +++ b/starport/templates/typed/options.go @@ -1,8 +1,8 @@ package typed import ( - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/multiformatname" + "github.com/tendermint/starport/starport/templates/field" ) // Options ... @@ -17,6 +17,7 @@ type Options struct { Fields field.Fields Indexes field.Fields NoMessage bool + IsIBC bool } // Validate that options are usuable diff --git a/starport/templates/typed/placeholders.go b/starport/templates/typed/placeholders.go index a874cb9f2f..1c4e9909de 100644 --- a/starport/templates/typed/placeholders.go +++ b/starport/templates/typed/placeholders.go @@ -20,4 +20,8 @@ const ( PlaceholderGenesisTypesValidate = "// this line is used by starport scaffolding # genesis/types/validate" PlaceholderGenesisModuleInit = "// this line is used by starport scaffolding # genesis/module/init" PlaceholderGenesisModuleExport = "// this line is used by starport scaffolding # genesis/module/export" + + PlaceholderSimappConst = "// this line is used by starport scaffolding # simapp/module/const" + PlaceholderSimappGenesisState = "// this line is used by starport scaffolding # simapp/module/genesisState" + PlaceholderSimappOperation = "// this line is used by starport scaffolding # simapp/module/operation" ) diff --git a/starport/templates/typed/simapp.go b/starport/templates/typed/simapp.go new file mode 100644 index 0000000000..55a7c7ce3e --- /dev/null +++ b/starport/templates/typed/simapp.go @@ -0,0 +1,47 @@ +package typed + +import ( + "fmt" + + "github.com/tendermint/starport/starport/pkg/multiformatname" + "github.com/tendermint/starport/starport/pkg/placeholder" +) + +func ModuleSimulationMsgModify( + replacer placeholder.Replacer, + content, + moduleName string, + typeName multiformatname.Name, + msgs ...string, +) string { + if len(msgs) == 0 { + msgs = append(msgs, "") + } + for _, msg := range msgs { + // simulation constants + templateConst := `opWeightMsg%[2]v%[3]v = "op_weight_msg_create_chain" + // TODO: Determine the simulation weight value + defaultWeightMsg%[2]v%[3]v int = 100 + + %[1]v` + replacementConst := fmt.Sprintf(templateConst, PlaceholderSimappConst, msg, typeName.UpperCamel) + content = replacer.Replace(content, PlaceholderSimappConst, replacementConst) + + // simulation operations + templateOp := `var weightMsg%[2]v%[3]v int + simState.AppParams.GetOrGenerate(simState.Cdc, opWeightMsg%[2]v%[3]v, &weightMsg%[2]v%[3]v, nil, + func(_ *rand.Rand) { + weightMsg%[2]v%[3]v = defaultWeightMsg%[2]v%[3]v + }, + ) + operations = append(operations, simulation.NewWeightedOperation( + weightMsg%[2]v%[3]v, + %[4]vsimulation.SimulateMsg%[2]v%[3]v(am.accountKeeper, am.bankKeeper, am.keeper), + )) + + %[1]v` + replacementOp := fmt.Sprintf(templateOp, PlaceholderSimappOperation, msg, typeName.UpperCamel, moduleName) + content = replacer.Replace(content, PlaceholderSimappOperation, replacementOp) + } + return content +} diff --git a/starport/templates/typed/singleton/simulation.go b/starport/templates/typed/singleton/simulation.go new file mode 100644 index 0000000000..affac8996f --- /dev/null +++ b/starport/templates/typed/singleton/simulation.go @@ -0,0 +1,30 @@ +package singleton + +import ( + "path/filepath" + + "github.com/gobuffalo/genny" + "github.com/tendermint/starport/starport/pkg/placeholder" + "github.com/tendermint/starport/starport/templates/typed" +) + +func moduleSimulationModify(replacer placeholder.Replacer, opts *typed.Options) genny.RunFn { + return func(r *genny.Runner) error { + path := filepath.Join(opts.AppPath, "x", opts.ModuleName, "module_simulation.go") + f, err := r.Disk.Find(path) + if err != nil { + return err + } + + content := typed.ModuleSimulationMsgModify( + replacer, + f.String(), + opts.ModuleName, + opts.TypeName, + "Create", "Update", "Delete", + ) + + newFile := genny.NewFileS(path, content) + return r.File(newFile) + } +} diff --git a/starport/templates/typed/singleton/stargate.go b/starport/templates/typed/singleton/stargate.go index ec2646fb8a..f58caa348b 100644 --- a/starport/templates/typed/singleton/stargate.go +++ b/starport/templates/typed/singleton/stargate.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/gobuffalo/genny" - "github.com/tendermint/starport/starport/pkg/field" "github.com/tendermint/starport/starport/pkg/placeholder" "github.com/tendermint/starport/starport/pkg/xgenny" "github.com/tendermint/starport/starport/templates/module" @@ -56,6 +55,7 @@ func NewStargate(replacer placeholder.Replacer, opts *typed.Options) (*genny.Gen g.RunFn(handlerModify(replacer, opts)) g.RunFn(clientCliTxModify(replacer, opts)) g.RunFn(typesCodecModify(replacer, opts)) + g.RunFn(moduleSimulationModify(replacer, opts)) if err := typed.Box(messagesTemplate, opts, g); err != nil { return nil, err @@ -105,17 +105,17 @@ func protoRPCModify(replacer placeholder.Replacer, opts *typed.Options) genny.Ru content = replacer.Replace(content, typed.Placeholder, replacementGogoImport) // Add the service - templateService := `// Queries a %[3]v by index. + templateService := `// Queries a %[2]v by index. rpc %[2]v(QueryGet%[2]vRequest) returns (QueryGet%[2]vResponse) { - option (google.api.http).get = "/%[4]v/%[5]v/%[6]v/%[3]v"; + option (google.api.http).get = "/%[3]v/%[4]v/%[5]v/%[6]v"; } %[1]v` replacementService := fmt.Sprintf(templateService, typed.Placeholder2, opts.TypeName.UpperCamel, - opts.TypeName.LowerCamel, opts.OwnerName, opts.AppName, opts.ModuleName, + opts.TypeName.Snake, ) content = replacer.Replace(content, typed.Placeholder2, replacementService) @@ -247,20 +247,13 @@ func genesisTestsModify(replacer placeholder.Replacer, opts *typed.Options) genn // Create a fields sampleFields := "" - for _, f := range opts.Fields { - switch f.DatatypeName { - case field.TypeString: - sampleFields += fmt.Sprintf("%s: \"%s\",\n", f.Name.UpperCamel, f.Name.LowerCamel) - case field.TypeInt, field.TypeUint: - sampleFields += fmt.Sprintf("%s: %d,\n", f.Name.UpperCamel, rand.Intn(100)) - case field.TypeBool: - sampleFields += fmt.Sprintf("%s: %t,\n", f.Name.UpperCamel, rand.Intn(2) == 0) - } + for _, field := range opts.Fields { + sampleFields += field.GenesisArgs(rand.Intn(100) + 1) } templateState := `%[2]v: &types.%[2]v{ %[3]v}, -%[1]v` + %[1]v` replacementState := fmt.Sprintf( templateState, module.PlaceholderGenesisTestState, @@ -293,15 +286,8 @@ func genesisTypesTestsModify(replacer placeholder.Replacer, opts *typed.Options) // Create a fields sampleFields := "" - for _, f := range opts.Fields { - switch f.DatatypeName { - case field.TypeString: - sampleFields += fmt.Sprintf("%s: \"%s\",\n", f.Name.UpperCamel, f.Name.LowerCamel) - case field.TypeInt, field.TypeUint: - sampleFields += fmt.Sprintf("%s: %d,\n", f.Name.UpperCamel, rand.Intn(100)) - case field.TypeBool: - sampleFields += fmt.Sprintf("%s: %t,\n", f.Name.UpperCamel, rand.Intn(2) == 0) - } + for _, field := range opts.Fields { + sampleFields += field.GenesisArgs(rand.Intn(100) + 1) } templateValid := `%[2]v: &types.%[2]v{ @@ -390,12 +376,20 @@ func protoTxModify(replacer placeholder.Replacer, opts *typed.Options) genny.Run // Messages var fields string - for i, f := range opts.Fields { - fields += fmt.Sprintf(" %s %s = %d;\n", f.Datatype, f.Name.LowerCamel, i+3) + for i, field := range opts.Fields { + fields += fmt.Sprintf(" %s;\n", field.ProtoType(i+3)) } + + // Ensure custom types are imported + protoImports := opts.Fields.ProtoImports() for _, f := range opts.Fields.Custom() { + protoImports = append(protoImports, + fmt.Sprintf("%[1]v/%[2]v.proto", opts.ModuleName, f), + ) + } + for _, f := range protoImports { importModule := fmt.Sprintf(` -import "%[1]v/%[2]v.proto";`, opts.ModuleName, f) +import "%[1]v";`, f) content = strings.ReplaceAll(content, importModule, "") replacementImport := fmt.Sprintf("%[1]v%[2]v", typed.PlaceholderProtoTxImport, importModule) diff --git a/starport/templates/typed/singleton/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush b/starport/templates/typed/singleton/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush index 13b8f17821..9cc9b70b83 100644 --- a/starport/templates/typed/singleton/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush +++ b/starport/templates/typed/singleton/stargate/component/proto/{{moduleName}}/{{typeName}}.proto.plush @@ -1,12 +1,11 @@ syntax = "proto3"; package <%= formatOwnerName(OwnerName) %>.<%= AppName %>.<%= ModuleName %>; -option go_package = "<%= ModulePath %>/x/<%= ModuleName %>/types"; - -import "gogoproto/gogo.proto"; -<%= for (i, importName) in Fields.Custom() { %>import "<%= ModuleName %>/<%= importName %>.proto"; <% } %> +option go_package = "<%= ModulePath %>/x/<%= ModuleName %>/types";<%= for (importName) in mergeCustomImports(Fields) { %> +import "<%= ModuleName %>/<%= importName %>.proto"; <% } %><%= for (importName) in mergeProtoImports(Fields) { %> +import "<%= importName %>"; <% } %> message <%= TypeName.UpperCamel %> {<%= for (i, field) in Fields { %> - <%= field.GetProtoDatatype() %> <%= field.Name.LowerCamel %> = <%= i+1 %>; <% } %> + <%= field.ProtoType(i+1) %>; <% } %> <%= if (!NoMessage) { %>string <%= MsgSigner.LowerCamel %> = <%= len(Fields)+1 %>;<% } %> } diff --git a/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush b/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush index 248d27db7d..ddf064e542 100644 --- a/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush +++ b/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/client/cli/query_{{typeName}}_test.go.plush @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc/status" "<%= ModulePath %>/testutil/network" + "<%= ModulePath %>/testutil/nullify" "<%= ModulePath %>/x/<%= ModuleName %>/client/cli" "<%= ModulePath %>/x/<%= ModuleName %>/types" ) @@ -20,7 +21,9 @@ func networkWith<%= TypeName.UpperCamel %>Objects(t *testing.T) (*network.Networ state := types.GenesisState{} require.NoError(t, cfg.Codec.UnmarshalJSON(cfg.GenesisState[types.ModuleName], &state)) - state.<%= TypeName.UpperCamel %> = &types.<%= TypeName.UpperCamel %>{} + <%= TypeName.LowerCamel %> := &types.<%= TypeName.UpperCamel %>{} + nullify.Fill(&<%= TypeName.LowerCamel %>) + state.<%= TypeName.UpperCamel %> = <%= TypeName.LowerCamel %> buf, err := cfg.Codec.MarshalJSON(&state) require.NoError(t, err) cfg.GenesisState[types.ModuleName] = buf @@ -60,7 +63,10 @@ func TestShow<%= TypeName.UpperCamel %>(t *testing.T) { var resp types.QueryGet<%= TypeName.UpperCamel %>Response require.NoError(t, net.Config.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.NotNil(t, resp.<%= TypeName.UpperCamel %>) - require.Equal(t, tc.obj, resp.<%= TypeName.UpperCamel %>) + require.Equal(t, + nullify.Fill(&tc.obj), + nullify.Fill(&resp.<%= TypeName.UpperCamel %>), + ) } }) } diff --git a/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush b/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush index eecc81dbf0..6d610e1579 100644 --- a/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush +++ b/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/keeper/grpc_query_{{typeName}}_test.go.plush @@ -9,6 +9,7 @@ import ( "google.golang.org/grpc/status" keepertest "<%= ModulePath %>/testutil/keeper" + "<%= ModulePath %>/testutil/nullify" "<%= ModulePath %>/x/<%= ModuleName %>/types" ) @@ -37,7 +38,11 @@ func Test<%= TypeName.UpperCamel %>Query(t *testing.T) { if tc.err != nil { require.ErrorIs(t, err, tc.err) } else { - require.Equal(t, tc.response, response) + require.NoError(t, err) + require.Equal(t, + nullify.Fill(tc.response), + nullify.Fill(response), + ) } }) } diff --git a/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush b/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush index 890a749e89..c2ed94e04b 100644 --- a/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush +++ b/starport/templates/typed/singleton/stargate/component/x/{{moduleName}}/keeper/{{typeName}}_test.go.plush @@ -5,10 +5,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - + "<%= ModulePath %>/x/<%= ModuleName %>/keeper" "<%= ModulePath %>/x/<%= ModuleName %>/types" keepertest "<%= ModulePath %>/testutil/keeper" + "<%= ModulePath %>/testutil/nullify" ) func createTest<%= TypeName.UpperCamel %>(keeper *keeper.Keeper, ctx sdk.Context) types.<%= TypeName.UpperCamel %> { @@ -22,8 +23,12 @@ func Test<%= TypeName.UpperCamel %>Get(t *testing.T) { item := createTest<%= TypeName.UpperCamel %>(keeper, ctx) rst, found := keeper.Get<%= TypeName.UpperCamel %>(ctx) require.True(t, found) - require.Equal(t, item, rst) + require.Equal(t, + nullify.Fill(&item), + nullify.Fill(&rst), + ) } + func Test<%= TypeName.UpperCamel %>Remove(t *testing.T) { keeper, ctx := keepertest.<%= title(ModuleName) %>Keeper(t) createTest<%= TypeName.UpperCamel %>(keeper, ctx) diff --git a/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush b/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush index 67b2e22018..5c27fa30a4 100644 --- a/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush +++ b/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}.go.plush @@ -1,11 +1,9 @@ package cli import ( - <%= if (Fields.IsComplex()) { %> "encoding/json" <% } %> - + <%= for (goImport) in mergeGoImports(Fields) { %> + <%= goImport.Alias %> "<%= goImport.Name %>"<% } %> "github.com/spf13/cobra" - <%= if (Fields.NeedCastImport()) { %> "github.com/spf13/cast" <% } %> - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -18,7 +16,7 @@ func CmdCreate<%= TypeName.UpperCamel %>() *cobra.Command { Short: "Create <%= TypeName.Original %>", Args: cobra.ExactArgs(<%= len(Fields) %>), RunE: func(cmd *cobra.Command, args []string) (err error) { - <%= for (i, field) in Fields { %> <%= castArg("arg", field, i) %> + <%= for (i, field) in Fields { %> <%= field.CLIArgs("arg", i) %> <% } %> clientCtx, err := client.GetClientTxContext(cmd) if err != nil { @@ -44,7 +42,7 @@ func CmdUpdate<%= TypeName.UpperCamel %>() *cobra.Command { Short: "Update <%= TypeName.Original %>", Args: cobra.ExactArgs(<%= len(Fields) %>), RunE: func(cmd *cobra.Command, args []string) (err error) { - <%= for (i, field) in Fields { %> <%= castArg("arg", field, i) %> + <%= for (i, field) in Fields { %> <%= field.CLIArgs("arg", i) %> <% } %> clientCtx, err := client.GetClientTxContext(cmd) if err != nil { diff --git a/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush b/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush index a765451e14..2e1efd80d5 100644 --- a/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush +++ b/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/client/cli/tx_{{typeName}}_test.go.plush @@ -18,7 +18,7 @@ func TestCreate<%= TypeName.UpperCamel %>(t *testing.T) { val := net.Validators[0] ctx := val.ClientCtx - fields := []string{<%= for (field) in Fields { %> "<%= genValidArg(field.DatatypeName) %>", <% } %>} + fields := []string{<%= for (field) in Fields { %> "<%= field.DefaultTestValue() %>", <% } %>} for _, tc := range []struct { desc string args []string @@ -46,7 +46,7 @@ func TestCreate<%= TypeName.UpperCamel %>(t *testing.T) { } else { require.NoError(t, err) var resp sdk.TxResponse - require.NoError(t, ctx.JSONCodec.UnmarshalJSON(out.Bytes(), &resp)) + require.NoError(t, ctx.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.Equal(t, tc.code, resp.Code) } }) @@ -58,7 +58,7 @@ func TestUpdate<%= TypeName.UpperCamel %>(t *testing.T) { val := net.Validators[0] ctx := val.ClientCtx - fields := []string{<%= for (field) in Fields { %> "<%= genValidArg(field.DatatypeName) %>", <% } %>} + fields := []string{<%= for (field) in Fields { %> "<%= field.DefaultTestValue() %>", <% } %>} common := []string{ fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), @@ -93,7 +93,7 @@ func TestUpdate<%= TypeName.UpperCamel %>(t *testing.T) { } else { require.NoError(t, err) var resp sdk.TxResponse - require.NoError(t, ctx.JSONCodec.UnmarshalJSON(out.Bytes(), &resp)) + require.NoError(t, ctx.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.Equal(t, tc.code, resp.Code) } }) @@ -106,7 +106,7 @@ func TestDelete<%= TypeName.UpperCamel %>(t *testing.T) { val := net.Validators[0] ctx := val.ClientCtx - fields := []string{<%= for (field) in Fields { %> "<%= genValidArg(field.DatatypeName) %>", <% } %>} + fields := []string{<%= for (field) in Fields { %> "<%= field.DefaultTestValue() %>", <% } %>} common := []string{ fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address.String()), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), @@ -138,7 +138,7 @@ func TestDelete<%= TypeName.UpperCamel %>(t *testing.T) { } else { require.NoError(t, err) var resp sdk.TxResponse - require.NoError(t, ctx.JSONCodec.UnmarshalJSON(out.Bytes(), &resp)) + require.NoError(t, ctx.Codec.UnmarshalJSON(out.Bytes(), &resp)) require.Equal(t, tc.code, resp.Code) } }) diff --git a/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/simulation/{{typeName}}.go.plush b/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/simulation/{{typeName}}.go.plush new file mode 100644 index 0000000000..2da6758a9a --- /dev/null +++ b/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/simulation/{{typeName}}.go.plush @@ -0,0 +1,127 @@ +package simulation + +import ( + "math/rand" + + "<%= ModulePath %>/x/<%= ModuleName %>/keeper" + "<%= ModulePath %>/x/<%= ModuleName %>/types" + "github.com/cosmos/cosmos-sdk/baseapp" + simappparams "github.com/cosmos/cosmos-sdk/simapp/params" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" +) + +func SimulateMsgCreate<%= TypeName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + simAccount, _ := simtypes.RandomAcc(r, accs) + + msg := &types.MsgCreate<%= TypeName.UpperCamel %>{ + <%= MsgSigner.UpperCamel %>: simAccount.Address.String(), + } + + _, found := k.Get<%= TypeName.UpperCamel %>(ctx) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.UpperCamel %> already exist"), nil, nil + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(), + AccountKeeper: ak, + Bankkeeper: bk, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +func SimulateMsgUpdate<%= TypeName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var ( + simAccount = simtypes.Account{} + msg = &types.MsgUpdate<%= TypeName.UpperCamel %>{} + <%= TypeName.LowerCamel %>, found = k.Get<%= TypeName.UpperCamel %>(ctx) + ) + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.LowerCamel %> store is empty"), nil, nil + } + simAccount, found = FindAccount(accs, <%= TypeName.LowerCamel %>.<%= MsgSigner.UpperCamel %>) + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.LowerCamel %> <%= MsgSigner.LowerCamel %> not found"), nil, nil + } + msg.<%= MsgSigner.UpperCamel %> = simAccount.Address.String() + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(), + AccountKeeper: ak, + Bankkeeper: bk, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} + +func SimulateMsgDelete<%= TypeName.UpperCamel %>( + ak types.AccountKeeper, + bk types.BankKeeper, + k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + var ( + simAccount = simtypes.Account{} + msg = &types.MsgUpdate<%= TypeName.UpperCamel %>{} + <%= TypeName.LowerCamel %>, found = k.Get<%= TypeName.UpperCamel %>(ctx) + ) + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.LowerCamel %> store is empty"), nil, nil + } + simAccount, found = FindAccount(accs, <%= TypeName.LowerCamel %>.<%= MsgSigner.UpperCamel %>) + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "<%= TypeName.LowerCamel %> <%= MsgSigner.LowerCamel %> not found"), nil, nil + } + msg.<%= MsgSigner.UpperCamel %> = simAccount.Address.String() + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(), + AccountKeeper: ak, + Bankkeeper: bk, + } + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush b/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush index 782d1264e2..8a54e7be60 100644 --- a/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush +++ b/starport/templates/typed/singleton/stargate/messages/x/{{moduleName}}/types/messages_{{typeName}}.go.plush @@ -5,9 +5,15 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) +const ( + TypeMsgCreate<%= TypeName.UpperCamel %> = "create_<%= TypeName.Snake %>" + TypeMsgUpdate<%= TypeName.UpperCamel %> = "update_<%= TypeName.Snake %>" + TypeMsgDelete<%= TypeName.UpperCamel %> = "delete_<%= TypeName.Snake %>" +) + var _ sdk.Msg = &MsgCreate<%= TypeName.UpperCamel %>{} -func NewMsgCreate<%= TypeName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.GetDatatype() %><% } %>) *MsgCreate<%= TypeName.UpperCamel %> { +func NewMsgCreate<%= TypeName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.DataType() %><% } %>) *MsgCreate<%= TypeName.UpperCamel %> { return &MsgCreate<%= TypeName.UpperCamel %>{ <%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>,<%= for (field) in Fields { %> <%= field.Name.UpperCamel %>: <%= field.Name.LowerCamel %>,<% } %> @@ -19,7 +25,7 @@ func (msg *MsgCreate<%= TypeName.UpperCamel %>) Route() string { } func (msg *MsgCreate<%= TypeName.UpperCamel %>) Type() string { - return "Create<%= TypeName.UpperCamel %>" + return TypeMsgCreate<%= TypeName.UpperCamel %> } func (msg *MsgCreate<%= TypeName.UpperCamel %>) GetSigners() []sdk.AccAddress { @@ -45,7 +51,7 @@ func (msg *MsgCreate<%= TypeName.UpperCamel %>) ValidateBasic() error { var _ sdk.Msg = &MsgUpdate<%= TypeName.UpperCamel %>{} -func NewMsgUpdate<%= TypeName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.GetDatatype() %><% } %>) *MsgUpdate<%= TypeName.UpperCamel %> { +func NewMsgUpdate<%= TypeName.UpperCamel %>(<%= MsgSigner.LowerCamel %> string<%= for (field) in Fields { %>, <%= field.Name.LowerCamel %> <%= field.DataType() %><% } %>) *MsgUpdate<%= TypeName.UpperCamel %> { return &MsgUpdate<%= TypeName.UpperCamel %>{ <%= MsgSigner.UpperCamel %>: <%= MsgSigner.LowerCamel %>,<%= for (field) in Fields { %> <%= field.Name.UpperCamel %>: <%= field.Name.LowerCamel %>,<% } %> @@ -57,7 +63,7 @@ func (msg *MsgUpdate<%= TypeName.UpperCamel %>) Route() string { } func (msg *MsgUpdate<%= TypeName.UpperCamel %>) Type() string { - return "Update<%= TypeName.UpperCamel %>" + return TypeMsgUpdate<%= TypeName.UpperCamel %> } func (msg *MsgUpdate<%= TypeName.UpperCamel %>) GetSigners() []sdk.AccAddress { @@ -93,7 +99,7 @@ func (msg *MsgDelete<%= TypeName.UpperCamel %>) Route() string { } func (msg *MsgDelete<%= TypeName.UpperCamel %>) Type() string { - return "Delete<%= TypeName.UpperCamel %>" + return TypeMsgDelete<%= TypeName.UpperCamel %> } func (msg *MsgDelete<%= TypeName.UpperCamel %>) GetSigners() []sdk.AccAddress { diff --git a/starport/templates/typed/typed.go b/starport/templates/typed/typed.go index c749a3fdbd..3e4fd37229 100644 --- a/starport/templates/typed/typed.go +++ b/starport/templates/typed/typed.go @@ -5,8 +5,8 @@ import ( "github.com/gobuffalo/packd" "github.com/gobuffalo/plush" "github.com/gobuffalo/plushgen" - "github.com/tendermint/starport/starport/pkg/plushhelpers" "github.com/tendermint/starport/starport/pkg/xstrings" + "github.com/tendermint/starport/starport/templates/field/plushhelpers" "github.com/tendermint/starport/starport/templates/testutil" ) @@ -37,14 +37,15 @@ func Box(box packd.Walker, opts *Options, g *genny.Generator) error { // Used for proto package name ctx.Set("formatOwnerName", xstrings.FormatUsername) - // Create the 'testutil' package with the test helpers - if err := testutil.Register(ctx, g, opts.AppPath); err != nil { - return err - } - plushhelpers.ExtendPlushContext(ctx) g.Transformer(plushgen.Transformer(ctx)) g.Transformer(genny.Replace("{{moduleName}}", opts.ModuleName)) g.Transformer(genny.Replace("{{typeName}}", opts.TypeName.Snake)) + + // Create the 'testutil' package with the test helpers + if err := testutil.Register(g, opts.AppPath); err != nil { + return err + } + return nil }