├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── slack-users.json └── workflows │ ├── basic-checks.yml │ ├── build-release.yml │ ├── integration-tests.yml │ └── on-workflow-end.yaml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── LICENSE.header ├── Makefile ├── README.md ├── analytics ├── amplitude.go ├── analytics_disable.go └── constant.go ├── client ├── graphql.go ├── graphql_test.go ├── grpc.go ├── grpc_test.go ├── http.go └── http_test.go ├── cmd ├── analytics.go ├── flags.go ├── gas_station.go ├── gas_station_integration_test.go ├── helper_integration_test.go ├── helper_text.go ├── init.go ├── initia.go ├── initia_integration_test.go ├── opinit_bots.go ├── opinit_integration_test.go ├── relayer.go ├── relayer_integration_test.go ├── rollup.go ├── rollup_integration_test.go ├── root.go ├── upgrade.go └── validate.go ├── common ├── constants.go ├── strings.go ├── strings_test.go ├── validate.go └── validate_test.go ├── config ├── config.go ├── config_test.go └── toml.go ├── context ├── base_model.go ├── context.go ├── layout.go ├── path.go ├── state.go └── tooltip.go ├── cosmosutils ├── binary.go ├── binary_test.go ├── cli_query.go ├── cli_tx.go ├── cosmos_query.go ├── graphql_query.go ├── keys.go ├── polkachu_query.go ├── statesync.go └── types.go ├── crypto ├── bech32.go ├── bip39.go └── wordlist.go ├── docs ├── gas_station.md ├── initia_node.md ├── opinit_bots.md ├── relayer.md └── rollup_launch.md ├── go.mod ├── go.sum ├── io ├── filesystem.go ├── filesystem_test.go └── keyfile.go ├── main.go ├── models ├── homepage.go ├── initia │ ├── constants.go │ ├── run_l1_node.go │ ├── run_l1_node_test.go │ └── state.go ├── initialize.go ├── minitia │ ├── constants.go │ ├── launch.go │ ├── launch_test.go │ ├── state.go │ ├── state_test.go │ └── tx.go ├── opinit_bots │ ├── bots.go │ ├── config.go │ ├── constants.go │ ├── field_input_model.go │ ├── init.go │ ├── init_test.go │ ├── setup.go │ ├── setup_test.go │ ├── state.go │ └── submodel.go ├── relayer │ ├── config.go │ ├── constants.go │ ├── field_input_model.go │ ├── init.go │ ├── init_test.go │ ├── state.go │ ├── submodel.go │ └── update_client.go └── weaveinit │ ├── weaveinit.go │ └── weaveinit_test.go ├── registry ├── constants.go ├── registry.go ├── registry_test.go └── types.go ├── service ├── launchd.go ├── service.go ├── systemd.go ├── template.go └── types.go ├── styles ├── color.go ├── text.go └── wordwrap.go ├── testutil ├── alias.go └── testutil.go ├── tooltip ├── common.go ├── description.go ├── gas_station.go ├── l1.go ├── opinit.go ├── relayer.go └── rollup.go ├── types ├── minitia_config.go ├── minitia_config_test.go ├── relayer.go └── state.go ├── ubuntu-script.sh └── ui ├── checkbox.go ├── checkbox_test.go ├── clickable.go ├── downloader.go ├── loading.go ├── selector.go ├── text_input.go └── tooltip.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @traviolus @Benzbeeb @WasinWatt 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Press button '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Closes: #XXXX 4 | 5 | 7 | 8 | --- 9 | 10 | ## Author Checklist 11 | 12 | _All items are required. Please add a note to the item if the item is not applicable and 13 | please add links to any relevant follow-up issues._ 14 | 15 | I have... 16 | 17 | - [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title, you can find examples of the prefixes below: 18 | 29 | - [ ] confirmed `!` in the type prefix if API or client breaking change 30 | - [ ] targeted the correct branch 31 | - [ ] provided a link to the relevant issue or specification 32 | - [ ] reviewed "Files changed" and left comments if necessary 33 | - [ ] included the necessary unit and integration tests 34 | - [ ] updated the relevant documentation or specification, including comments for [documenting Go code](https://blog.golang.org/godoc) 35 | - [ ] confirmed all CI checks have passed 36 | 37 | ## Reviewers Checklist 38 | 39 | _All items are required. Please add a note if the item is not applicable and please add 40 | your handle next to the items reviewed if you only reviewed selected items._ 41 | 42 | I have... 43 | 44 | - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title 45 | - [ ] confirmed all author checklist items have been addressed 46 | - [ ] reviewed state machine logic, API design and naming, documentation is accurate, tests and test coverage 47 | -------------------------------------------------------------------------------- /.github/slack-users.json: -------------------------------------------------------------------------------- 1 | { 2 | "WasinWatt": "U079BTK4WFP", 3 | "traviolus": "U07A8KRJ50C", 4 | "Benzbeeb": "U07A62A4N91", 5 | "songwongtp": "U07AJR8DXCH", 6 | "Poafs1": "U079ZGFKSVC", 7 | "jennieramida": "U07AUUTS8F2", 8 | "evilpeach": "U07AJR8MFMF", 9 | "tansawit": "U075LURT1EG" 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/basic-checks.yml: -------------------------------------------------------------------------------- 1 | name: Basic Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | lint: 13 | env: 14 | GOLANGCI_LINT_VERSION: v1.61.0 15 | name: golangci-lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: ^1.22 22 | - uses: technote-space/get-diff-action@v6.1.2 23 | id: git_diff 24 | with: 25 | PATTERNS: | 26 | **/**.go 27 | go.mod 28 | go.sum 29 | - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} 30 | - name: Run golangci-lint 31 | run: make lint 32 | unit-tests: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions/setup-go@v5 37 | with: 38 | go-version: ^1.22 39 | 40 | - name: Run unit tests 41 | run: go test -v ./... 42 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build Go binaries and upload release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | basic-checks: 10 | name: Basic Checks 11 | runs-on: ubuntu-latest 12 | env: 13 | GOLANGCI_LINT_VERSION: v1.61.0 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version: ^1.22 21 | 22 | - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} 23 | - name: Run golangci-lint 24 | run: make lint 25 | 26 | - name: Run unit tests 27 | run: go test -v ./... 28 | 29 | integration-tests: 30 | name: Run Integration Tests 31 | needs: basic-checks 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | 37 | - uses: actions/setup-go@v5 38 | with: 39 | go-version: ^1.22 40 | 41 | - name: Run Integration Tests 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | run: go test -v ./... -tags=integration 45 | 46 | build-and-upload: 47 | name: Build Go binaries and upload release 48 | needs: integration-tests 49 | runs-on: ubuntu-latest 50 | 51 | strategy: 52 | matrix: 53 | goos: [linux, darwin] 54 | goarch: [amd64, arm64] 55 | 56 | steps: 57 | - uses: actions/checkout@v4 58 | 59 | - uses: actions/setup-go@v5 60 | with: 61 | go-version: ^1.22 62 | 63 | - name: Build binary 64 | run: | 65 | mkdir -p build/${{ matrix.goos }}_${{ matrix.goarch }} 66 | GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -ldflags "-X github.com/initia-labs/weave/cmd.Version=${{ github.ref_name }}" -o build/${{ matrix.goos }}_${{ matrix.goarch }}/weave 67 | 68 | - name: Write version 69 | run: | 70 | TAG=${{ github.event.release.tag_name }} 71 | echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" 72 | 73 | - name: Create tar.gz 74 | run: | 75 | tar -czvf weave-${{ env.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz -C build/${{ matrix.goos }}_${{ matrix.goarch }} weave 76 | 77 | - name: Upload binary to release 78 | uses: softprops/action-gh-release@v2 79 | if: startsWith(github.ref, 'refs/tags/') 80 | with: 81 | name: ${{ github.ref_name }} 82 | draft: false 83 | token: ${{ secrets.GH_RELEASE_TOKEN }} 84 | files: | 85 | weave-${{ env.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz 86 | 87 | homebrew-release: 88 | runs-on: ubuntu-latest 89 | name: homebrew-release 90 | needs: build-and-upload 91 | steps: 92 | - name: Release project to Homebrew tap 93 | uses: Justintime50/homebrew-releaser@v2 94 | with: 95 | homebrew_owner: initia-labs 96 | homebrew_tap: homebrew-tap 97 | formula_folder: Formula 98 | version: ${{ github.event.release.tag_name }} 99 | github_token: ${{ secrets.GH_RELEASE_TOKEN }} 100 | commit_owner: github-actions[bot] 101 | commit_email: github-actions[bot]@users.noreply.github.com 102 | install: 'bin.install "weave"' 103 | test: 'assert_match version.to_s, shell_output("#{bin}/weave version")' 104 | target_darwin_amd64: true 105 | target_darwin_arm64: true 106 | target_linux_amd64: false 107 | target_linux_arm64: false 108 | update_readme_table: false 109 | skip_commit: false 110 | debug: false 111 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | integration-tests-ubuntu: 17 | name: Run Integration Tests on Ubuntu 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: actions/setup-go@v5 24 | with: 25 | go-version: ^1.22 26 | 27 | - name: Run Integration Tests 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: go test -v ./... -tags=integration 31 | 32 | integration-tests-macos: 33 | name: Run Integration Tests on macOS 34 | needs: integration-tests-ubuntu 35 | runs-on: macos-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - uses: actions/setup-go@v5 41 | with: 42 | go-version: ^1.22 43 | 44 | - name: Run Integration Tests 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | run: go test -v ./... -tags=integration 48 | -------------------------------------------------------------------------------- /.github/workflows/on-workflow-end.yaml: -------------------------------------------------------------------------------- 1 | name: Notify Slack on workflow completion 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - "*" 7 | branches: 8 | - main 9 | types: [completed] 10 | 11 | jobs: 12 | notify-slack: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Get Slack member IDs 18 | id: get-slack-id 19 | if: github.event.workflow_run.conclusion != 'success' 20 | run: | 21 | SLACK_ID=$(jq -r --arg GITHUB_USER "${{ github.actor }}" '.[$GITHUB_USER] // "Unknown"' .github/slack-users.json) 22 | echo "slack_id=$SLACK_ID" >> $GITHUB_OUTPUT 23 | 24 | - name: Set job-specific variables 25 | id: set-variables 26 | run: | 27 | if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then 28 | echo "status_emoji=:white_check_mark:" >> $GITHUB_OUTPUT 29 | echo "view_text=View Success" >> $GITHUB_OUTPUT 30 | echo "footer=Congratulations! 🎉" >> $GITHUB_OUTPUT 31 | else 32 | echo "status_emoji=:fire:" >> $GITHUB_OUTPUT 33 | echo "view_text=View Failure" >> $GITHUB_OUTPUT 34 | echo "footer=Culprit: <@${{ steps.get-slack-id.outputs.slack_id }}>" >> $GITHUB_OUTPUT 35 | fi 36 | 37 | - uses: ravsamhq/notify-slack-action@v2 38 | with: 39 | status: ${{ github.event.workflow_run.conclusion }} 40 | notification_title: "${{github.event.workflow_run.name}} - ${{github.event.workflow_run.conclusion}} on ${{github.event.workflow_run.head_branch}} - <${{github.server_url}}/${{github.repository}}/actions/runs/${{github.event.workflow_run.id}}|${{ steps.set-variables.outputs.view_text }}>" 41 | message_format: "${{ steps.set-variables.outputs.status_emoji }} *${{github.event.workflow_run.name}}* ${{github.event.workflow_run.conclusion}} in <${{github.server_url}}/${{github.repository}}/${{github.event.workflow_run.head_branch}}|${{github.repository}}>" 42 | footer: ${{ steps.set-variables.outputs.footer }} 43 | env: 44 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | weave 2 | build/ 3 | 4 | # IDE specific files 5 | .idea/ 6 | .vscode/ 7 | *.swp 8 | 9 | # Dependency directories 10 | vendor/ 11 | 12 | # Logs 13 | *.log 14 | 15 | # OS generated files 16 | .DS_Store 17 | Thumbs.db 18 | 19 | # GoLand specific files 20 | *.iml 21 | 22 | *.weave 23 | .github_token 24 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: false 3 | disable: 4 | - thelper 5 | - varnamelen 6 | - tagliatelle 7 | - wrapcheck 8 | - typecheck 9 | errcheck: 10 | exclude-functions: 11 | - fmt:.* 12 | - io/ioutil:^Read.* 13 | - github.com/spf13/cobra:MarkFlagRequired 14 | - github.com/spf13/viper:BindPFlag 15 | linters-settings: 16 | gocyclo: 17 | min-complexity: 11 18 | golint: 19 | min-confidence: 1.1 20 | issues: 21 | exclude: 22 | - composite 23 | - 'SA1019: "golang.org/x/crypto/ripemd160" is deprecated:' # Exclude deprecated ripemd160 warning 24 | run: 25 | tests: false 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024-2025, Initia Foundation 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /LICENSE.header: -------------------------------------------------------------------------------- 1 | Copyright 2024-2025 Initia Foundation 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the Initia Foundation nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # Go version and build settings 4 | GO_VERSION := 1.23 5 | GO_SYSTEM_VERSION := $(shell go version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1-2) 6 | REQUIRE_GO_VERSION := $(GO_VERSION) 7 | 8 | # Project version 9 | WEAVE_VERSION := $(shell git describe --tags) 10 | 11 | # Build directory 12 | BUILDDIR ?= $(CURDIR)/build 13 | 14 | # Build targets 15 | BUILD_TARGETS := build install test 16 | 17 | release_version=$(filter-out $@,$(MAKECMDGOALS)) 18 | 19 | # Version check 20 | check_version: 21 | @if [ $(shell echo "$(GO_SYSTEM_VERSION) < $(REQUIRE_GO_VERSION)" | bc -l) -eq 1 ]; then \ 22 | echo "ERROR: Go version $(REQUIRE_GO_VERSION) is required for Weave."; \ 23 | exit 1; \ 24 | fi 25 | 26 | # Build settings 27 | LDFLAGS := -X github.com/initia-labs/weave/cmd.Version=$(WEAVE_VERSION) 28 | 29 | dev: check_version 30 | go install -ldflags "$(LDFLAGS) -X github.com/initia-labs/weave/analytics.AmplitudeKey=aba1be3e2335dd5b8b060e977d93410b" . 31 | 32 | # Build targets 33 | build: check_version $(BUILDDIR) 34 | go build -mod=readonly -ldflags "$(LDFLAGS)" -o $(BUILDDIR)/weave . 35 | 36 | install: check_version 37 | go install -ldflags "$(LDFLAGS)" . 38 | 39 | .PHONY: lint lint-fix 40 | 41 | # Run golangci-lint to check code quality 42 | lint: check_version 43 | @command -v golangci-lint >/dev/null 2>&1 || { echo "golangci-lint is required but not installed. Install it by following instructions at https://golangci-lint.run/welcome/install/"; exit 1; } 44 | golangci-lint run --out-format=tab --timeout=15m 45 | 46 | # Run golangci-lint and automatically fix issues where possible (use with caution) 47 | lint-fix: check_version 48 | @echo "Warning: This will automatically modify your files to fix linting issues" 49 | @read -p "Are you sure you want to continue? [y/N] " -n 1 -r; echo; if [[ ! $$REPLY =~ ^[Yy]$$ ]]; then exit 1; fi 50 | @command -v golangci-lint >/dev/null 2>&1 || { echo "golangci-lint is required but not installed. Install it by following instructions at https://golangci-lint.run/welcome/install/"; exit 1; } 51 | golangci-lint run --fix --out-format=tab --timeout=15m 52 | 53 | test: check_version 54 | go clean -testcache 55 | go test -v ./... 56 | 57 | integration-test: check_version 58 | go clean -testcache 59 | go test -v ./... -tags=integration 60 | 61 | test-all: test integration-test 62 | 63 | precommit: lint test-all 64 | 65 | # Release process 66 | release: 67 | @if [ -z "$(release_version)" ]; then \ 68 | echo "ERROR: You must provide a release version. Example: make release v0.0.15"; \ 69 | exit 1; \ 70 | fi 71 | git tag -a $(release_version) -m "$(release_version)" 72 | git push origin $(release_version) 73 | @echo "Paste the release notes below (end with Ctrl+D):" 74 | @notes=$$(cat); \ 75 | gh release create $(release_version) --title "$(release_version)" --notes "$$notes" 76 | 77 | # Development purpose 78 | local: build 79 | clear 80 | ./build/weave opinit init 81 | 82 | # Catch-all target 83 | %: 84 | @: 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image (1)](https://github.com/user-attachments/assets/74ca0671-a4b7-48bc-aded-cca1816a418d) 2 | 3 | # Weave 4 | 5 | Weave is a CLI tool designed to make working with Initia and its Interwoven Rollups easier. Instead of dealing with multiple tools and extensive documentation, 6 | developers can use a single command-line interface for the entire development and deployment workflow. 7 | 8 | Its primary purpose is to solve several key challenges: 9 | 10 | 1. **Infrastructure Management:** Weave can handle all critical infrastructure components within the Interwoven Rollup ecosystem: 11 | - Initia node setup and management (including state sync and chain upgrade management) 12 | - Rollup deployment and configuration 13 | - OPinit bots setup for the Optimistic bridge 14 | - IBC Relayer setup between Initia L1 and your Rollup 15 | 2. **Built for both local development and production deployments:** Weave provides 16 | - Interactive guided setup for step-by-step configuration and 17 | - Configuration file support for automated deployments 18 | 3. **Developer Experience:** Not only does it consolidate multiple complex operations into a single CLI tool, but it also changes how you interact with the tool to set up your configuration. 19 | 20 | ## Prerequisites 21 | 22 | - Operating System: **Linux, macOS** 23 | - Go **v1.23** or higher when building from scratch 24 | - LZ4 compression tool 25 | - For macOS: `brew install lz4` 26 | - For Ubuntu/Debian: `apt-get install lz4` 27 | - For other Linux distributions: Use your package manager to install lz4 28 | 29 | > **Important:** While Weave can run as root, it does not support switching users via commands like `sudo su ubuntu` or `su - someuser`. Instead, directly SSH or log in as the user you intend to run Weave with. For example: 30 | > 31 | > ```bash 32 | > ssh ubuntu@your-server # Good: Direct login as ubuntu user 33 | > ssh root@your-server # Good: Direct login as root 34 | > ``` 35 | > 36 | > This ensures proper handling of user-specific configurations and paths. 37 | 38 | ## Installation 39 | 40 | ### On macOS 41 | 42 | Install _Weave_ via [Homebrew](https://brew.sh/): 43 | 44 | ```bash 45 | brew install initia-labs/tap/weave 46 | ``` 47 | 48 | ### On Linux 49 | 50 | Install _Weave_ by downloading the appropriate binary for your architecture using `wget`: 51 | 52 | **For x86_64 (amd64)** 53 | 54 | ```bash 55 | VERSION=$(curl -s https://api.github.com/repos/initia-labs/weave/releases/latest | grep '"tag_name":' | cut -d'"' -f4 | cut -c 2-) 56 | wget https://github.com/initia-labs/weave/releases/download/v$VERSION/weave-$VERSION-linux-amd64.tar.gz 57 | tar -xvf weave-$VERSION-linux-amd64.tar.gz 58 | ``` 59 | 60 | **For arm64** 61 | 62 | ```bash 63 | VERSION=$(curl -s https://api.github.com/repos/initia-labs/weave/releases/latest | grep '"tag_name":' | cut -d'"' -f4 | cut -c 2-) 64 | wget https://github.com/initia-labs/weave/releases/download/v$VERSION/weave-$VERSION-linux-arm64.tar.gz 65 | tar -xvf weave-$VERSION-linux-arm64.tar.gz 66 | ``` 67 | 68 | ### Building from Scratch 69 | 70 | To build _Weave_ from source, you will need a working Go environment and `make`. Follow these steps: 71 | 72 | ```bash 73 | git clone https://github.com/initia-labs/weave.git 74 | cd weave 75 | VERSION=$(curl -s https://api.github.com/repos/initia-labs/weave/releases/latest | grep '"tag_name":' | cut -d'"' -f4 | cut -c 2-) 76 | git checkout tags/v$VERSION 77 | make install 78 | ``` 79 | 80 | ### Download Pre-built binaries 81 | 82 | Go to the [Releases](https://github.com/initia-labs/weave/releases) page and download the binary for your operating system. 83 | 84 | ### Verify Installation 85 | 86 | ```bash 87 | weave version 88 | ``` 89 | 90 | This should return the version of the Weave binary you have installed. Example output: 91 | 92 | ```bash 93 | vx.x.x # The actual version number will reflect your installed version 94 | ``` 95 | 96 | ## Quick Start 97 | 98 | To get started with Weave, run 99 | 100 | ```bash 101 | weave init 102 | ``` 103 | 104 | It will ask you to set up the [Gas Station](/docs/gas_station.md) account and ask which infrastructure you want to setup. 105 | After that, Weave will guide you through the setup process step-by-step. 106 | 107 | ## Usage 108 | 109 | 1. [Bootstrapping Initia Node](/docs/initia_node.md) 110 | 2. [Launch a new rollup](/docs/rollup_launch.md) 111 | 3. [Setting up IBC relayer](/docs/relayer.md) 112 | 4. [Setting up OPinit bots](/docs/opinit_bots.md) 113 | 114 | ## Usage data collection 115 | 116 | By default, Weave collects non-identifiable usage data to help improve the product. If you prefer not to share this data, you can opt out by running the following command: 117 | 118 | ```bash 119 | weave analytics disable 120 | ``` 121 | 122 | ## Contributing 123 | 124 | We welcome contributions! Please feel free to submit a pull request. 125 | -------------------------------------------------------------------------------- /analytics/amplitude.go: -------------------------------------------------------------------------------- 1 | package analytics 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "time" 8 | 9 | "github.com/amplitude/analytics-go/amplitude" 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/initia-labs/weave/config" 13 | ) 14 | 15 | var ( 16 | Client amplitude.Client 17 | SessionID int64 18 | GlobalEventProperties map[string]interface{} 19 | ) 20 | 21 | type disabledLogger struct{} 22 | 23 | func (d disabledLogger) Debugf(format string, v ...interface{}) {} 24 | 25 | func (d disabledLogger) Errorf(format string, v ...interface{}) {} 26 | 27 | func (d disabledLogger) Infof(format string, v ...interface{}) {} 28 | 29 | func (d disabledLogger) Warnf(format string, v ...interface{}) {} 30 | 31 | func Initialize(weaveVersion string) { 32 | c := amplitude.NewConfig(AmplitudeKey) 33 | c.OptOut = config.AnalyticsOptOut() 34 | c.Logger = disabledLogger{} 35 | 36 | Client = amplitude.NewClient(c) 37 | identify := amplitude.Identify{} 38 | identify.Set("Arch", runtime.GOARCH) 39 | identify.Set("Go Version", runtime.Version()) 40 | Client.Identify(identify, amplitude.EventOptions{ 41 | DeviceID: config.GetAnalyticsDeviceID(), 42 | OSName: runtime.GOOS, 43 | AppVersion: weaveVersion, 44 | }) 45 | 46 | SessionID = time.Now().Unix() 47 | } 48 | 49 | type EventAttributes map[string]interface{} 50 | 51 | // AmplitudeEvent represents an event with some attributes 52 | type AmplitudeEvent struct { 53 | Attributes EventAttributes 54 | } 55 | 56 | func AppendGlobalEventProperties(properties EventAttributes) { 57 | if GlobalEventProperties == nil { 58 | GlobalEventProperties = make(EventAttributes) 59 | } 60 | 61 | for k, v := range properties { 62 | GlobalEventProperties[k] = v 63 | } 64 | } 65 | 66 | // NewEmptyEvent creates and returns an empty event 67 | func NewEmptyEvent() *AmplitudeEvent { 68 | return &AmplitudeEvent{ 69 | Attributes: make(EventAttributes), 70 | } 71 | } 72 | 73 | func TrackEvent(eventType Event, overrideProperties *AmplitudeEvent) { 74 | eventProperties := make(EventAttributes) 75 | for k, v := range GlobalEventProperties { 76 | eventProperties[k] = v 77 | } 78 | 79 | for k, v := range overrideProperties.Attributes { 80 | eventProperties[k] = v 81 | } 82 | 83 | Client.Track(amplitude.Event{ 84 | EventType: string(eventType), 85 | EventOptions: amplitude.EventOptions{ 86 | DeviceID: config.GetAnalyticsDeviceID(), 87 | SessionID: int(SessionID), 88 | }, 89 | EventProperties: eventProperties, 90 | }) 91 | } 92 | 93 | func TrackRunEvent(cmd *cobra.Command, args []string, feature Feature, events *AmplitudeEvent) { 94 | AppendGlobalEventProperties(EventAttributes{ 95 | ComponentEventKey: feature.Component, 96 | FeatureEventKey: feature.Name, 97 | CommandEventKey: cmd.CommandPath(), 98 | }) 99 | 100 | if len(args) > 0 { 101 | for idx, arg := range args { 102 | events.Add(fmt.Sprintf("arg-%d", idx), arg) 103 | } 104 | } 105 | TrackEvent(RunEvent, events) 106 | 107 | // Flush the events to guarantee that run event is the first event 108 | Client.Flush() 109 | } 110 | 111 | func TrackCompletedEvent(feature Feature) { 112 | // Flush the events to guarantee that completed event is the last event 113 | Client.Flush() 114 | 115 | TrackEvent(CompletedEvent, NewEmptyEvent().Add(ComponentEventKey, feature.Component).Add(FeatureEventKey, feature.Name)) 116 | } 117 | 118 | // Add adds a key-value pair to the event's attributes 119 | func (e *AmplitudeEvent) Add(key string, value interface{}) *AmplitudeEvent { 120 | if key != ModelNameKey { 121 | if str, ok := value.(string); ok { 122 | value = strings.ToLower(str) // Convert string value to lowercase 123 | } 124 | } 125 | e.Attributes[key] = value 126 | return e 127 | } 128 | -------------------------------------------------------------------------------- /analytics/analytics_disable.go: -------------------------------------------------------------------------------- 1 | //go:build !test 2 | // +build !test 3 | 4 | package analytics 5 | 6 | import "github.com/amplitude/analytics-go/amplitude" 7 | 8 | // Define a No-Op Client for tests 9 | 10 | // NoOpClient is a no-op implementation of the amplitude.Client interface 11 | type NoOpClient struct{} 12 | 13 | // Track does nothing in the NoOpClient 14 | func (n *NoOpClient) Track(event amplitude.Event) {} 15 | 16 | // Identify does nothing in the NoOpClient 17 | func (n *NoOpClient) Identify(identify amplitude.Identify, eventOptions amplitude.EventOptions) {} 18 | 19 | // GroupIdentify does nothing in the NoOpClient 20 | func (n *NoOpClient) GroupIdentify(groupType string, groupName string, identify amplitude.Identify, eventOptions amplitude.EventOptions) { 21 | } 22 | 23 | // SetGroup does nothing in the NoOpClient 24 | func (n *NoOpClient) SetGroup(groupType string, groupName []string, eventOptions amplitude.EventOptions) { 25 | } 26 | 27 | // Revenue does nothing in the NoOpClient 28 | func (n *NoOpClient) Revenue(revenue amplitude.Revenue, eventOptions amplitude.EventOptions) {} 29 | 30 | // Flush does nothing in the NoOpClient 31 | func (n *NoOpClient) Flush() {} 32 | 33 | // Shutdown does nothing in the NoOpClient 34 | func (n *NoOpClient) Shutdown() {} 35 | 36 | // Add does nothing in the NoOpClient 37 | func (n *NoOpClient) Add(plugin amplitude.Plugin) {} 38 | 39 | // Remove does nothing in the NoOpClient 40 | func (n *NoOpClient) Remove(pluginName string) {} 41 | 42 | // Config returns an empty Config in the NoOpClient 43 | func (n *NoOpClient) Config() amplitude.Config { 44 | return amplitude.Config{} 45 | } 46 | -------------------------------------------------------------------------------- /analytics/constant.go: -------------------------------------------------------------------------------- 1 | package analytics 2 | 3 | var ( 4 | AmplitudeKey = "2f88e103ec6463ae4af81f0ef6c16c09" 5 | ) 6 | 7 | const ( 8 | 9 | // Component 10 | AnalyticsComponent Component = "analytics" 11 | GasStationComponent Component = "gas station" 12 | L1NodeComponent Component = "l1 node" 13 | RollupComponent Component = "rollup" 14 | OPinitComponent Component = "opinit" 15 | RelayerComponent Component = "relayer" 16 | UpgradeComponent Component = "upgrade" 17 | 18 | // EventKeys 19 | ComponentEventKey string = "component" 20 | FeatureEventKey string = "feature" 21 | CommandEventKey string = "command" 22 | OptionEventKey string = "option" 23 | L1ChainIdEventKey string = "l1_chain_id" 24 | ErrorEventKey string = "panic_error" 25 | ModelNameKey string = "model_name" 26 | GenerateKeyfileKey string = "generate_key_file" 27 | KeyFileKey string = "key_file" 28 | WithConfigKey string = "with_config" 29 | VmKey string = "vm" 30 | BotTypeKey string = "bot_type" 31 | 32 | // Event 33 | RunEvent Event = "run" 34 | CompletedEvent Event = "completed" 35 | 36 | // Init Event 37 | InitActionSelected Event = "init_action_selected" 38 | ExistingAppReplaceSelected Event = "existing_app_replace_selected" 39 | L1NetworkSelected Event = "l1_network_selected" 40 | Interrupted Event = "interrupted" 41 | Panicked Event = "panicked" 42 | L1NodeVersionSelected Event = "l1_node_version_selected" 43 | PruningStrategySelected Event = "pruning_strategy_selected" 44 | ExistingGenesisReplaceSelected Event = "existing_genesis_replace_selected" 45 | SyncMethodSelected Event = "sync_method_selected" 46 | CosmovisorAutoUpgradeSelected Event = "cosmovisor_auto_upgrade_selected" 47 | ExistingDataReplaceSelected Event = "existing_data_replace_selected" 48 | FeaturesEnabled Event = "feature_enabled" 49 | 50 | // Rollup Event 51 | VmTypeSelected Event = "vm_type_selected" 52 | OpBridgeBatchSubmissionTargetSelected Event = "op_bridge_batch_submission_selected" 53 | EnableOracleSelected Event = "enable_oracle_selected" 54 | SystemKeysSelected Event = "system_keys_selected" 55 | AccountsFundingPresetSelected Event = "accounts_funding_preset_selected" 56 | AddGasStationToGenesisSelected Event = "add_gas_station_to_genesis_selected" 57 | AddGenesisAccountsSelected Event = "add_genesis_accounts_selected" 58 | 59 | // Opinit Event 60 | OPInitBotInitSelected Event = "opinit_bot_init_selected" 61 | ResetDBSelected Event = "reset_db_selected" 62 | UseCurrentConfigSelected Event = "use_current_config_selected" 63 | PrefillFromArtifactsSelected Event = "prefill_from_artifacts_selected" 64 | L1PrefillSelected Event = "l1_prefill_selected" 65 | DALayerSelected Event = "da_layer_selected" 66 | ImportKeysFromArtifactsSelected Event = "import_keys_from_artifacts_selected" 67 | RecoverKeySelected Event = "recover_key_selected" 68 | 69 | // Relayer 70 | RelayerRollupSelected Event = "relayer_rollup_selected" 71 | RelayerL2Selected Event = "relayer_l2_selected" 72 | SettingUpIBCChannelsMethodSelected Event = "setting_up_ibc_channels_method_selected" 73 | IBCChannelsSelected Event = "ibc_channel_selected" 74 | UseChallengerKeySelected Event = "use_challenger_key_selected" 75 | 76 | // GasStation 77 | GasStationMethodSelected Event = "gas_station_method_selected" 78 | ) 79 | 80 | var ( 81 | SetupL1NodeFeature Feature = Feature{Name: "setup l1 node", Component: L1NodeComponent} 82 | RollupLaunchFeature Feature = Feature{Name: "launch rollup", Component: RollupComponent} 83 | SetupOPinitBotFeature Feature = Feature{Name: "setup opinit bot", Component: OPinitComponent} 84 | SetupOPinitKeysFeature Feature = Feature{Name: "setup opinit keys", Component: OPinitComponent} 85 | ResetOPinitBotFeature Feature = Feature{Name: "reset opinit bot", Component: OPinitComponent} 86 | SetupGasStationFeature Feature = Feature{Name: "setup gas station", Component: GasStationComponent} 87 | SetupRelayerFeature Feature = Feature{Name: "setup relayer", Component: RelayerComponent} 88 | OptOutAnalyticsFeature Feature = Feature{Name: "opt-out analytics", Component: AnalyticsComponent} 89 | ) 90 | 91 | type Component string 92 | 93 | type Feature struct { 94 | Name string 95 | Component Component 96 | } 97 | 98 | type Event string 99 | -------------------------------------------------------------------------------- /client/graphql.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // GraphQLClient defines the logic for interacting with GraphQL APIs. 9 | type GraphQLClient struct { 10 | httpClient *HTTPClient 11 | baseURL string 12 | } 13 | 14 | // NewGraphQLClient creates and returns a new GraphQLClient instance. 15 | func NewGraphQLClient(baseURL string, httpClient *HTTPClient) *GraphQLClient { 16 | return &GraphQLClient{ 17 | httpClient: httpClient, 18 | baseURL: baseURL, 19 | } 20 | } 21 | 22 | // Query sends a GraphQL query to the API and decodes the response into the result. 23 | func (c *GraphQLClient) Query(query string, variables map[string]interface{}, result interface{}) error { 24 | payload := map[string]interface{}{ 25 | "query": query, 26 | "variables": variables, 27 | } 28 | payloadBytes, err := json.Marshal(payload) 29 | if err != nil { 30 | return fmt.Errorf("failed to marshal GraphQL payload: %w", err) 31 | } 32 | 33 | headers := map[string]string{ 34 | "Content-Type": "application/json", 35 | } 36 | 37 | _, err = c.httpClient.Post(c.baseURL, "", headers, payloadBytes, result) 38 | if err != nil { 39 | return fmt.Errorf("failed to perform GraphQL query: %w", err) 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /client/graphql_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // TestGraphQLClient_Query_Success tests a successful GraphQL query. 12 | func TestGraphQLClient_Query_Success(t *testing.T) { 13 | mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | assert.Equal(t, "application/json", r.Header.Get("Content-Type")) 15 | assert.Equal(t, http.MethodPost, r.Method) 16 | 17 | w.Header().Set("Content-Type", "application/json") 18 | w.WriteHeader(http.StatusOK) 19 | _, err := w.Write([]byte(`{"data":{"message":"GraphQL query successful"}}`)) 20 | assert.NoError(t, err) 21 | })) 22 | defer mockServer.Close() 23 | 24 | httpClient := NewHTTPClient() 25 | graphqlClient := NewGraphQLClient(mockServer.URL, httpClient) 26 | 27 | query := ` 28 | query GetMessage { 29 | message 30 | } 31 | ` 32 | variables := map[string]interface{}{} 33 | var result struct { 34 | Data struct { 35 | Message string `json:"message"` 36 | } `json:"data"` 37 | } 38 | 39 | err := graphqlClient.Query(query, variables, &result) 40 | 41 | assert.NoError(t, err) 42 | assert.Equal(t, "GraphQL query successful", result.Data.Message) 43 | } 44 | 45 | // TestGraphQLClient_Query_Error tests a GraphQL query with an error response. 46 | func TestGraphQLClient_Query_Error(t *testing.T) { 47 | mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 | w.Header().Set("Content-Type", "application/json") 49 | w.WriteHeader(http.StatusBadRequest) 50 | _, err := w.Write([]byte(`{"errors":[{"message":"Invalid query"}]}`)) 51 | assert.NoError(t, err) 52 | })) 53 | defer mockServer.Close() 54 | 55 | httpClient := NewHTTPClient() 56 | graphqlClient := NewGraphQLClient(mockServer.URL, httpClient) 57 | 58 | query := ` 59 | query InvalidQuery { 60 | invalidField 61 | } 62 | ` 63 | variables := map[string]interface{}{} 64 | var result struct { 65 | Data interface{} `json:"data"` 66 | } 67 | 68 | err := graphqlClient.Query(query, variables, &result) 69 | 70 | assert.Error(t, err) 71 | assert.Contains(t, err.Error(), "failed to perform GraphQL query") 72 | } 73 | -------------------------------------------------------------------------------- /client/grpc.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "time" 9 | 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials" 12 | "google.golang.org/grpc/credentials/insecure" 13 | "google.golang.org/grpc/reflection/grpc_reflection_v1" 14 | ) 15 | 16 | const ( 17 | DefaultTimeout = 3 * time.Second 18 | ) 19 | 20 | // GRPCClient defines the logic for making gRPC requests. 21 | type GRPCClient struct{} 22 | 23 | // NewGRPCClient initializes and returns a new GRPCClient instance. 24 | func NewGRPCClient() *GRPCClient { 25 | return &GRPCClient{} 26 | } 27 | 28 | // CheckHealth attempts to connect to the server and uses the reflection service to verify the server is up. 29 | func (g *GRPCClient) CheckHealth(serverAddr string) error { 30 | serverAddr = strings.TrimPrefix(serverAddr, "grpc://") 31 | 32 | _, port, err := net.SplitHostPort(serverAddr) 33 | if err != nil { 34 | return fmt.Errorf("invalid grpc server address: %w", err) 35 | } 36 | 37 | var opts []grpc.DialOption 38 | if port == "443" { 39 | opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(nil))) 40 | } else { 41 | opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) 42 | } 43 | 44 | conn, err := grpc.Dial(serverAddr, opts...) 45 | if err != nil { 46 | return fmt.Errorf("failed to connect to gRPC server: %v", err) 47 | } 48 | defer conn.Close() 49 | 50 | reflectionClient := grpc_reflection_v1.NewServerReflectionClient(conn) 51 | 52 | ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) 53 | defer cancel() 54 | 55 | stream, err := reflectionClient.ServerReflectionInfo(ctx) 56 | if err != nil { 57 | return fmt.Errorf("failed to start reflection stream: %v", err) 58 | } 59 | 60 | req := &grpc_reflection_v1.ServerReflectionRequest{ 61 | MessageRequest: &grpc_reflection_v1.ServerReflectionRequest_ListServices{ 62 | ListServices: "", 63 | }, 64 | } 65 | 66 | if err = stream.Send(req); err != nil { 67 | return fmt.Errorf("failed to send reflection request: %v", err) 68 | } 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /client/grpc_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | // MockServerReflectionClient is a mock of the grpc_reflection_v1.ServerReflectionServer interface. 13 | type MockServerReflectionClient struct { 14 | mock.Mock 15 | } 16 | 17 | // MockServerReflectionInfoServer is the mock server stream for ServerReflectionInfo. 18 | type MockServerReflectionInfoServer struct { 19 | mock.Mock 20 | grpc.ServerStream 21 | } 22 | 23 | func (m *MockServerReflectionInfoServer) SendMsg(mess interface{}) error { 24 | args := m.Called(mess) 25 | return args.Error(0) 26 | } 27 | 28 | func (m *MockServerReflectionInfoServer) RecvMsg(mess interface{}) error { 29 | args := m.Called(mess) 30 | return args.Error(0) 31 | } 32 | 33 | // Test the CheckHealth method with a mock gRPC server 34 | func TestGRPCClient_CheckHealth_Success(t *testing.T) { 35 | mockServerStream := new(MockServerReflectionInfoServer) 36 | mockClient := new(MockServerReflectionClient) 37 | 38 | mockClient.On("ServerReflectionInfo", mockServerStream).Return(nil) 39 | mockServerStream.On("SendMsg", mock.Anything).Return(nil) 40 | mockServerStream.On("RecvMsg", mock.Anything).Return(nil) 41 | 42 | server := grpc.NewServer() 43 | 44 | lis, err := net.Listen("tcp", "localhost:9090") 45 | if err != nil { 46 | t.Fatalf("failed to listen on port: %v", err) 47 | } 48 | 49 | go func() { 50 | if err := server.Serve(lis); err != nil { 51 | t.Errorf("failed to serve mock gRPC server: %v", err) 52 | return 53 | } 54 | }() 55 | defer server.Stop() 56 | 57 | serverAddr := "localhost:9090" 58 | 59 | client := NewGRPCClient() 60 | err = client.CheckHealth(serverAddr) 61 | if err != nil { 62 | t.Fatalf("Health check failed: %v", err) 63 | } 64 | assert.NoError(t, err) 65 | } 66 | -------------------------------------------------------------------------------- /client/http_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestHTTPClient_Get_Success(t *testing.T) { 13 | // Create a mock HTTP server 14 | mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | w.Header().Set("Content-Type", "application/json") 16 | w.WriteHeader(http.StatusOK) 17 | _, err := w.Write([]byte(`{"message":"success"}`)) 18 | if err != nil { 19 | t.Fatalf("Error writing response: %v", err) 20 | } 21 | })) 22 | defer mockServer.Close() 23 | 24 | httpClient := NewHTTPClient() 25 | 26 | // Test successful GET request 27 | var result map[string]string 28 | _, err := httpClient.Get(mockServer.URL, "", nil, &result) 29 | 30 | assert.NoError(t, err) 31 | assert.Equal(t, "success", result["message"]) 32 | } 33 | 34 | // TestHTTPClient_Get_Failure tests a GET request that returns an error 35 | func TestHTTPClient_Get_Failure(t *testing.T) { 36 | // Create a mock HTTP server that returns a bad status code 37 | mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 38 | w.WriteHeader(http.StatusInternalServerError) 39 | })) 40 | defer mockServer.Close() 41 | 42 | httpClient := NewHTTPClient() 43 | 44 | // Test GET failure with status code 500 45 | _, err := httpClient.Get(mockServer.URL, "", nil, nil) 46 | 47 | assert.Error(t, err) 48 | assert.Contains(t, err.Error(), "unexpected status code") 49 | } 50 | 51 | // TestHTTPClient_DownloadFile_Success tests a successful file download 52 | func TestHTTPClient_DownloadFile_Success(t *testing.T) { 53 | // Mock file download behavior 54 | mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | w.Header().Set("Content-Type", "application/octet-stream") 56 | w.WriteHeader(http.StatusOK) 57 | _, err := w.Write([]byte("test content")) 58 | if err != nil { 59 | t.Fatalf("Error writing response: %v", err) 60 | } 61 | })) 62 | defer mockServer.Close() 63 | 64 | // Create a temporary file to save the content 65 | destFile, err := os.CreateTemp("", "testfile") 66 | if err != nil { 67 | t.Fatalf("Error creating temp file: %v", err) 68 | } 69 | defer os.Remove(destFile.Name()) 70 | 71 | httpClient := NewHTTPClient() 72 | 73 | // Test DownloadFile method 74 | progress := int64(0) 75 | totalSize := int64(0) 76 | err = httpClient.DownloadFile(mockServer.URL, destFile.Name(), &progress, &totalSize) 77 | 78 | assert.NoError(t, err) 79 | assert.Equal(t, int64(12), totalSize) // The content length of "test content" 80 | // Verify file content 81 | content, err := os.ReadFile(destFile.Name()) 82 | assert.NoError(t, err) 83 | assert.Equal(t, "test content", string(content)) 84 | } 85 | 86 | // TestHTTPClient_DownloadFile_Failure tests a failed file download (non-200 status) 87 | func TestHTTPClient_DownloadFile_Failure(t *testing.T) { 88 | // Create a mock HTTP server that returns a bad status code 89 | mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 | w.WriteHeader(http.StatusInternalServerError) 91 | })) 92 | defer mockServer.Close() 93 | 94 | httpClient := NewHTTPClient() 95 | 96 | // Test DownloadFile failure with status code 500 97 | destFile, err := os.CreateTemp("", "testfile") 98 | if err != nil { 99 | t.Fatalf("Error creating temp file: %v", err) 100 | } 101 | defer os.Remove(destFile.Name()) 102 | 103 | err = httpClient.DownloadFile(mockServer.URL, destFile.Name(), nil, nil) 104 | 105 | assert.Error(t, err) 106 | assert.Contains(t, err.Error(), "failed to download") 107 | } 108 | 109 | // TestHTTPClient_DownloadFile_InvalidURL tests invalid URL handling for file download 110 | func TestHTTPClient_DownloadFile_InvalidURL(t *testing.T) { 111 | httpClient := NewHTTPClient() 112 | 113 | // Test with invalid URL 114 | err := httpClient.DownloadFile("http://invalid-url", "/path/to/file", nil, nil) 115 | 116 | assert.Error(t, err) 117 | assert.Contains(t, err.Error(), "failed to connect") 118 | } 119 | -------------------------------------------------------------------------------- /cmd/analytics.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/initia-labs/weave/analytics" 9 | "github.com/initia-labs/weave/config" 10 | ) 11 | 12 | func AnalyticsCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "analytics", 15 | Short: "Configure analytics e.g. enable/disable data collection", 16 | DisableFlagParsing: true, 17 | SuggestionsMinimumDistance: 2, 18 | } 19 | 20 | cmd.AddCommand( 21 | AnalyticsEnableCommand(), 22 | AnalyticsDisableCommand(), 23 | ) 24 | 25 | return cmd 26 | } 27 | 28 | func AnalyticsEnableCommand() *cobra.Command { 29 | enableCmd := &cobra.Command{ 30 | Use: "enable", 31 | Short: "Allow Weave to collect analytics data", 32 | RunE: func(cmd *cobra.Command, args []string) error { 33 | err := config.SetAnalyticsOptOut(false) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | // Initialize the analytics client so the event is tracked 39 | analytics.Initialize(Version) 40 | 41 | fmt.Println("Analytics enabled") 42 | return nil 43 | }, 44 | } 45 | 46 | return enableCmd 47 | } 48 | 49 | func AnalyticsDisableCommand() *cobra.Command { 50 | disableCmd := &cobra.Command{ 51 | Use: "disable", 52 | Short: "Do not allow Weave to collect analytics data", 53 | RunE: func(cmd *cobra.Command, args []string) error { 54 | analytics.TrackRunEvent(cmd, args, analytics.OptOutAnalyticsFeature, analytics.NewEmptyEvent()) 55 | 56 | err := config.SetAnalyticsOptOut(true) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | fmt.Println("Analytics disabled") 62 | analytics.TrackCompletedEvent(analytics.OptOutAnalyticsFeature) 63 | return nil 64 | }, 65 | } 66 | 67 | return disableCmd 68 | } 69 | -------------------------------------------------------------------------------- /cmd/flags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | const ( 4 | FlagForce = "force" 5 | FlagN = "n" 6 | FlagVm = "vm" 7 | FlagDetach = "detach" 8 | 9 | FlagInitiaHome = "initia-dir" 10 | FlagMinitiaHome = "minitia-dir" 11 | FlagOPInitHome = "opinit-dir" 12 | 13 | FlagPollingInterval = "polling-interval" 14 | 15 | FlagUpdateClient = "update-client" 16 | 17 | FlagWithConfig = "with-config" 18 | FlagKeyFile = "key-file" 19 | FlagGenerateKeyFile = "generate-key-file" 20 | ) 21 | -------------------------------------------------------------------------------- /cmd/gas_station_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package cmd_test 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | 13 | "github.com/initia-labs/weave/common" 14 | "github.com/initia-labs/weave/models" 15 | "github.com/initia-labs/weave/service" 16 | "github.com/initia-labs/weave/testutil" 17 | ) 18 | 19 | func TestGasStationSetup(t *testing.T) { 20 | setup(t, []service.CommandName{}) 21 | defer teardown(t, []service.CommandName{}) 22 | 23 | userHome, _ := os.UserHomeDir() 24 | weaveDir := filepath.Join(userHome, common.WeaveDirectory) 25 | 26 | finalModel := testutil.SetupGasStation(t) 27 | 28 | // Check the final state here 29 | assert.IsType(t, &models.WeaveAppSuccessfullyInitialized{}, finalModel) 30 | 31 | if _, ok := finalModel.(*models.WeaveAppSuccessfullyInitialized); ok { 32 | assert.True(t, ok) 33 | } 34 | 35 | // Check if Weave home has been created 36 | _, err := os.Stat(weaveDir) 37 | assert.Nil(t, err) 38 | 39 | // Assert values 40 | weaveConfig := filepath.Join(weaveDir, "config.json") 41 | testutil.CompareJsonValue(t, weaveConfig, "common.gas_station.mnemonic", testutil.GasStationMnemonic) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/helper_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package cmd_test 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "slices" 11 | "testing" 12 | 13 | "github.com/initia-labs/weave/analytics" 14 | "github.com/initia-labs/weave/common" 15 | "github.com/initia-labs/weave/service" 16 | ) 17 | 18 | const ( 19 | weaveDirectoryBackup = ".weave_backup" 20 | hermesDirectory = ".hermes" 21 | hermesDirectoryBackup = ".hermes_backup" 22 | 23 | TestMinitiaHome = ".minitia.weave.test" 24 | TestOPInitHome = ".opinit.weave.test" 25 | TestInitiaHome = ".initia.weave.test" 26 | ) 27 | 28 | func getServiceFilePathAndBackupFilePath(serviceName service.CommandName) (string, string, error) { 29 | s, err := service.NewService(service.UpgradableInitia) 30 | if err != nil { 31 | return "", "", fmt.Errorf("failed to create service: %v", err) 32 | } 33 | 34 | serviceFilePath, err := s.GetServiceFile() 35 | if err != nil { 36 | return "", "", fmt.Errorf("failed to get service file: %v", err) 37 | } 38 | 39 | backupServiceFilePath := serviceFilePath + ".backup" 40 | 41 | return serviceFilePath, backupServiceFilePath, nil 42 | } 43 | 44 | func backupServiceFiles(services []service.CommandName) error { 45 | for _, serviceName := range services { 46 | serviceFilePath, backupServiceFilePath, err := getServiceFilePathAndBackupFilePath(serviceName) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if _, err := os.Stat(serviceFilePath); os.IsNotExist(err) { 52 | continue 53 | } 54 | 55 | fmt.Printf("Backing up service file %s to %s\n", serviceFilePath, backupServiceFilePath) 56 | 57 | if err := os.Rename(serviceFilePath, backupServiceFilePath); err != nil { 58 | return fmt.Errorf("failed to backup service file: %v", err) 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func restoreServiceFiles(services []service.CommandName) error { 66 | for _, serviceName := range services { 67 | serviceFilePath, backupServiceFilePath, err := getServiceFilePathAndBackupFilePath(serviceName) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if _, err := os.Stat(backupServiceFilePath); os.IsNotExist(err) { 73 | // remove the service file if the backup file does not exist 74 | os.Remove(serviceFilePath) 75 | continue 76 | } 77 | 78 | if err := os.Rename(backupServiceFilePath, serviceFilePath); err != nil { 79 | return fmt.Errorf("failed to restore service file: %v", err) 80 | } 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func setup(t *testing.T, services []service.CommandName) { 87 | // disable analytics 88 | analytics.Client = &analytics.NoOpClient{} 89 | 90 | userHome, _ := os.UserHomeDir() 91 | weaveDir := filepath.Join(userHome, common.WeaveDirectory) 92 | weaveDirBackup := filepath.Join(userHome, weaveDirectoryBackup) 93 | if _, err := os.Stat(weaveDir); !os.IsNotExist(err) { 94 | // remove the backup directory if it exists 95 | os.RemoveAll(weaveDirBackup) 96 | // rename the weave directory to back up 97 | fmt.Println("Backing up weave directory") 98 | 99 | if err := os.Rename(weaveDir, weaveDirBackup); err != nil { 100 | t.Fatalf("failed to backup weave directory: %v", err) 101 | } 102 | } 103 | 104 | if slices.Contains(services, service.Relayer) { 105 | relayerDir := filepath.Join(userHome, hermesDirectory) 106 | relayerDirBackup := filepath.Join(userHome, hermesDirectoryBackup) 107 | if _, err := os.Stat(relayerDir); !os.IsNotExist(err) { 108 | // remove the backup directory if it exists 109 | os.RemoveAll(relayerDirBackup) 110 | // rename the hermes directory to back up 111 | fmt.Println("Backing up hermes directory") 112 | 113 | if err := os.Rename(relayerDir, relayerDirBackup); err != nil { 114 | t.Fatalf("failed to backup hermes directory: %v", err) 115 | } 116 | } 117 | } 118 | 119 | // move service files to backup 120 | err := backupServiceFiles(services) 121 | if err != nil { 122 | t.Fatalf(err.Error()) 123 | } 124 | } 125 | 126 | func teardown(t *testing.T, services []service.CommandName) { 127 | userHome, _ := os.UserHomeDir() 128 | weaveDir := filepath.Join(userHome, common.WeaveDirectory) 129 | weaveDirBackup := filepath.Join(userHome, weaveDirectoryBackup) 130 | if _, err := os.Stat(weaveDirBackup); !os.IsNotExist(err) { 131 | fmt.Println("Restoring weave directory") 132 | os.RemoveAll(weaveDir) 133 | os.Rename(weaveDirBackup, weaveDir) 134 | } 135 | 136 | if slices.Contains(services, service.Relayer) { 137 | relayerDir := filepath.Join(userHome, hermesDirectory) 138 | relayerDirBackup := filepath.Join(userHome, hermesDirectoryBackup) 139 | if _, err := os.Stat(relayerDirBackup); !os.IsNotExist(err) { 140 | fmt.Println("Restoring hermes directory") 141 | os.RemoveAll(relayerDir) 142 | os.Rename(relayerDirBackup, relayerDir) 143 | } 144 | } 145 | 146 | // restore service files 147 | err := restoreServiceFiles(services) 148 | if err != nil { 149 | t.Fatalf(err.Error()) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /cmd/helper_text.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "fmt" 4 | 5 | const ( 6 | DocsURLPrefix = "https://github.com/initia-labs/weave/blob/main/" 7 | ) 8 | 9 | func SubHelperText(docPath string) string { 10 | return fmt.Sprintf("See %s%s for more information about the setup process and potential issues.", DocsURLPrefix, docPath) 11 | } 12 | 13 | var ( 14 | WeaveHelperText = fmt.Sprintf("Weave is the CLI for managing Initia deployments.\n\nSee %sREADME.md for more information.", DocsURLPrefix) 15 | L1NodeHelperText = SubHelperText("docs/initia_node.md") 16 | RollupHelperText = SubHelperText("docs/rollup_launch.md") 17 | OPinitBotsHelperText = SubHelperText("docs/opinit_bots.md") 18 | RelayerHelperText = SubHelperText("docs/relayer.md") 19 | GasStationHelperText = SubHelperText("docs/gas_station.md") 20 | ) 21 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | "github.com/spf13/cobra" 8 | 9 | "github.com/initia-labs/weave/analytics" 10 | "github.com/initia-labs/weave/config" 11 | weavecontext "github.com/initia-labs/weave/context" 12 | "github.com/initia-labs/weave/models" 13 | "github.com/initia-labs/weave/models/weaveinit" 14 | ) 15 | 16 | func InitCommand() *cobra.Command { 17 | initCmd := &cobra.Command{ 18 | Use: "init", 19 | Short: "Initialize Weave CLI, funding gas station and setting up config.", 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | analytics.TrackEvent(analytics.RunEvent, analytics.NewEmptyEvent().Add(analytics.CommandEventKey, cmd.CommandPath())) 22 | if config.IsFirstTimeSetup() { 23 | ctx := weavecontext.NewAppContext(models.NewExistingCheckerState()) 24 | 25 | // Capture both the final model and the error from Run() 26 | if finalModel, err := tea.NewProgram(models.NewGasStationMethodSelect(ctx), tea.WithAltScreen()).Run(); err != nil { 27 | return err 28 | } else { 29 | fmt.Println(finalModel.View()) 30 | if _, ok := finalModel.(*models.WeaveAppSuccessfullyInitialized); !ok { 31 | return nil 32 | } 33 | } 34 | } 35 | 36 | if finalModel, err := tea.NewProgram(weaveinit.NewWeaveInit(), tea.WithAltScreen()).Run(); err != nil { 37 | return err 38 | } else { 39 | fmt.Println(finalModel.View()) 40 | return nil 41 | } 42 | }, 43 | } 44 | 45 | return initCmd 46 | } 47 | -------------------------------------------------------------------------------- /cmd/relayer_integration_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package cmd_test 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/assert" 15 | 16 | "github.com/initia-labs/weave/common" 17 | weavecontext "github.com/initia-labs/weave/context" 18 | "github.com/initia-labs/weave/models/relayer" 19 | "github.com/initia-labs/weave/registry" 20 | "github.com/initia-labs/weave/service" 21 | "github.com/initia-labs/weave/testutil" 22 | ) 23 | 24 | func setupRelayer(t *testing.T) context.Context { 25 | setup(t, []service.CommandName{service.Relayer}) 26 | 27 | ctx := weavecontext.NewAppContext(relayer.NewRelayerState()) 28 | return weavecontext.SetMinitiaHome(ctx, TestMinitiaHome) 29 | } 30 | 31 | func teardownRelayer(t *testing.T) { 32 | teardown(t, []service.CommandName{service.Relayer}) 33 | } 34 | 35 | func TestRelayerInit(t *testing.T) { 36 | ctx := setupRelayer(t) 37 | defer teardownRelayer(t) 38 | 39 | firstModel, err := relayer.NewRollupSelect(ctx) 40 | assert.Nil(t, err) 41 | 42 | miniEvmIdx := -1 43 | networks, err := registry.GetAllL2AvailableNetwork(registry.InitiaL1Testnet) 44 | if err != nil { 45 | t.Fatalf("get all l2 available networks: %v", err) 46 | } 47 | 48 | sort.Slice(networks, func(i, j int) bool { 49 | return strings.ToLower(networks[i].PrettyName) < strings.ToLower(networks[j].PrettyName) 50 | }) 51 | for i, network := range networks { 52 | if strings.EqualFold(network.PrettyName, "Minievm") { 53 | miniEvmIdx = i 54 | break 55 | } 56 | } 57 | 58 | if miniEvmIdx == -1 { 59 | t.Fatalf("'Minievm' not found in networks") 60 | } 61 | 62 | pressDownSteps := make(testutil.Steps, miniEvmIdx) 63 | for i := 0; i < miniEvmIdx; i++ { 64 | pressDownSteps[i] = testutil.PressDown 65 | } 66 | 67 | steps := testutil.Steps{ 68 | testutil.PressEnter, // press enter to confirm selecting whitelisted rollups 69 | testutil.PressEnter, // press enter to confirm selecting testnet 70 | testutil.WaitFetching, // wait fetching rollup networks 71 | } 72 | steps = append(steps, pressDownSteps...) // press down until its minievm 73 | steps = append(steps, testutil.Steps{ 74 | testutil.PressEnter, // press enter to confirm selecting minievm 75 | testutil.PressSpace, // press space to select relaying all channels 76 | testutil.PressEnter, // press enter to confirm the selection 77 | testutil.WaitFor(func() bool { 78 | userHome, _ := os.UserHomeDir() 79 | if _, err := os.Stat(filepath.Join(userHome, common.HermesHome, "config.toml")); os.IsNotExist(err) { 80 | return false 81 | } 82 | return true 83 | }), // wait for relayer config to be created 84 | testutil.PressEnter, // press enter to confirm generating new key on l1 85 | testutil.PressDown, // press down once to select generate key on l2 86 | testutil.PressEnter, // press enter to confirm the selection 87 | testutil.WaitFetching, // wait for the generation of keys 88 | testutil.TypeText("continue"), // type to proceed after the mnemonic display page 89 | testutil.PressEnter, // press enter to confirm the typing 90 | testutil.WaitFetching, // wait for account balances fetching 91 | testutil.WaitFetching, // wait for account balances fetching 92 | testutil.PressDown, // press down once to move the selector 93 | testutil.PressDown, // press down again to move the selector to skip the funding 94 | testutil.PressEnter, // press enter to confirm the selection 95 | }...) 96 | 97 | finalModel := testutil.RunProgramWithSteps(t, firstModel, steps) 98 | 99 | // Check the final state here 100 | assert.IsType(t, &relayer.TerminalState{}, finalModel) 101 | 102 | if _, ok := finalModel.(*relayer.TerminalState); ok { 103 | assert.True(t, ok) 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os/exec" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | 11 | "github.com/initia-labs/weave/analytics" 12 | "github.com/initia-labs/weave/config" 13 | ) 14 | 15 | var Version string 16 | 17 | func Execute() error { 18 | rootCmd := &cobra.Command{ 19 | Use: "weave", 20 | Long: WeaveHelperText, 21 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 22 | viper.AutomaticEnv() 23 | viper.SetEnvPrefix("weave") 24 | if err := config.InitializeConfig(); err != nil { 25 | return err 26 | } 27 | analytics.Initialize(Version) 28 | 29 | // Skip LZ4 check for certain commands that don't need it 30 | if cmd.Name() != "version" && cmd.Name() != "analytics" { 31 | if _, err := exec.LookPath("lz4"); err != nil { 32 | return fmt.Errorf("lz4 is not installed. Please install it first:\n" + 33 | "- For macOS: Run 'brew install lz4'\n" + 34 | "- For Ubuntu/Debian: Run 'apt-get install lz4'\n" + 35 | "- For other Linux distributions: Use your package manager to install lz4") 36 | } 37 | } 38 | 39 | return nil 40 | }, 41 | PersistentPostRunE: func(cmd *cobra.Command, args []string) error { 42 | analytics.Client.Flush() 43 | analytics.Client.Shutdown() 44 | return nil 45 | }, 46 | } 47 | 48 | rootCmd.AddCommand( 49 | InitCommand(), 50 | InitiaCommand(), 51 | GasStationCommand(), 52 | VersionCommand(), 53 | UpgradeCommand(), 54 | MinitiaCommand(), 55 | OPInitBotsCommand(), 56 | RelayerCommand(), 57 | AnalyticsCommand(), 58 | ) 59 | 60 | return rootCmd.ExecuteContext(context.Background()) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/validate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | weaveio "github.com/initia-labs/weave/io" 9 | "github.com/initia-labs/weave/service" 10 | ) 11 | 12 | func isInitiated(cmd service.CommandName) func(_ *cobra.Command, _ []string) error { 13 | return func(_ *cobra.Command, _ []string) error { 14 | prettyName, prettyErr := cmd.GetPrettyName() 15 | if prettyErr != nil { 16 | return fmt.Errorf("could not get pretty name: %w", prettyErr) 17 | } 18 | 19 | if err := func() error { 20 | service, err := service.NewService(cmd) 21 | if err != nil { 22 | return fmt.Errorf("could not create %v service: %w", prettyName, err) 23 | } 24 | 25 | serviceFile, err := service.GetServiceFile() 26 | if err != nil { 27 | return fmt.Errorf("could not get service file for %s: %w", prettyName, err) 28 | } 29 | 30 | if !weaveio.FileOrFolderExists(serviceFile) { 31 | return fmt.Errorf("service file %s not found", serviceFile) 32 | } 33 | 34 | serviceBinary, serviceHome, err := service.GetServiceBinaryAndHome() 35 | if err != nil { 36 | return fmt.Errorf("could not determine %v binary and home directory: %w", prettyName, err) 37 | } 38 | 39 | if !weaveio.FileOrFolderExists(serviceHome) { 40 | return fmt.Errorf("home directory %s not found", serviceHome) 41 | } 42 | 43 | if !weaveio.FileOrFolderExists(serviceBinary) { 44 | return fmt.Errorf("%s binary not found at %s", prettyName, serviceBinary) 45 | } 46 | 47 | return nil 48 | }(); err != nil { 49 | initCmd, initErr := cmd.GetInitCommand() 50 | if initErr != nil { 51 | return fmt.Errorf("could not get init command: %w", initErr) 52 | } 53 | 54 | return fmt.Errorf("weave %s is not properly configured: %w: run `weave %s` to setup", prettyName, err, initCmd) 55 | } 56 | 57 | return nil 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /common/constants.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | WeaveDirectory = ".weave" 5 | WeaveConfigFile = WeaveDirectory + "/config.json" 6 | WeaveDataDirectory = WeaveDirectory + "/data" 7 | WeaveLogDirectory = WeaveDirectory + "/log" 8 | 9 | SnapshotFilename = "snapshot.weave" 10 | 11 | InitiaDirectory = ".initia" 12 | InitiaConfigDirectory = "/config" 13 | InitiaDataDirectory = "/data" 14 | 15 | WeaveGasStationKeyName = "weave.GasStation" 16 | 17 | MinitiaDirectory = ".minitia" 18 | MinitiaConfigPath = ".minitia/config" 19 | MinitiaArtifactsConfigJson = "/artifacts/config.json" 20 | MinitiaArtifactsJson = "/artifacts/artifacts.json" 21 | 22 | OPinitDirectory = ".opinit" 23 | OPinitAppName = "opinitd" 24 | OPinitKeyFileJson = "/weave.keyfile.json" 25 | OpinitGeneratedKeyFilename = "weave.opinit.generated" 26 | 27 | HermesHome = ".hermes" 28 | HermesKeysDirectory = HermesHome + "/keys" 29 | HermesKeyFileJson = HermesHome + "/weave.keyfile.json" 30 | HermesTempMnemonicFilename = "weave.mnemonic" 31 | ) 32 | -------------------------------------------------------------------------------- /common/strings.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/charmbracelet/x/term" 8 | ) 9 | 10 | const ( 11 | DefaultTerminalWidth = 80 12 | ) 13 | 14 | // CleanString utility function to clean the string by trimming spaces and removing ^M characters 15 | func CleanString(input string) string { 16 | return strings.TrimSpace(strings.ReplaceAll(input, "\r", "")) 17 | } 18 | 19 | func TransformFirstWordUpperCase(input string) string { 20 | words := strings.Fields(input) 21 | if len(words) > 0 { 22 | return strings.ToUpper(words[0]) 23 | } 24 | return "" 25 | } 26 | 27 | func TransformFirstWordLowerCase(input string) string { 28 | words := strings.Fields(input) 29 | if len(words) > 0 { 30 | return strings.ToLower(words[0]) 31 | } 32 | return "" 33 | } 34 | 35 | func WrapText(text string) string { 36 | width, _, err := term.GetSize(os.Stdout.Fd()) 37 | if err != nil { 38 | width = DefaultTerminalWidth 39 | } 40 | return WrapTextWithLimit(text, width) 41 | } 42 | 43 | func WrapTextWithLimit(text string, limit int) string { 44 | var result []string 45 | for len(text) > limit { 46 | result = append(result, text[:limit]) 47 | text = text[limit:] 48 | } 49 | result = append(result, text) 50 | return strings.Join(result, "\n") 51 | } 52 | -------------------------------------------------------------------------------- /common/strings_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTransformFirstWordUpperCase(t *testing.T) { 8 | tests := []struct { 9 | input string 10 | expected string 11 | }{ 12 | {"Celestia", "CELESTIA"}, 13 | {"Initia L1", "INITIA"}, 14 | {"Cosmos Hub", "COSMOS"}, 15 | {"Test123 Interwoven", "TEST123"}, 16 | {" extra spaces ", "EXTRA"}, 17 | {"", ""}, 18 | {" ", ""}, 19 | } 20 | 21 | for _, test := range tests { 22 | output := TransformFirstWordUpperCase(test.input) 23 | if output != test.expected { 24 | t.Errorf("For input '%s', expected '%s', but got '%s'", test.input, test.expected, output) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/initia-labs/weave/io" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/google/uuid" 11 | "github.com/spf13/viper" 12 | 13 | "github.com/initia-labs/weave/common" 14 | ) 15 | 16 | var DevMode string 17 | 18 | func InitializeConfig() error { 19 | homeDir, err := os.UserHomeDir() 20 | if err != nil { 21 | return fmt.Errorf("failed to get user home directory: %v", err) 22 | } 23 | 24 | configPath := filepath.Join(homeDir, common.WeaveConfigFile) 25 | if err := os.MkdirAll(filepath.Dir(configPath), os.ModePerm); err != nil { 26 | return fmt.Errorf("failed to create config directory: %v", err) 27 | } 28 | 29 | dataPath := filepath.Join(homeDir, common.WeaveDataDirectory) 30 | if err := os.MkdirAll(dataPath, os.ModePerm); err != nil { 31 | return fmt.Errorf("failed to create data directory: %v", err) 32 | } 33 | 34 | logPath := filepath.Join(homeDir, common.WeaveLogDirectory) 35 | if err := os.MkdirAll(logPath, os.ModePerm); err != nil { 36 | return fmt.Errorf("failed to create log directory: %v", err) 37 | } 38 | 39 | viper.SetConfigFile(configPath) 40 | viper.SetConfigType("json") 41 | 42 | if _, err := os.Stat(configPath); os.IsNotExist(err) { 43 | if err := createDefaultConfigFile(configPath); err != nil { 44 | return fmt.Errorf("failed to create default config file: %v", err) 45 | } 46 | } 47 | 48 | return LoadConfig() 49 | } 50 | 51 | func createDefaultConfigFile(filePath string) error { 52 | file, err := os.Create(filePath) 53 | if err != nil { 54 | return fmt.Errorf("failed to create config file: %v", err) 55 | } 56 | defer file.Close() 57 | 58 | _, err = file.WriteString(DefaultConfigTemplate) 59 | if err != nil { 60 | return fmt.Errorf("failed to write to config file: %v", err) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func LoadConfig() error { 67 | if err := viper.ReadInConfig(); err != nil { 68 | return fmt.Errorf("failed to read config file: %v", err) 69 | } 70 | return nil 71 | } 72 | 73 | func GetConfig(key string) interface{} { 74 | return viper.Get(key) 75 | } 76 | 77 | func SetConfig(key string, value interface{}) error { 78 | viper.Set(key, value) 79 | return WriteConfig() 80 | } 81 | 82 | func WriteConfig() error { 83 | if err := viper.WriteConfig(); err != nil { 84 | return fmt.Errorf("failed to write config file: %v", err) 85 | } 86 | return nil 87 | } 88 | 89 | func IsFirstTimeSetup() bool { 90 | return viper.Get("common.gas_station") == nil 91 | } 92 | 93 | func GetGasStationKey() (*GasStationKey, error) { 94 | if IsFirstTimeSetup() { 95 | return nil, fmt.Errorf("gas station key not exists") 96 | } 97 | 98 | data := GetConfig("common.gas_station") 99 | jsonData, err := json.Marshal(data) 100 | if err != nil { 101 | return nil, fmt.Errorf("failed to marshal json: %v", err) 102 | } 103 | 104 | var gasKey GasStationKey 105 | err = json.Unmarshal(jsonData, &gasKey) 106 | if err != nil { 107 | return nil, fmt.Errorf("failed to unmarshal json: %v", err) 108 | } 109 | 110 | return &gasKey, nil 111 | } 112 | 113 | func AnalyticsOptOut() bool { 114 | // In dev mode, always opt out 115 | if DevMode == "true" { 116 | return true 117 | } 118 | 119 | if GetConfig("common.analytics_opt_out") == nil { 120 | _ = SetConfig("common.analytics_opt_out", false) 121 | return false 122 | } 123 | 124 | return GetConfig("common.analytics_opt_out").(bool) 125 | } 126 | 127 | func GetAnalyticsDeviceID() string { 128 | if GetConfig("common.analytics_device_id") == nil { 129 | deviceID := uuid.New().String() 130 | _ = SetConfig("common.analytics_device_id", deviceID) 131 | return deviceID 132 | } 133 | 134 | return GetConfig("common.analytics_device_id").(string) 135 | } 136 | 137 | func SetAnalyticsOptOut(optOut bool) error { 138 | return SetConfig("common.analytics_opt_out", optOut) 139 | } 140 | 141 | const DefaultConfigTemplate = `{}` 142 | 143 | type GasStationKey struct { 144 | InitiaAddress string `json:"initia_address"` 145 | CelestiaAddress string `json:"celestia_address"` 146 | Mnemonic string `json:"mnemonic"` 147 | } 148 | 149 | func RecoverGasStationKey(mnemonic string) (*GasStationKey, error) { 150 | initiaKey, err := io.RecoverKey("init", mnemonic) 151 | if err != nil { 152 | return nil, fmt.Errorf("failed to recover initia gas station key: %v", err) 153 | } 154 | 155 | celestiaKey, err := io.RecoverKey("celestia", mnemonic) 156 | if err != nil { 157 | return nil, fmt.Errorf("failed to recover celestia gas station key: %v", err) 158 | } 159 | 160 | return &GasStationKey{ 161 | InitiaAddress: initiaKey.Address, 162 | CelestiaAddress: celestiaKey.Address, 163 | Mnemonic: mnemonic, 164 | }, nil 165 | } 166 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | 12 | "github.com/initia-labs/weave/common" 13 | ) 14 | 15 | // Mocking the os package functions 16 | type MockedFilesystem struct { 17 | mock.Mock 18 | } 19 | 20 | func (m *MockedFilesystem) UserHomeDir() (string, error) { 21 | args := m.Called() 22 | return args.String(0), args.Error(1) 23 | } 24 | 25 | func (m *MockedFilesystem) MkdirAll(path string, perm os.FileMode) error { 26 | args := m.Called(path, perm) 27 | return args.Error(0) 28 | } 29 | 30 | func (m *MockedFilesystem) Create(path string) (*os.File, error) { 31 | args := m.Called(path) 32 | return nil, args.Error(1) 33 | } 34 | 35 | func (m *MockedFilesystem) Stat(name string) (os.FileInfo, error) { 36 | args := m.Called(name) 37 | return nil, args.Error(1) 38 | } 39 | 40 | func TestInitializeConfig(t *testing.T) { 41 | fs := new(MockedFilesystem) 42 | home := "/mock/home" 43 | configPath := filepath.Join(home, common.WeaveConfigFile) 44 | 45 | // Resetting mocks for next test case 46 | fs.Mock = mock.Mock{} 47 | 48 | // Case 3: Successful configuration initialization 49 | fs.On("UserHomeDir").Return(home, nil) 50 | fs.On("MkdirAll", filepath.Dir(configPath), os.ModePerm).Return(nil) 51 | fs.On("Stat", configPath).Return(nil, errors.New("file does not exist")) 52 | fs.On("Create", configPath).Return(nil, nil) 53 | 54 | err := InitializeConfig() 55 | assert.NoError(t, err, "should initialize config without error") 56 | } 57 | -------------------------------------------------------------------------------- /config/toml.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/initia-labs/weave/common" 10 | ) 11 | 12 | // UpdateTomlValue updates a TOML file based on the provided key and value. 13 | // The key can be a field in a section (e.g., "api.enable") or a top-level field (e.g., "minimum-gas-prices"). 14 | func UpdateTomlValue(filePath, key, value string) error { 15 | value = common.CleanString(value) 16 | // Open the TOML file for reading 17 | file, err := os.Open(filePath) 18 | if err != nil { 19 | return fmt.Errorf("error opening file: %w", err) 20 | } 21 | defer file.Close() 22 | 23 | // Determine if the key has a section (e.g., "api.enable") or is a top-level field (e.g., "minimum-gas-prices") 24 | var section, field string 25 | parts := strings.SplitN(key, ".", 2) 26 | if len(parts) == 2 { 27 | section = parts[0] // e.g., "api" 28 | field = parts[1] // e.g., "enable" 29 | } else { 30 | field = key // e.g., "minimum-gas-prices" 31 | } 32 | 33 | // Slice to store updated file lines 34 | var updatedLines []string 35 | var currentSection string 36 | inTargetSection := false 37 | 38 | // Read the file line by line 39 | scanner := bufio.NewScanner(file) 40 | for scanner.Scan() { 41 | line := scanner.Text() 42 | trimmedLine := strings.TrimSpace(line) 43 | 44 | // Check if the line is a section header (e.g., [api]) 45 | if isSectionHeader(trimmedLine) { 46 | currentSection = getSectionName(trimmedLine) 47 | inTargetSection = currentSection == section 48 | } 49 | 50 | // Modify the field if it's in the correct section or at the top-level 51 | if shouldModifyField(inTargetSection, currentSection, field, trimmedLine) { 52 | line = fmt.Sprintf(`%s = "%s"`, field, value) 53 | } 54 | 55 | // Add the line to the updated content 56 | updatedLines = append(updatedLines, line) 57 | } 58 | 59 | // Check for any scanner errors 60 | if err := scanner.Err(); err != nil { 61 | return fmt.Errorf("error reading file: %w", err) 62 | } 63 | 64 | // Write the modified lines back to the file 65 | err = os.WriteFile(filePath, []byte(strings.Join(updatedLines, "\n")), 0644) 66 | if err != nil { 67 | return fmt.Errorf("error writing to file: %w", err) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // isSectionHeader checks if a line is a section header (e.g., [api]). 74 | func isSectionHeader(line string) bool { 75 | return strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") 76 | } 77 | 78 | // getSectionName extracts the section name from a section header (e.g., [api] -> api). 79 | func getSectionName(header string) string { 80 | return strings.Trim(header, "[]") 81 | } 82 | 83 | // shouldModifyField checks if the current line should be modified by splitting and trimming the key and value. 84 | func shouldModifyField(inTargetSection bool, currentSection, field, line string) bool { 85 | trimmedLine := strings.TrimSpace(line) 86 | 87 | // Check if the line contains the '=' delimiter 88 | if !strings.Contains(trimmedLine, "=") { 89 | // No '=' found, so we don't need to modify this line 90 | return false 91 | } 92 | 93 | // Split the line by '=' into a key and value pair 94 | parts := strings.SplitN(trimmedLine, "=", 2) 95 | key := strings.TrimSpace(parts[0]) 96 | 97 | // Check if the key matches the target field 98 | if key != field { 99 | return false 100 | } 101 | 102 | // If we are at the top-level or in the target section, return true 103 | if currentSection == "" || inTargetSection { 104 | return true 105 | } 106 | 107 | return false 108 | } 109 | -------------------------------------------------------------------------------- /context/base_model.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "runtime/debug" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | 11 | "github.com/initia-labs/weave/analytics" 12 | "github.com/initia-labs/weave/styles" 13 | ) 14 | 15 | // BaseModelInterface is an interface for base models 16 | type BaseModelInterface interface { 17 | tea.Model 18 | SetContext(ctx context.Context) 19 | GetContext() context.Context 20 | CanGoPreviousPage() bool 21 | WrapView(content string) string 22 | HandlePanic(err error) tea.Cmd 23 | } 24 | 25 | // BaseModel provides common functionality for all context-aware models 26 | type BaseModel struct { 27 | Ctx context.Context 28 | CannotBack bool 29 | PanicText string 30 | } 31 | 32 | // SetContext set the context from BaseModel 33 | func (b *BaseModel) SetContext(ctx context.Context) { 34 | b.Ctx = ctx 35 | } 36 | 37 | // GetContext retrieves the context from BaseModel 38 | func (b *BaseModel) GetContext() context.Context { 39 | return b.Ctx 40 | } 41 | 42 | func (b *BaseModel) CanGoPreviousPage() bool { 43 | return !b.CannotBack 44 | } 45 | 46 | func (b *BaseModel) WrapView(content string) string { 47 | windowWidth := GetWindowWidth(b.Ctx) 48 | if b.PanicText != "" { 49 | return styles.WordwrapString(fmt.Sprintf("%s\n\n%s", content, b.PanicText), windowWidth) 50 | } 51 | return styles.WordwrapString(content, windowWidth) 52 | } 53 | 54 | // extractModelNameFromStackTrace parses the stack trace and tries to extract the model name 55 | func extractModelNameFromStackTrace(stackTrace string) string { 56 | re := regexp.MustCompile(`\(\*(\w+)\)\.(Update)`) 57 | 58 | matches := re.FindStringSubmatch(stackTrace) 59 | if len(matches) > 0 { 60 | return matches[1] 61 | } 62 | 63 | return "UnknownModel" 64 | } 65 | 66 | func (b *BaseModel) HandlePanic(err error) tea.Cmd { 67 | stackTrace := string(debug.Stack()) 68 | events := analytics.NewEmptyEvent(). 69 | Add(analytics.ErrorEventKey, fmt.Sprint(err)). 70 | Add(analytics.ModelNameKey, extractModelNameFromStackTrace(stackTrace)) 71 | analytics.TrackEvent(analytics.Panicked, events) 72 | 73 | b.PanicText = fmt.Sprintf("Caught panic:\n\n%v\n\n%s", err, stackTrace) 74 | return tea.Quit 75 | } 76 | 77 | func HandleCommonCommands[S CloneableState[S]](model BaseModelInterface, msg tea.Msg) (tea.Model, tea.Cmd, bool) { 78 | model = AdjustWindowSize(model, msg) 79 | 80 | ctx := model.GetContext() 81 | if newCtx, handled := ToggleTooltip(ctx, msg); handled { 82 | model.SetContext(newCtx) 83 | return model, nil, true 84 | } 85 | 86 | if model.CanGoPreviousPage() { 87 | if newCtx, returnedModel, cmd, handled := Undo[S](ctx, msg); handled { 88 | if baseModel, ok := returnedModel.(BaseModelInterface); ok { 89 | SetTooltip(newCtx, GetTooltip(ctx)) // Preserve tooltip state 90 | baseModel.SetContext(newCtx) 91 | return baseModel, cmd, true 92 | } 93 | } 94 | } 95 | 96 | return nil, nil, false 97 | } 98 | -------------------------------------------------------------------------------- /context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | ) 8 | 9 | // Key is a custom type to prevent key collisions in the context 10 | type Key string 11 | 12 | const ( 13 | PageKey Key = "currentPage" 14 | StateKey Key = "currentState" 15 | PageStackKey Key = "pageStack" 16 | TooltipToggleKey Key = "tooltipToggle" 17 | WindowWidth Key = "windowWidth" 18 | 19 | InitiaHomeKey Key = "initiaHome" 20 | MinitiaHomeKey Key = "minitiaHome" 21 | OPInitHomeKey Key = "opInitHomeKey" 22 | ) 23 | 24 | var ( 25 | ExistingContextKey = []Key{ 26 | PageKey, 27 | StateKey, 28 | PageStackKey, 29 | TooltipToggleKey, 30 | InitiaHomeKey, 31 | MinitiaHomeKey, 32 | OPInitHomeKey, 33 | WindowWidth, 34 | } 35 | ) 36 | 37 | // NewAppContext initializes a new context with a generic state. 38 | func NewAppContext[S CloneableState[S]](initialState S) context.Context { 39 | ctx := context.Background() 40 | 41 | // Set initial context values 42 | ctx = context.WithValue(ctx, StateKey, initialState) 43 | ctx = context.WithValue(ctx, PageStackKey, []PageStatePair[S]{}) // Initialize with an empty slice 44 | ctx = context.WithValue(ctx, TooltipToggleKey, false) // Default to hiding more information 45 | ctx = context.WithValue(ctx, WindowWidth, 0) 46 | 47 | ctx = context.WithValue(ctx, InitiaHomeKey, "") 48 | ctx = context.WithValue(ctx, MinitiaHomeKey, "") 49 | ctx = context.WithValue(ctx, OPInitHomeKey, "") 50 | 51 | return ctx 52 | } 53 | 54 | // CloneContext creates a shallow copy of the existing context while preserving all keys and values 55 | func CloneContext(ctx context.Context) context.Context { 56 | // Create a base context 57 | clonedCtx := context.Background() 58 | 59 | for _, key := range ExistingContextKey { 60 | if value := ctx.Value(key); value != nil { 61 | clonedCtx = context.WithValue(clonedCtx, key, value) 62 | } 63 | } 64 | 65 | return clonedCtx 66 | } 67 | 68 | // SetCurrentModel updates the current model in the context 69 | func SetCurrentModel(ctx context.Context, currentModel tea.Model) context.Context { 70 | return context.WithValue(ctx, PageKey, currentModel) 71 | } 72 | 73 | // GetCurrentModel retrieves the current model from the context 74 | func GetCurrentModel(ctx context.Context) tea.Model { 75 | if model, ok := ctx.Value(PageKey).(tea.Model); ok { 76 | return model 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /context/layout.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | tea "github.com/charmbracelet/bubbletea" 6 | ) 7 | 8 | const ( 9 | DefaultPadding int = 2 10 | ) 11 | 12 | func AdjustWindowSize(model BaseModelInterface, msg tea.Msg) BaseModelInterface { 13 | if windowMsg, ok := msg.(tea.WindowSizeMsg); ok { 14 | ctx := model.GetContext() 15 | ctx = SetWindowWidth(ctx, windowMsg.Width-DefaultPadding) 16 | model.SetContext(ctx) 17 | return model 18 | } 19 | return model 20 | } 21 | 22 | func SetWindowWidth(ctx context.Context, windowWidth int) context.Context { 23 | return context.WithValue(ctx, WindowWidth, windowWidth) 24 | } 25 | 26 | func GetWindowWidth(ctx context.Context) int { 27 | if value, ok := ctx.Value(WindowWidth).(int); ok { 28 | return value 29 | } 30 | panic("context does not have a WindowWidth value") 31 | } 32 | -------------------------------------------------------------------------------- /context/path.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/initia-labs/weave/common" 9 | ) 10 | 11 | func SetInitiaHome(ctx context.Context, initiaHome string) context.Context { 12 | return context.WithValue(ctx, InitiaHomeKey, initiaHome) 13 | } 14 | 15 | func GetInitiaHome(ctx context.Context) (string, error) { 16 | if value, ok := ctx.Value(InitiaHomeKey).(string); ok { 17 | return value, nil 18 | } 19 | return "", fmt.Errorf("cannot cast the InitiaHomeKey value into type string") 20 | } 21 | 22 | func GetInitiaConfigDirectory(ctx context.Context) (string, error) { 23 | initiaHome, err := GetInitiaHome(ctx) 24 | if err != nil { 25 | return "", err 26 | } 27 | return filepath.Join(initiaHome, common.InitiaConfigDirectory), nil 28 | } 29 | 30 | func GetInitiaDataDirectory(ctx context.Context) (string, error) { 31 | initiaHome, err := GetInitiaHome(ctx) 32 | if err != nil { 33 | return "", err 34 | } 35 | return filepath.Join(initiaHome, common.InitiaDataDirectory), nil 36 | } 37 | 38 | func SetMinitiaHome(ctx context.Context, minitiaHome string) context.Context { 39 | return context.WithValue(ctx, MinitiaHomeKey, minitiaHome) 40 | } 41 | 42 | func GetMinitiaHome(ctx context.Context) (string, error) { 43 | if value, ok := ctx.Value(MinitiaHomeKey).(string); ok { 44 | return value, nil 45 | } 46 | return "", fmt.Errorf("cannot cast the MinitiaHomeKey value into type string") 47 | } 48 | 49 | func GetMinitiaArtifactsConfigJson(ctx context.Context) (string, error) { 50 | minitiaHome, err := GetMinitiaHome(ctx) 51 | if err != nil { 52 | return "", err 53 | } 54 | return filepath.Join(minitiaHome, common.MinitiaArtifactsConfigJson), nil 55 | } 56 | 57 | func GetMinitiaArtifactsJson(ctx context.Context) (string, error) { 58 | minitiaHome, err := GetMinitiaHome(ctx) 59 | if err != nil { 60 | return "", err 61 | } 62 | return filepath.Join(minitiaHome, common.MinitiaArtifactsJson), nil 63 | } 64 | 65 | func SetOPInitHome(ctx context.Context, opInitHome string) context.Context { 66 | return context.WithValue(ctx, OPInitHomeKey, opInitHome) 67 | } 68 | 69 | func GetOPInitHome(ctx context.Context) (string, error) { 70 | if value, ok := ctx.Value(OPInitHomeKey).(string); ok { 71 | return value, nil 72 | } 73 | return "", fmt.Errorf("cannot cast the OPInitHomeKey value into type string") 74 | } 75 | 76 | func GetOPInitKeyFileJson(ctx context.Context) (string, error) { 77 | opInitHome, err := GetOPInitHome(ctx) 78 | if err != nil { 79 | return "", err 80 | } 81 | return filepath.Join(opInitHome, common.OPinitKeyFileJson), nil 82 | } 83 | -------------------------------------------------------------------------------- /context/state.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | ) 8 | 9 | // CloneableState is an interface that requires the Clone method 10 | type CloneableState[S any] interface { 11 | Clone() S 12 | } 13 | 14 | // PageStatePair is a generic struct that holds a pair of page (model) and its associated state. 15 | type PageStatePair[S CloneableState[S]] struct { 16 | Page tea.Model 17 | State S 18 | } 19 | 20 | // CloneStateAndPushPage clones the current state and pushes the current page-state pair onto the context (non-pointer version) 21 | func CloneStateAndPushPage[S CloneableState[S]](ctx context.Context, page tea.Model) context.Context { 22 | // Retrieve the current state and the cleanup function 23 | currentState := GetCurrentState[S](ctx) 24 | 25 | // Clone the state 26 | clonedState := currentState.Clone() 27 | 28 | // Clone the context by updating only the necessary keys without losing existing values 29 | updatedCtx := CloneContext(ctx) 30 | 31 | // Set the current page in the cloned context 32 | updatedCtx = SetCurrentModel(updatedCtx, page) 33 | 34 | // Push the cloned state and the current page onto the cloned context 35 | newCtx := PushPageState(updatedCtx, page, clonedState) 36 | 37 | return newCtx 38 | } 39 | 40 | func PushPageAndGetState[S CloneableState[S]](baseModel BaseModelInterface) S { 41 | ctx := CloneStateAndPushPage[S](baseModel.GetContext(), baseModel) 42 | baseModel.SetContext(ctx) 43 | return GetCurrentState[S](ctx) 44 | } 45 | 46 | // SetCurrentState stores the current state in the context using StateKey (non-pointer version) 47 | func SetCurrentState[S any](ctx context.Context, state S) context.Context { 48 | // Store the state in the context without using a pointer 49 | return context.WithValue(ctx, StateKey, state) 50 | } 51 | 52 | // GetCurrentState retrieves the current state from the context and panics if not found (a non-pointer version) 53 | func GetCurrentState[S any](ctx context.Context) S { 54 | // Retrieve the state from the context using StateKey 55 | if state, ok := ctx.Value(StateKey).(S); ok { 56 | // Return the retrieved state and a function to update it back into the context 57 | return state 58 | } 59 | panic("GetCurrentState: state not found in the context") 60 | } 61 | 62 | // PushPageState pushes the current page and state onto the stack in the context 63 | func PushPageState[S CloneableState[S]](ctx context.Context, page tea.Model, state S) context.Context { 64 | pageStack := GetPageStack[S](ctx) 65 | clonedState := state.Clone() // Clone the state before pushing it 66 | pageStack = append(pageStack, PageStatePair[S]{Page: page, State: clonedState}) 67 | return context.WithValue(ctx, PageStackKey, pageStack) 68 | } 69 | 70 | // PopPageState pops the last page-state pair from the stack 71 | func PopPageState[S CloneableState[S]](ctx context.Context) (context.Context, *PageStatePair[S]) { 72 | pageStack := GetPageStack[S](ctx) 73 | if len(pageStack) == 0 { 74 | return ctx, nil 75 | } 76 | lastPair := pageStack[len(pageStack)-1] 77 | pageStack = pageStack[:len(pageStack)-1] 78 | ctx = context.WithValue(ctx, PageStackKey, pageStack) 79 | return ctx, &lastPair 80 | } 81 | 82 | // GetPageStack retrieves the page-state stack from the context 83 | func GetPageStack[S CloneableState[S]](ctx context.Context) []PageStatePair[S] { 84 | if ctx == nil { 85 | return nil 86 | } 87 | if stack, ok := ctx.Value(PageStackKey).([]PageStatePair[S]); ok { 88 | return stack 89 | } 90 | return []PageStatePair[S]{} // Return an empty slice if not found 91 | } 92 | 93 | // Undo handles the undo functionality 94 | func Undo[S CloneableState[S]](ctx context.Context, msg tea.Msg) (context.Context, tea.Model, tea.Cmd, bool) { 95 | if keyMsg, ok := msg.(tea.KeyMsg); ok { 96 | 97 | if keyMsg.String() == "ctrl+z" { 98 | // Attempt to undo: Go back to the previous page and state 99 | newCtx, prevPair := PopPageState[S](ctx) 100 | if prevPair != nil { 101 | newCtx = SetCurrentModel(newCtx, prevPair.Page) 102 | newCtx = SetCurrentState(newCtx, prevPair.State) 103 | return newCtx, prevPair.Page, nil, true 104 | } 105 | } 106 | } 107 | return ctx, nil, nil, false 108 | } 109 | -------------------------------------------------------------------------------- /context/tooltip.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "context" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | ) 8 | 9 | // ToggleTooltip toggles the "tooltip" flag in the context for showing tooltips. 10 | func ToggleTooltip(ctx context.Context, msg tea.Msg) (context.Context, bool) { 11 | if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == "ctrl+t" { 12 | ctx = ToggleTooltipInContext(ctx) 13 | return ctx, true 14 | } 15 | return ctx, false 16 | } 17 | 18 | func ToggleTooltipInContext(ctx context.Context) context.Context { 19 | currentValue := GetTooltip(ctx) 20 | return SetTooltip(ctx, !currentValue) 21 | } 22 | 23 | // SetTooltip sets the boolean value for showing or hiding tooltip information in the context 24 | func SetTooltip(ctx context.Context, showTooltip bool) context.Context { 25 | return context.WithValue(ctx, TooltipToggleKey, showTooltip) 26 | } 27 | 28 | // GetTooltip retrieves the boolean value for showing or hiding tooltip information from the context 29 | func GetTooltip(ctx context.Context) bool { 30 | if value, ok := ctx.Value(TooltipToggleKey).(bool); ok { 31 | return value 32 | } 33 | return false // Default to hidden if not set 34 | } 35 | -------------------------------------------------------------------------------- /cosmosutils/cosmos_query.go: -------------------------------------------------------------------------------- 1 | package cosmosutils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/initia-labs/weave/client" 8 | ) 9 | 10 | const ( 11 | NoBalancesText string = "No Balances" 12 | ) 13 | 14 | func QueryBankBalances(rest, address string) (*Coins, error) { 15 | httpClient := client.NewHTTPClient() 16 | var result map[string]interface{} 17 | _, err := httpClient.Get( 18 | rest, 19 | fmt.Sprintf("/cosmos/bank/v1beta1/balances/%s", address), 20 | map[string]string{"pagination.limit": "100"}, 21 | &result, 22 | ) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | rawBalances, ok := result["balances"].([]interface{}) 28 | if !ok { 29 | return nil, fmt.Errorf("failed to parse balances field") 30 | } 31 | 32 | balancesJSON, err := json.Marshal(rawBalances) 33 | if err != nil { 34 | return nil, fmt.Errorf("failed to marshal balances: %w", err) 35 | } 36 | 37 | var balances Coins 38 | err = json.Unmarshal(balancesJSON, &balances) 39 | if err != nil { 40 | return nil, fmt.Errorf("failed to unmarshal balances into Coins: %w", err) 41 | } 42 | 43 | return &balances, nil 44 | } 45 | 46 | func QueryOPChildParams(address string) (params OPChildParams, err error) { 47 | httpClient := client.NewHTTPClient() 48 | 49 | var res OPChildParamsResponse 50 | if _, err := httpClient.Get(address, "/opinit/opchild/v1/params", nil, &res); err != nil { 51 | return params, err 52 | } 53 | 54 | return res.Params, nil 55 | } 56 | -------------------------------------------------------------------------------- /cosmosutils/graphql_query.go: -------------------------------------------------------------------------------- 1 | package cosmosutils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/initia-labs/weave/client" 7 | ) 8 | 9 | type TransactionEventsResponse struct { 10 | Data struct { 11 | TransactionEvents []struct { 12 | BlockHeight int `json:"block_height,omitempty"` 13 | TransactionHash string `json:"transaction_hash,omitempty"` 14 | } `json:"transaction_events"` 15 | } `json:"data"` 16 | } 17 | 18 | func QueryCreateBridgeHeight(client *client.GraphQLClient, bridgeId string) (int, error) { 19 | query := ` 20 | query CreateBridgeHeight($bridgeId: String) { 21 | transaction_events( 22 | where: {event_key: {_eq: "create_bridge.bridge_id"}, event_value: {_eq: $bridgeId}} 23 | limit: 1 24 | order_by: {block_height: desc} 25 | ) { 26 | block_height 27 | } 28 | } 29 | ` 30 | var response TransactionEventsResponse 31 | err := client.Query( 32 | query, 33 | map[string]interface{}{ 34 | "bridgeId": bridgeId, 35 | }, 36 | &response, 37 | ) 38 | if err != nil { 39 | return 0, fmt.Errorf("failed to query CreateBridgeHeight: %w", err) 40 | } 41 | if len(response.Data.TransactionEvents) != 1 { 42 | return 0, fmt.Errorf("expected 1 CreateBridgeHeight event, got %d", len(response.Data.TransactionEvents)) 43 | } 44 | 45 | return response.Data.TransactionEvents[0].BlockHeight, err 46 | } 47 | 48 | func QueryLatestDepositHeight(client *client.GraphQLClient, bridgeId, sequence string) (int, error) { 49 | txSequenceQuery := ` 50 | query TransactionWithL1Sequence($sequence: String, $offset: Int) { 51 | transaction_events( 52 | where: {event_key: {_eq: "initiate_token_deposit.l1_sequence"}, event_value: {_eq: $sequence}} 53 | limit: 100 54 | offset: $offset 55 | order_by: {block_height: desc} 56 | ) { 57 | transaction_hash 58 | block_height 59 | } 60 | } 61 | ` 62 | txBridgeQuery := ` 63 | query HeightWithBridgeTransaction($bridgeId: String, $txHash: String, $height: bigint) { 64 | transaction_events( 65 | where: {transaction_hash: {_eq: $txHash}, block_height: {_eq: $height}, event_key: {_eq: "initiate_token_deposit.bridge_id"}, event_value: {_eq: $bridgeId}} 66 | limit: 1 67 | order_by: {block_height: desc} 68 | ) { 69 | block_height 70 | } 71 | } 72 | ` 73 | 74 | pageSize := 100 75 | for offset := 0; ; offset += pageSize { 76 | var seqResponse TransactionEventsResponse 77 | err := client.Query( 78 | txSequenceQuery, 79 | map[string]interface{}{ 80 | "sequence": sequence, 81 | "offset": offset, 82 | }, 83 | &seqResponse, 84 | ) 85 | if err != nil { 86 | return 0, fmt.Errorf("failed to query LatestDepositHeight: %w", err) 87 | } 88 | if len(seqResponse.Data.TransactionEvents) == 0 { 89 | break 90 | } 91 | 92 | for _, transactionEvent := range seqResponse.Data.TransactionEvents { 93 | var bridgeResponse TransactionEventsResponse 94 | err = client.Query( 95 | txBridgeQuery, 96 | map[string]interface{}{ 97 | "bridgeId": bridgeId, 98 | "txHash": transactionEvent.TransactionHash, 99 | "height": transactionEvent.BlockHeight, 100 | }, 101 | &bridgeResponse, 102 | ) 103 | if err != nil { 104 | return 0, fmt.Errorf("failed to query HeightWithBridgeTransaction: %w", err) 105 | } 106 | if len(bridgeResponse.Data.TransactionEvents) == 1 { 107 | return bridgeResponse.Data.TransactionEvents[0].BlockHeight, nil 108 | } 109 | } 110 | } 111 | return 0, fmt.Errorf("cannot find deposit height according to the given bridge and sequence") 112 | } 113 | -------------------------------------------------------------------------------- /cosmosutils/statesync.go: -------------------------------------------------------------------------------- 1 | package cosmosutils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/initia-labs/weave/client" 8 | ) 9 | 10 | type BlockResponse struct { 11 | Result struct { 12 | Block struct { 13 | Header struct { 14 | Height string `json:"height"` 15 | } `json:"header"` 16 | } `json:"block"` 17 | } `json:"result"` 18 | } 19 | 20 | type HashResponse struct { 21 | Result struct { 22 | BlockID struct { 23 | Hash string `json:"hash"` 24 | } `json:"block_id"` 25 | } `json:"result"` 26 | } 27 | 28 | type StateSyncInfo struct { 29 | TrustHeight int 30 | TrustHash string 31 | } 32 | 33 | func GetStateSyncInfo(url string) (*StateSyncInfo, error) { 34 | httpClient := client.NewHTTPClient() 35 | var latestBlock BlockResponse 36 | _, err := httpClient.Get(url, "/block", nil, &latestBlock) 37 | if err != nil { 38 | return nil, fmt.Errorf("Error fetching latest block height: %v\n", err) 39 | } 40 | 41 | latestHeight, err := strconv.Atoi(latestBlock.Result.Block.Header.Height) 42 | if err != nil { 43 | return nil, fmt.Errorf("Error converting block height to integer: %v\n", err) 44 | } 45 | blockHeight := latestHeight - 2000 46 | 47 | var trustHashResp HashResponse 48 | _, err = httpClient.Get(url, "/block", map[string]string{"height": strconv.Itoa(blockHeight)}, &trustHashResp) 49 | if err != nil { 50 | return nil, fmt.Errorf("Error fetching trust hash: %v\n", err) 51 | } 52 | 53 | return &StateSyncInfo{ 54 | TrustHeight: blockHeight, 55 | TrustHash: trustHashResp.Result.BlockID.Hash, 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /cosmosutils/types.go: -------------------------------------------------------------------------------- 1 | package cosmosutils 2 | 3 | import ( 4 | "fmt" 5 | "math/big" 6 | "strings" 7 | 8 | "github.com/initia-labs/weave/styles" 9 | ) 10 | 11 | type Coin struct { 12 | Denom string `json:"denom"` 13 | Amount string `json:"amount"` 14 | } 15 | 16 | func (coin Coin) IsZero() bool { 17 | normalizedAmount := strings.TrimSpace(coin.Amount) 18 | if normalizedAmount == "" || normalizedAmount == "0" { 19 | return true 20 | } 21 | 22 | amountBigInt := new(big.Int) 23 | amountBigInt, success := amountBigInt.SetString(normalizedAmount, 10) 24 | if !success { 25 | return false 26 | } 27 | 28 | return amountBigInt.Cmp(big.NewInt(0)) == 0 29 | } 30 | 31 | type Coins []Coin 32 | 33 | func (cs *Coins) Render(maxWidth int) string { 34 | if len(*cs) == 0 { 35 | return styles.CreateFrame(NoBalancesText, maxWidth) 36 | } 37 | 38 | maxAmountLen := cs.getMaxAmountLength() 39 | 40 | var content strings.Builder 41 | for _, coin := range *cs { 42 | line := fmt.Sprintf("%-*s %s", maxAmountLen, coin.Amount, coin.Denom) 43 | content.WriteString(line + "\n") 44 | } 45 | 46 | contentStr := strings.TrimSuffix(content.String(), "\n") 47 | return styles.CreateFrame(contentStr, maxWidth) 48 | } 49 | 50 | func (cs *Coins) getMaxAmountLength() int { 51 | maxLen := 0 52 | for _, coin := range *cs { 53 | if len(coin.Amount) > maxLen { 54 | maxLen = len(coin.Amount) 55 | } 56 | } 57 | return maxLen 58 | } 59 | 60 | func (cs *Coins) IsZero() bool { 61 | for _, coin := range *cs { 62 | if !coin.IsZero() { 63 | return false 64 | } 65 | } 66 | return true 67 | } 68 | 69 | type NodeInfoResponse struct { 70 | ApplicationVersion struct { 71 | Version string `json:"version"` 72 | } `json:"application_version"` 73 | } 74 | 75 | type DecCoin struct { 76 | Denom string `protobuf:"bytes,1,opt,name=denom,proto3" json:"denom,omitempty"` 77 | Amount string `protobuf:"bytes,2,opt,name=amount,proto3,customtype=cosmossdk.io/math.LegacyDec" json:"amount"` 78 | } 79 | 80 | // DecCoins defines a slice of coins with decimal values 81 | type DecCoins []DecCoin 82 | 83 | // Params defines the set of opchild parameters. 84 | type OPChildParams struct { 85 | // max_validators is the maximum number of validators. 86 | MaxValidators uint32 `protobuf:"varint,1,opt,name=max_validators,json=maxValidators,proto3" json:"max_validators,omitempty" yaml:"max_validators"` 87 | // historical_entries is the number of historical entries to persist. 88 | HistoricalEntries uint32 `protobuf:"varint,2,opt,name=historical_entries,json=historicalEntries,proto3" json:"historical_entries,omitempty" yaml:"historical_entries"` 89 | MinGasPrices DecCoins `protobuf:"bytes,3,rep,name=min_gas_prices,json=minGasPrices,proto3,castrepeated=github.com/cosmos/cosmos-sdk/types.DecCoins" json:"min_gas_prices" yaml:"min_gas_price"` 90 | // the account address of bridge executor who can execute permissioned bridge 91 | // messages. 92 | BridgeExecutors []string `protobuf:"bytes,4,rep,name=bridge_executors,json=bridgeExecutors,proto3" json:"bridge_executors,omitempty" yaml:"bridge_executors"` 93 | // the account address of admin who can execute permissioned cosmos messages. 94 | Admin string `protobuf:"bytes,5,opt,name=admin,proto3" json:"admin,omitempty" yaml:"admin"` 95 | // the list of addresses that are allowed to pay zero fee. 96 | FeeWhitelist []string `protobuf:"bytes,6,rep,name=fee_whitelist,json=feeWhitelist,proto3" json:"fee_whitelist,omitempty" yaml:"fee_whitelist"` 97 | // Max gas for hook execution of `MsgFinalizeTokenDeposit` 98 | HookMaxGas string `protobuf:"varint,7,opt,name=hook_max_gas,json=hookMaxGas,proto3" json:"hook_max_gas,omitempty" yaml:"hook_max_gas"` 99 | } 100 | 101 | type OPChildParamsResponse struct { 102 | Params OPChildParams `protobuf:"bytes,1,opt,name=params,proto3" json:"params,omitempty"` 103 | } 104 | -------------------------------------------------------------------------------- /crypto/bech32.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/btcsuite/btcd/btcec/v2" 10 | "github.com/btcsuite/btcutil/bech32" 11 | "github.com/tyler-smith/go-bip32" 12 | "github.com/tyler-smith/go-bip39" 13 | "golang.org/x/crypto/ripemd160" 14 | ) 15 | 16 | const ( 17 | CosmosHDPath string = "m/44'/118'/0'/0/0" 18 | HardenedOffset int = 0x80000000 19 | ) 20 | 21 | // MnemonicToBech32Address converts a mnemonic to a Cosmos SDK Bech32 address. 22 | func MnemonicToBech32Address(hrp, mnemonic string) (string, error) { 23 | seed, err := bip39.NewSeedWithErrorChecking(mnemonic, "") 24 | if err != nil { 25 | return "", fmt.Errorf("failed to generate seed: %w", err) 26 | } 27 | 28 | masterKey, err := bip32.NewMasterKey(seed) 29 | if err != nil { 30 | return "", fmt.Errorf("failed to derive master key: %w", err) 31 | } 32 | 33 | derivedKey, err := deriveKey(masterKey, CosmosHDPath) 34 | if err != nil { 35 | return "", fmt.Errorf("failed to derive child key: %w", err) 36 | } 37 | 38 | _, pubKey := btcec.PrivKeyFromBytes(derivedKey.Key) 39 | pubKeyBytes := pubKey.SerializeCompressed() 40 | 41 | shaHash := sha256.Sum256(pubKeyBytes) 42 | ripemd := ripemd160.New() 43 | ripemd.Write(shaHash[:]) 44 | addressHash := ripemd.Sum(nil) 45 | 46 | converted, err := bech32.ConvertBits(addressHash, 8, 5, true) 47 | if err != nil { 48 | return "", fmt.Errorf("failed to convert to Bech32: %w", err) 49 | } 50 | 51 | bech32Addr, err := bech32.Encode(hrp, converted) 52 | if err != nil { 53 | return "", fmt.Errorf("failed to encode to Bech32: %w", err) 54 | } 55 | 56 | return bech32Addr, nil 57 | } 58 | 59 | // deriveKey derives the private key along the given HD path. 60 | func deriveKey(masterKey *bip32.Key, path string) (*bip32.Key, error) { 61 | key := masterKey 62 | var err error 63 | 64 | for _, n := range parseHDPath(path) { 65 | key, err = key.NewChildKey(n) 66 | if err != nil { 67 | return nil, err 68 | } 69 | } 70 | return key, nil 71 | } 72 | 73 | // parseHDPath parses the hd path string 74 | func parseHDPath(path string) []uint32 { 75 | parts := strings.Split(path, "/")[1:] 76 | keys := make([]uint32, len(parts)) 77 | 78 | for i, part := range parts { 79 | hardened := strings.HasSuffix(part, "'") 80 | if hardened { 81 | part = strings.TrimSuffix(part, "'") 82 | } 83 | 84 | n, _ := strconv.Atoi(part) 85 | if hardened { 86 | n = n + HardenedOffset 87 | } 88 | keys[i] = uint32(n) 89 | } 90 | return keys 91 | } 92 | 93 | // GenerateMnemonic generates new fresh mnemonic 94 | func GenerateMnemonic() (string, error) { 95 | entropy, err := bip39.NewEntropy(256) 96 | if err != nil { 97 | return "", fmt.Errorf("failed to generate entropy: %w", err) 98 | } 99 | 100 | mnemonic, err := bip39.NewMnemonic(entropy) 101 | if err != nil { 102 | return "", fmt.Errorf("failed to generate mnemonic: %w", err) 103 | } 104 | 105 | return mnemonic, nil 106 | } 107 | -------------------------------------------------------------------------------- /crypto/bip39.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // IsMnemonicValid attempts to verify that the provided mnemonic is valid. 8 | // Validity is determined by both the number of words being appropriate, 9 | // and that all the words in the mnemonic are present in the word list. 10 | func IsMnemonicValid(mnemonic string) bool { 11 | // Create a list of all the words in the mnemonic sentence 12 | words := strings.Fields(mnemonic) 13 | 14 | //Get num of words 15 | numOfWords := len(words) 16 | 17 | // The number of words should be 12, 15, 18, 21 or 24 18 | if numOfWords < 12 || numOfWords > 24 || numOfWords%3 != 0 { 19 | return false 20 | } 21 | 22 | // Check if all words belong in the wordlist 23 | for i := 0; i < numOfWords; i++ { 24 | if _, ok := ReverseWordMap[words[i]]; !ok { 25 | return false 26 | } 27 | } 28 | 29 | return true 30 | } 31 | -------------------------------------------------------------------------------- /docs/gas_station.md: -------------------------------------------------------------------------------- 1 | # Gas Station 2 | 3 | The Gas Station is a dedicated account used by Weave to fund critical infrastructure components of the Interwoven stack. It distributes funds to essential services like [OPinit Bots](https://github.com/initia-labs/opinit-bots) (including Bridge Executor, Output Submitter, Batch Submitter, and Challenger) and the [IBC relayer](https://tutorials.cosmos.network/academy/2-cosmos-concepts/13-relayer-intro.html) to ensure smooth operation of the network. 4 | 5 | This is essential for seamless operation with Weave as it eliminates the need for manual fund distribution. 6 | 7 | > While Weave requires your consent for all fund transfers, using a separate account prevents any potential misuse of an existing account. We strongly recommend creating a new dedicated account for Gas Station use rather than using an existing account 8 | 9 | ## Setting up the Gas Station 10 | 11 | ```bash 12 | weave gas-station setup 13 | ``` 14 | 15 | You can either import an existing mnemonic or have Weave generate a new one. 16 | Once setup is complete, you'll see two addresses in `init` and `celestia` format. 17 | 18 | > While the Gas Station addresses for Celestia and the Initia ecosystem will be different, both are derived from the same mnemonic that you entered. 19 | 20 | Then fund the account with at least 10 INIT tokens to support the necessary components. If you're planning to use Celestia as your Data Availability Layer, you'll also need to fund the account with `TIA` tokens. 21 | 22 | > For testnet operations: - Get testnet `INIT` tokens from the [Initia faucet](https://faucet.testnet.initia.xyz/) - Get testnet `TIA` tokens from the [Celestia faucet](https://docs.celestia.org/how-to-guides/mocha-testnet#mocha-testnet-faucet) 23 | 24 | ## Viewing Gas Station Information 25 | 26 | ```bash 27 | weave gas-station show 28 | ``` 29 | 30 | This command displays the addresses and current balances of the Gas Station account in both `init` and `celestia` bech32 formats. 31 | -------------------------------------------------------------------------------- /docs/initia_node.md: -------------------------------------------------------------------------------- 1 | # Bootstrapping Initia Node 2 | 3 | Setting up a node for a Cosmos SDK chain has traditionally been a complex process requiring multiple steps: 4 | - Locating the correct repository and version of the node binary compatible with your target network 5 | - Either cloning and building the source code or downloading a pre-built binary from the release page 6 | - Configuring the node with appropriate `config.toml` and `app.toml` files, which involves: 7 | - Setting correct values for `seeds`, `persistent_peers`, and `pruning` 8 | - Navigating through numerous other parameters that rarely need modification 9 | - Finding and implementing the correct genesis file to sync with the network 10 | - Setting up cosmovisor for automatic updates or manually maintaining the node binary 11 | 12 | Weave streamlines this entire process into a simple command. 13 | 14 | ## Initialize your node 15 | 16 | ```bash 17 | weave initia init 18 | ``` 19 | This command guides you through the node setup process, taking you from an empty directory to a fully synced node ready for operation. 20 | Once complete, you can run the node using `weave initia start`. 21 | 22 | ### Peer issues 23 | if you observed that your node cannot communicate and sync with peers, try adding Polkachu's [live peers](https://polkachu.com/testnets/initia/peers) and [address book](https://polkachu.com/testnets/initia/addrbooks). 24 | 25 | ## Running your node 26 | 27 | ### Start the node 28 | 29 | ```bash 30 | weave initia start 31 | ``` 32 | Specify `--detach` or `-d` to run in the background. 33 | 34 | ### Stop the node 35 | 36 | ```bash 37 | weave initia stop 38 | ``` 39 | 40 | ### Restart the node 41 | 42 | ```bash 43 | weave initia restart 44 | ``` 45 | 46 | ### See the logs 47 | 48 | ```bash 49 | weave initia log 50 | ``` 51 | 52 | ## Help 53 | 54 | To see all the available commands: 55 | ```bash 56 | weave initia --help 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/opinit_bots.md: -------------------------------------------------------------------------------- 1 | # Running OPinit Bots 2 | 3 | Weave provides a streamlined way to configure and run [OPinit Bots](https://github.com/initia-labs/opinit-bots) (executor and challenger) for your rollup. 4 | 5 | ## Setting up 6 | 7 | ```bash 8 | weave opinit init 9 | ``` 10 | 11 | This command will guide you through selecting the bot type (executor or challenger), configuring bot keys if needed, and setting up the bot's configuration. 12 | 13 | You can also specify the bot type directly: 14 | 15 | ```bash 16 | weave opinit init 17 | ``` 18 | 19 | ## Managing Keys 20 | 21 | To modify bot keys, use the following command to either generate new keys or restore existing ones: 22 | ```bash 23 | weave opinit setup-keys 24 | ``` 25 | 26 | ## Resetting OPinit Bots 27 | 28 | Reset a bot's database. This will clear all the data stored in the bot's database (the configuration files are not affected). 29 | 30 | ```bash 31 | weave opinit reset 32 | ``` 33 | 34 | ## Running OPinit Bots 35 | 36 | ### Start the bot 37 | 38 | ```bash 39 | weave opinit start 40 | ``` 41 | Specify `--detach` or `-d` to run in the background. 42 | 43 | ### Stop the bot 44 | 45 | ```bash 46 | weave opinit stop 47 | ``` 48 | 49 | ### Restart the bot 50 | 51 | ```bash 52 | weave opinit restart 53 | ``` 54 | 55 | ### See the logs 56 | 57 | ```bash 58 | weave opinit log 59 | ``` 60 | 61 | ## Help 62 | 63 | To see all the available commands: 64 | ```bash 65 | weave opinit --help 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/relayer.md: -------------------------------------------------------------------------------- 1 | # Running IBC Relayer 2 | 3 | An IBC relayer is a software component that facilitates communication between two distinct blockchain networks that support the Inter-Blockchain Communication (IBC) protocol. 4 | 5 | It is required for built-in oracle, Minitswap, and other cross-chain services to function with your rollup. 6 | 7 | While setting up a relayer is traditionally one of the most complex tasks in the Cosmos ecosystem, 8 | Weave simplifies this process significantly, reducing the typical 10+ step IBC Relayer setup to just a few simple steps. 9 | 10 | > Weave only supports IBC relayer setup between Initia L1 and Interwoven Rollups. Setting up relayers between other arbitrary networks is not supported. 11 | 12 | > Currently, Weave only supports the `Hermes` relayer. For detailed information about Hermes, please refer to the [Hermes documentation](https://github.com/informalsystems/hermes). 13 | 14 | ## Setting up 15 | 16 | ```bash 17 | weave relayer init 18 | ``` 19 | This command will guide you through 2 major parts of the relayer setup: 20 | - Setting up networks and channels to relay messages between 21 | - Setting up the account responsible for relaying messages 22 | 23 | 24 | For the former, Weave will present you with three options: 25 | 1. Configure channels between Initia L1 and a whitelisted Rollup (those available in [Initia Registry](https://github.com/initia-labs/initia-registry) 26 | 2. Configure using artifacts from `weave rollup launch` (recommended for users who have just launched their rollup) 27 | 3. Configure manually 28 | 29 | As for the latter, Weave will ask whether you want to use OPinit Challenger bot account for the relayer. This is recommended as it is exempted from gas fees on the rollup and able to stop other relayers from relaying when it detects a malicious message coming from it. 30 | 31 | > Relayer requires funds to relay messages between Initia L1 and your rollup (if it's not in the fee whitelist). If Weave detects that your account does not have enough funds, Weave will ask you to fund via Gas Station. 32 | 33 | > For advanced configuration options, you can refer to the [Hermes Configuration Guide](https://hermes.informal.systems/documentation/configuration/configure-hermes.html) and customize the relayer's configuration file located at `~/.hermes/config.toml`. 34 | 35 | ## Running Relayer 36 | 37 | ### Start the relayer 38 | 39 | ```bash 40 | weave relayer start 41 | ``` 42 | 43 | Specify `--detach` or `-d` to run in the background. 44 | Specify `--update-client false` to disable update IBC clients on relayer starts. Default to `true` 45 | 46 | ### Stop the relayer 47 | 48 | ```bash 49 | weave relayer stop 50 | ``` 51 | 52 | ### Restart the relayer 53 | 54 | ```bash 55 | weave relayer restart 56 | ``` 57 | 58 | ### See the logs 59 | 60 | ```bash 61 | weave relayer log 62 | ``` 63 | 64 | ## Help 65 | 66 | To see all the available commands: 67 | ```bash 68 | weave relayer --help 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/rollup_launch.md: -------------------------------------------------------------------------------- 1 | # Launching your Rollup 2 | 3 | Weave simplifies complicated rollup launch steps into a single command. 4 | 5 | > Weave will send some funds from Gas Station to the OPinit Bot accounts during this process. Please make sure that your Gas Station account has enough funds to cover the total amount of funds to be sent (this amount will be shown to you before sending the funds). 6 | > Haven't set up the Gas Station yet? Please [Check out this guide](/docs/gas_station.md) first. 7 | 8 | ```bash 9 | weave rollup launch 10 | ``` 11 | 12 | Once the process completes, your rollup node will be running and ready to process queries and transactions. 13 | The command also provides an [Initia Scan](https://scan.testnet.initia.xyz/) magic link that automatically adds your local rollup to the explorer, allowing you to instantly view your rollup's transactions and state. 14 | 15 | 16 | > This command only sets up the bot addresses but does not start the OPinit Bots (executor and challenger). To complete the setup, proceed to the [OPinit Bots setup](/docs/opinit_bots.md) section to configure and run the OPinit Bots. 17 | 18 | ## Running your Rollup node 19 | 20 | ### Start the node 21 | 22 | ```bash 23 | weave rollup start 24 | ``` 25 | Specify `--detach` or `-d` to run in the background. 26 | 27 | ### Stop the node 28 | 29 | ```bash 30 | weave rollup stop 31 | ``` 32 | 33 | ### Restart the node 34 | 35 | ```bash 36 | weave rollup restart 37 | ``` 38 | 39 | ### See the logs 40 | 41 | ```bash 42 | weave rollup log 43 | ``` 44 | 45 | ## Help 46 | 47 | To see all the available commands: 48 | 49 | ```bash 50 | weave rollup --help 51 | ``` 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/initia-labs/weave 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/BurntSushi/toml v1.4.0 9 | github.com/PuerkitoBio/goquery v1.10.0 10 | github.com/amplitude/analytics-go v1.0.2 11 | github.com/atotto/clipboard v0.1.4 12 | github.com/btcsuite/btcd/btcec/v2 v2.3.4 13 | github.com/btcsuite/btcutil v1.0.2 14 | github.com/charmbracelet/bubbles v0.20.0 15 | github.com/charmbracelet/bubbletea v1.1.0 16 | github.com/charmbracelet/lipgloss v0.13.0 17 | github.com/charmbracelet/x/term v0.2.0 18 | github.com/fynelabs/selfupdate v0.2.0 19 | github.com/google/uuid v1.6.0 20 | github.com/muesli/reflow v0.3.0 21 | github.com/pelletier/go-toml/v2 v2.2.2 22 | github.com/spf13/cobra v1.8.1 23 | github.com/spf13/viper v1.19.0 24 | github.com/stretchr/testify v1.9.0 25 | github.com/test-go/testify v1.1.4 26 | github.com/tyler-smith/go-bip32 v1.0.0 27 | github.com/tyler-smith/go-bip39 v1.1.0 28 | golang.org/x/crypto v0.31.0 29 | google.golang.org/grpc v1.62.1 30 | ) 31 | 32 | require ( 33 | github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect 34 | github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect 35 | github.com/andybalholm/cascadia v1.3.2 // indirect 36 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 37 | github.com/charmbracelet/harmonica v0.2.0 // indirect 38 | github.com/charmbracelet/x/ansi v0.2.3 // indirect 39 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 40 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 41 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 42 | github.com/fsnotify/fsnotify v1.7.0 // indirect 43 | github.com/golang/protobuf v1.5.3 // indirect 44 | github.com/hashicorp/hcl v1.0.0 // indirect 45 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 46 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 47 | github.com/magiconair/properties v1.8.7 // indirect 48 | github.com/mattn/go-isatty v0.0.20 // indirect 49 | github.com/mattn/go-localereader v0.0.1 // indirect 50 | github.com/mattn/go-runewidth v0.0.16 // indirect 51 | github.com/mitchellh/mapstructure v1.5.0 // indirect 52 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 53 | github.com/muesli/cancelreader v0.2.2 // indirect 54 | github.com/muesli/termenv v0.15.2 // indirect 55 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 56 | github.com/rivo/uniseg v0.4.7 // indirect 57 | github.com/sagikazarmark/locafero v0.4.0 // indirect 58 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 59 | github.com/sourcegraph/conc v0.3.0 // indirect 60 | github.com/spf13/afero v1.11.0 // indirect 61 | github.com/spf13/cast v1.6.0 // indirect 62 | github.com/spf13/pflag v1.0.5 // indirect 63 | github.com/stretchr/objx v0.5.2 // indirect 64 | github.com/subosito/gotenv v1.6.0 // indirect 65 | go.uber.org/atomic v1.9.0 // indirect 66 | go.uber.org/multierr v1.9.0 // indirect 67 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 68 | golang.org/x/net v0.33.0 // indirect 69 | golang.org/x/sync v0.10.0 // indirect 70 | golang.org/x/sys v0.28.0 // indirect 71 | golang.org/x/text v0.21.0 // indirect 72 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect 73 | google.golang.org/protobuf v1.33.0 // indirect 74 | gopkg.in/ini.v1 v1.67.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /io/filesystem.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | 13 | "github.com/initia-labs/weave/client" 14 | ) 15 | 16 | // FileOrFolderExists checks if a file or folder exists at the given path. 17 | func FileOrFolderExists(path string) bool { 18 | _, err := os.Stat(path) 19 | return !os.IsNotExist(err) 20 | } 21 | 22 | func DownloadAndExtractTarGz(url, tarballPath, extractedPath string) error { 23 | httpClient := client.NewHTTPClient() 24 | if err := httpClient.DownloadFile(url, tarballPath, nil, nil); err != nil { 25 | return err 26 | } 27 | 28 | if err := ExtractTarGz(tarballPath, extractedPath); err != nil { 29 | return err 30 | } 31 | 32 | if err := os.Remove(tarballPath); err != nil { 33 | return fmt.Errorf("failed to remove tarball file: %v", err) 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func ExtractTarGz(src string, dest string) error { 40 | file, err := os.Open(src) 41 | if err != nil { 42 | return err 43 | } 44 | defer file.Close() 45 | 46 | gzr, err := gzip.NewReader(file) 47 | if err != nil { 48 | return err 49 | } 50 | defer gzr.Close() 51 | 52 | tarReader := tar.NewReader(gzr) 53 | for { 54 | header, err := tarReader.Next() 55 | if err == io.EOF { 56 | break 57 | } 58 | if err != nil { 59 | return err 60 | } 61 | 62 | target := filepath.Join(dest, header.Name) 63 | switch header.Typeflag { 64 | case tar.TypeDir: 65 | if err := os.MkdirAll(target, os.ModePerm); err != nil { 66 | return err 67 | } 68 | case tar.TypeReg: 69 | file, err := os.Create(target) 70 | if err != nil { 71 | return err 72 | } 73 | _, err = io.Copy(file, tarReader) 74 | if err != nil { 75 | return err 76 | } 77 | err = file.Close() 78 | if err != nil { 79 | return err 80 | } 81 | default: 82 | return fmt.Errorf("unknown type: %c", header.Typeflag) 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func SetLibraryPaths(binaryDir string) error { 89 | switch runtime.GOOS { 90 | case "darwin": 91 | if err := os.Setenv("DYLD_LIBRARY_PATH", binaryDir); err != nil { 92 | return fmt.Errorf("failed to set DYLD_LIBRARY_PATH: %v", err) 93 | } 94 | case "linux": 95 | if err := os.Setenv("LD_LIBRARY_PATH", binaryDir); err != nil { 96 | return fmt.Errorf("failed to set LD_LIBRARY_PATH: %v", err) 97 | } 98 | default: 99 | return fmt.Errorf("unsupported OS for setting library paths: %v", runtime.GOOS) 100 | } 101 | return nil 102 | } 103 | 104 | func WriteFile(path, content string) error { 105 | file, err := os.Create(path) 106 | if err != nil { 107 | return fmt.Errorf("failed to create or open file: %v", err) 108 | } 109 | defer file.Close() 110 | 111 | _, err = file.WriteString(content) 112 | if err != nil { 113 | return fmt.Errorf("failed to write content to file: %v", err) 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func DeleteFile(path string) error { 120 | err := os.Remove(path) 121 | if err != nil { 122 | return fmt.Errorf("failed to delete file: %v", err) 123 | } 124 | 125 | return nil 126 | } 127 | 128 | func DeleteDirectory(path string) error { 129 | err := os.RemoveAll(path) 130 | if err != nil { 131 | return fmt.Errorf("failed to delete directory: %v", err) 132 | } 133 | return nil 134 | } 135 | 136 | // CopyDirectory uses the cp -r command to copy files or directories from src to des. 137 | func CopyDirectory(src, des string) error { 138 | // Check if destination exists 139 | if _, err := os.Stat(des); err == nil { 140 | // Remove the contents of the destination directory 141 | err := os.RemoveAll(des) 142 | if err != nil { 143 | return fmt.Errorf("could not clear destination directory: %v", err) 144 | } 145 | } 146 | 147 | // Now, perform the copy 148 | cmd := exec.Command("cp", "-r", src, des) 149 | output, err := cmd.CombinedOutput() 150 | if err != nil { 151 | return fmt.Errorf("could not run cp command: %v, output: %s", err, string(output)) 152 | } 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /io/filesystem_test.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Mock HTTP client for testing 12 | type MockHTTPClient struct { 13 | mock.Mock 14 | } 15 | 16 | func (m *MockHTTPClient) DownloadFile(url, dest string, progress, totalSize *int64) error { 17 | args := m.Called(url, dest, progress, totalSize) 18 | return args.Error(0) 19 | } 20 | 21 | func TestFileOrFolderExists(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | path string 25 | exists bool 26 | }{ 27 | {"File exists", "./testfile", true}, 28 | {"File does not exist", "./nonexistent", false}, 29 | } 30 | 31 | // Create a test file for this example 32 | t.Run("FileExists", func(t *testing.T) { 33 | f, err := os.Create("./testfile") 34 | assert.NoError(t, err) 35 | f.Close() 36 | defer os.Remove("./testfile") 37 | 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | assert.Equal(t, tt.exists, FileOrFolderExists(tt.path)) 41 | }) 42 | } 43 | }) 44 | } 45 | 46 | func TestDownloadAndExtractTarGz(t *testing.T) { 47 | client := new(MockHTTPClient) 48 | 49 | t.Run("TestDownloadAndExtractTarGzFailure", func(t *testing.T) { 50 | client.On("DownloadFile", "http://example.com/tarball.tar.gz", "./test.tar.gz", nil, nil).Return(assert.AnError) 51 | err := DownloadAndExtractTarGz("http://example.com/tarball.tar.gz", "./test.tar.gz", "./testdir") 52 | assert.Error(t, err) 53 | }) 54 | } 55 | 56 | func TestExtractTarGz(t *testing.T) { 57 | t.Run("TestExtractTarGzFailure", func(t *testing.T) { 58 | // Test invalid tarball 59 | err := ExtractTarGz("./invalid.tar.gz", "./invalid") 60 | assert.Error(t, err) 61 | }) 62 | } 63 | 64 | func TestSetLibraryPaths(t *testing.T) { 65 | t.Run("TestSetLibraryPathsLinux", func(t *testing.T) { 66 | // Mock Linux environment variable setting 67 | if err := os.Setenv("GOOS", "linux"); err != nil { 68 | t.Fatal("Failed to set GOOS environment variable") 69 | } 70 | // Normally, you'd check the environment variable being set 71 | SetLibraryPaths("./somepath") 72 | }) 73 | 74 | t.Run("TestSetLibraryPathsDarwin", func(t *testing.T) { 75 | // Mock Darwin environment variable setting 76 | if err := os.Setenv("GOOS", "darwin"); err != nil { 77 | t.Fatal("Failed to set GOOS environment variable") 78 | } 79 | SetLibraryPaths("./somepath") 80 | }) 81 | } 82 | 83 | func TestWriteFile(t *testing.T) { 84 | t.Run("TestWriteFileSuccess", func(t *testing.T) { 85 | err := WriteFile("./testfile.txt", "Hello, World!") 86 | assert.NoError(t, err) 87 | defer os.Remove("./testfile.txt") 88 | 89 | // Check file content 90 | content, err := os.ReadFile("./testfile.txt") 91 | assert.NoError(t, err) 92 | assert.Equal(t, "Hello, World!", string(content)) 93 | }) 94 | 95 | t.Run("TestWriteFileFailure", func(t *testing.T) { 96 | err := WriteFile("/invalid/path/to/file.txt", "Hello, World!") 97 | assert.Error(t, err) 98 | }) 99 | } 100 | 101 | func TestDeleteFile(t *testing.T) { 102 | t.Run("TestDeleteFileSuccess", func(t *testing.T) { 103 | _, err := os.Create("./fileToDelete.txt") 104 | assert.NoError(t, err) 105 | defer os.Remove("./fileToDelete.txt") 106 | 107 | err = DeleteFile("./fileToDelete.txt") 108 | assert.NoError(t, err) 109 | _, err = os.Stat("./fileToDelete.txt") 110 | assert.True(t, os.IsNotExist(err)) 111 | }) 112 | 113 | t.Run("TestDeleteFileFailure", func(t *testing.T) { 114 | err := DeleteFile("./nonexistent.txt") 115 | assert.Error(t, err) 116 | }) 117 | } 118 | 119 | func TestDeleteDirectory(t *testing.T) { 120 | t.Run("TestDeleteDirectorySuccess", func(t *testing.T) { 121 | err := os.Mkdir("./testdir", os.ModePerm) 122 | assert.NoError(t, err) 123 | defer os.RemoveAll("./testdir") 124 | 125 | err = DeleteDirectory("./testdir") 126 | assert.NoError(t, err) 127 | _, err = os.Stat("./testdir") 128 | assert.True(t, os.IsNotExist(err)) 129 | }) 130 | } 131 | 132 | func TestCopyDirectory(t *testing.T) { 133 | t.Run("TestCopyDirectorySuccess", func(t *testing.T) { 134 | err := os.Mkdir("./src", os.ModePerm) 135 | assert.NoError(t, err) 136 | defer os.RemoveAll("./src") 137 | 138 | err = os.Mkdir("./des", os.ModePerm) 139 | assert.NoError(t, err) 140 | defer os.RemoveAll("./des") 141 | 142 | err = CopyDirectory("./src", "./des") 143 | assert.NoError(t, err) 144 | }) 145 | 146 | t.Run("TestCopyDirectoryFailure", func(t *testing.T) { 147 | err := CopyDirectory("./nonexistentdir", "./des") 148 | assert.Error(t, err) 149 | }) 150 | } 151 | -------------------------------------------------------------------------------- /io/keyfile.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/initia-labs/weave/crypto" 9 | ) 10 | 11 | type Key struct { 12 | Address string `json:"address"` 13 | Mnemonic string `json:"mnemonic"` 14 | } 15 | 16 | func NewKey(address, mnemonic string) *Key { 17 | return &Key{ 18 | Address: address, 19 | Mnemonic: mnemonic, 20 | } 21 | } 22 | 23 | func GenerateKey(hrp string) (*Key, error) { 24 | mnemonic, err := crypto.GenerateMnemonic() 25 | if err != nil { 26 | return nil, fmt.Errorf("failed to generate mnemonic: %w", err) 27 | } 28 | 29 | address, err := crypto.MnemonicToBech32Address(hrp, mnemonic) 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to derive address: %w", err) 32 | } 33 | 34 | return &Key{ 35 | Mnemonic: mnemonic, 36 | Address: address, 37 | }, nil 38 | } 39 | 40 | func RecoverKey(hrp, mnemonic string) (*Key, error) { 41 | address, err := crypto.MnemonicToBech32Address(hrp, mnemonic) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to derive address: %w", err) 44 | } 45 | 46 | return &Key{ 47 | Mnemonic: mnemonic, 48 | Address: address, 49 | }, nil 50 | } 51 | 52 | type KeyFile map[string]*Key 53 | 54 | func NewKeyFile() KeyFile { 55 | kf := make(KeyFile) 56 | return kf 57 | } 58 | 59 | func (k KeyFile) AddKey(name string, key *Key) { 60 | k[name] = key 61 | } 62 | 63 | func (k KeyFile) GetMnemonic(name string) string { 64 | return k[name].Mnemonic 65 | } 66 | 67 | func (k KeyFile) Write(filePath string) error { 68 | data, err := json.MarshalIndent(k, "", " ") 69 | if err != nil { 70 | return fmt.Errorf("error marshaling KeyFile to JSON: %w", err) 71 | } 72 | 73 | return os.WriteFile(filePath, data, 0644) 74 | } 75 | 76 | // Load tries to load an existing key file into the struct if the file exists 77 | func (k KeyFile) Load(filePath string) error { 78 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 79 | return nil 80 | } 81 | 82 | data, err := os.ReadFile(filePath) 83 | if err != nil { 84 | return fmt.Errorf("error reading file: %w", err) 85 | } 86 | 87 | err = json.Unmarshal(data, &k) 88 | if err != nil { 89 | return fmt.Errorf("error unmarshaling JSON: %w", err) 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/initia-labs/weave/cmd" 9 | ) 10 | 11 | func main() { 12 | if err := executeWithRecovery(); err != nil { 13 | log.Fatal(err) 14 | } 15 | } 16 | 17 | func executeWithRecovery() (err error) { 18 | defer func() { 19 | if r := recover(); r != nil { 20 | // Print a clean error message to stderr on panic 21 | fmt.Fprintln(os.Stderr, "An unexpected error occurred:", r) 22 | err = fmt.Errorf("%v", r) 23 | } 24 | }() 25 | 26 | // Execute the main command 27 | return cmd.Execute() 28 | } 29 | -------------------------------------------------------------------------------- /models/homepage.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | 6 | "github.com/initia-labs/weave/models/weaveinit" 7 | "github.com/initia-labs/weave/styles" 8 | "github.com/initia-labs/weave/ui" 9 | ) 10 | 11 | type Homepage struct { 12 | ui.Selector[HomepageOption] 13 | TextInput ui.TextInput 14 | } 15 | 16 | type HomepageOption string 17 | 18 | const ( 19 | InitOption HomepageOption = "Weave Init" 20 | ) 21 | 22 | func NewHomepage() tea.Model { 23 | return &Homepage{ 24 | Selector: ui.Selector[HomepageOption]{ 25 | Options: []HomepageOption{ 26 | InitOption, 27 | }, 28 | Cursor: 0, 29 | }, 30 | TextInput: ui.NewTextInput(true), 31 | } 32 | } 33 | 34 | func (m *Homepage) Init() tea.Cmd { 35 | return nil 36 | } 37 | 38 | func (m *Homepage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 39 | 40 | selected, cmd := m.Select(msg) 41 | if selected != nil { 42 | switch *selected { 43 | case InitOption: 44 | return weaveinit.NewWeaveInit(), nil 45 | } 46 | } 47 | 48 | return m, cmd 49 | } 50 | 51 | func (m *Homepage) View() string { 52 | view := styles.FadeText("\nWelcome to Weave! 🪢 CLI for managing Initia deployments.\n") 53 | view += styles.RenderPrompt("What would you like to do today?", []string{}, styles.Question) + m.Selector.View() 54 | return view 55 | } 56 | -------------------------------------------------------------------------------- /models/initia/constants.go: -------------------------------------------------------------------------------- 1 | package initia 2 | 3 | const ( 4 | DefaultGasPriceDenom string = "uinit" 5 | CosmovisorVersion string = "v1.7.0" 6 | ) 7 | 8 | var ( 9 | PolkachuChainIdSlugMap = map[string]string{ 10 | "interwoven-1": "tendermint_snapshots/initia", 11 | "initiation-2": "testnets/initia/snapshots", 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /models/initia/state.go: -------------------------------------------------------------------------------- 1 | package initia 2 | 3 | import ( 4 | "github.com/initia-labs/weave/registry" 5 | "github.com/initia-labs/weave/types" 6 | ) 7 | 8 | // RunL1NodeState represents the configuration state of a Layer 1 Node 9 | type RunL1NodeState struct { 10 | weave types.WeaveState 11 | network string 12 | chainType registry.ChainType 13 | chainRegistry *registry.ChainRegistry // We can store the registry here since we only need one 14 | initiadVersion string 15 | initiadEndpoint string 16 | chainId string 17 | moniker string 18 | existingApp bool 19 | replaceExistingApp bool 20 | minGasPrice string 21 | enableLCD bool 22 | enableGRPC bool 23 | seeds string 24 | persistentPeers string 25 | existingGenesis bool 26 | genesisEndpoint string 27 | existingData bool 28 | syncMethod string 29 | replaceExistingData bool 30 | replaceExistingGenesisWithDefault bool 31 | snapshotEndpoint string 32 | stateSyncEndpoint string 33 | additionalStateSyncPeers string 34 | allowAutoUpgrade bool 35 | pruning string 36 | } 37 | 38 | // NewRunL1NodeState initializes a new RunL1NodeState with default values. 39 | func NewRunL1NodeState() RunL1NodeState { 40 | return RunL1NodeState{ 41 | weave: types.NewWeaveState(), 42 | } 43 | } 44 | 45 | // Clone creates a deep copy of RunL1NodeState without pointers. 46 | func (s RunL1NodeState) Clone() RunL1NodeState { 47 | return RunL1NodeState{ 48 | weave: s.weave.Clone(), // Assuming WeaveState has a Clone method 49 | network: s.network, 50 | chainType: s.chainType, 51 | chainRegistry: s.chainRegistry, 52 | initiadVersion: s.initiadVersion, 53 | initiadEndpoint: s.initiadEndpoint, 54 | chainId: s.chainId, 55 | moniker: s.moniker, 56 | existingApp: s.existingApp, 57 | replaceExistingApp: s.replaceExistingApp, 58 | minGasPrice: s.minGasPrice, 59 | enableLCD: s.enableLCD, 60 | enableGRPC: s.enableGRPC, 61 | seeds: s.seeds, 62 | persistentPeers: s.persistentPeers, 63 | existingGenesis: s.existingGenesis, 64 | genesisEndpoint: s.genesisEndpoint, 65 | existingData: s.existingData, 66 | syncMethod: s.syncMethod, 67 | replaceExistingData: s.replaceExistingData, 68 | replaceExistingGenesisWithDefault: s.replaceExistingGenesisWithDefault, 69 | snapshotEndpoint: s.snapshotEndpoint, 70 | stateSyncEndpoint: s.stateSyncEndpoint, 71 | additionalStateSyncPeers: s.additionalStateSyncPeers, 72 | allowAutoUpgrade: s.allowAutoUpgrade, 73 | pruning: s.pruning, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /models/minitia/constants.go: -------------------------------------------------------------------------------- 1 | package minitia 2 | 3 | import "math/big" 4 | 5 | const ( 6 | AppName string = "minitiad" 7 | 8 | OperatorKeyName string = "weave.Operator" 9 | BridgeExecutorKeyName string = "weave.BridgeExecutor" 10 | OutputSubmitterKeyName string = "weave.OutputSubmitter" 11 | BatchSubmitterKeyName string = "weave.BatchSubmitter" 12 | ChallengerKeyName string = "weave.Challenger" 13 | 14 | DefaultL1BridgeExecutorBalance string = "2000000" 15 | DefaultL1OutputSubmitterBalance string = "2000000" 16 | DefaultL1BatchSubmitterBalance string = "1000000" 17 | DefaultL1ChallengerBalance string = "2000000" 18 | DefaultL2BridgeExecutorBalance string = "100000000" 19 | 20 | TmpTxFilename string = "weave.minitia.tx.json" 21 | 22 | DefaultL1GasDenom string = "uinit" 23 | DefaultL1GasPrices = "0.15" + DefaultL1GasDenom 24 | DefaultCelestiaGasDenom string = "utia" 25 | 26 | MaxMonikerLength int = 70 27 | MaxChainIDLength int = 50 28 | 29 | LaunchConfigFilename = "minitia.config.json" 30 | 31 | CelestiaAppName string = "celestia-appd" 32 | 33 | InitiaScanMainnetURL string = "https://scan.initia.xyz" 34 | InitiaScanTestnetURL string = "https://scan.testnet.initia.xyz" 35 | 36 | DefaultMinitiaLCD string = "http://localhost:1317" 37 | DefaultMinitiaRPC string = "http://localhost:26657" 38 | DefaultMinitiaWebsocket string = "ws://localhost:26657/websocket" 39 | DefaultMinitiaGRPC string = "http://localhost:9090" 40 | DefaultMinitiaJsonRPC string = "http://localhost:8545" 41 | DefaultMinitiaJsonRPCWS string = "ws://localhost:8546" 42 | 43 | DefaultRollupDenom string = "umin" 44 | DefaultMinievmDenom string = "GAS" 45 | ) 46 | 47 | var ( 48 | DefaultL1InitiaNeededBalanceIfCelestiaDA string 49 | DefaultL1InitiaNeededBalanceIfInitiaDA string 50 | ) 51 | 52 | func init() { 53 | total := big.NewInt(0) 54 | values := []string{ 55 | DefaultL1BridgeExecutorBalance, 56 | DefaultL1OutputSubmitterBalance, 57 | DefaultL1ChallengerBalance, 58 | } 59 | 60 | for _, v := range values { 61 | num := new(big.Int) 62 | num, _ = num.SetString(v, 10) 63 | total.Add(total, num) 64 | } 65 | 66 | DefaultL1InitiaNeededBalanceIfCelestiaDA = total.String() 67 | 68 | num := new(big.Int) 69 | num, _ = num.SetString(DefaultL1BatchSubmitterBalance, 10) 70 | total.Add(total, num) 71 | DefaultL1InitiaNeededBalanceIfInitiaDA = total.String() 72 | } 73 | -------------------------------------------------------------------------------- /models/minitia/state_test.go: -------------------------------------------------------------------------------- 1 | package minitia 2 | 3 | // 4 | //import ( 5 | // "testing" 6 | // 7 | // "github.com/stretchr/testify/assert" 8 | //) 9 | // 10 | //func TestFinalizeGenesisAccounts(t *testing.T) { 11 | // launchState := NewLaunchState() 12 | // launchState.batchSubmissionIsCelestia = false 13 | // 14 | // // Set up test data for system keys and balances 15 | // launchState.systemKeyOperatorAddress = "operator-address" 16 | // launchState.systemKeyBridgeExecutorAddress = "bridge-executor-address" 17 | // launchState.systemKeyOutputSubmitterAddress = "output-submitter-address" 18 | // launchState.systemKeyBatchSubmitterAddress = "batch-submitter-address" 19 | // launchState.systemKeyChallengerAddress = "challenger-address" 20 | // 21 | // launchState.systemKeyL2OperatorBalance = "100operator" 22 | // launchState.systemKeyL2BridgeExecutorBalance = "200bridge" 23 | // launchState.systemKeyL2OutputSubmitterBalance = "300output" 24 | // launchState.systemKeyL2BatchSubmitterBalance = "400batch" 25 | // launchState.systemKeyL2ChallengerBalance = "500challenger" 26 | // 27 | // // Call the FinalizeGenesisAccounts method 28 | // launchState.FinalizeGenesisAccounts() 29 | // 30 | // // Assertions to check if genesisAccounts is populated correctly 31 | // assert.Equal(t, 5, len(launchState.genesisAccounts), "Expected 5 genesis accounts") 32 | // 33 | // assert.Equal(t, "operator-address", launchState.genesisAccounts[0].Address) 34 | // assert.Equal(t, "100operator", launchState.genesisAccounts[0].Coins) 35 | // 36 | // assert.Equal(t, "bridge-executor-address", launchState.genesisAccounts[1].Address) 37 | // assert.Equal(t, "200bridge", launchState.genesisAccounts[1].Coins) 38 | // 39 | // assert.Equal(t, "output-submitter-address", launchState.genesisAccounts[2].Address) 40 | // assert.Equal(t, "300output", launchState.genesisAccounts[2].Coins) 41 | // 42 | // assert.Equal(t, "challenger-address", launchState.genesisAccounts[3].Address) 43 | // assert.Equal(t, "500challenger", launchState.genesisAccounts[3].Coins) 44 | // 45 | // assert.Equal(t, "batch-submitter-address", launchState.genesisAccounts[4].Address) 46 | // assert.Equal(t, "400batch", launchState.genesisAccounts[4].Coins) 47 | //} 48 | -------------------------------------------------------------------------------- /models/opinit_bots/bots.go: -------------------------------------------------------------------------------- 1 | package opinit_bots 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/initia-labs/weave/common" 11 | "github.com/initia-labs/weave/cosmosutils" 12 | ) 13 | 14 | // BotName defines a custom type for the bot names 15 | type BotName string 16 | 17 | // Create constants for the BotNames 18 | const ( 19 | BridgeExecutor BotName = "Bridge Executor" 20 | OutputSubmitter BotName = "Output Submitter" 21 | BatchSubmitter BotName = "Batch Submitter" 22 | Challenger BotName = "Challenger" 23 | OracleBridgeExecutor BotName = "Oracle Bridge Executor" 24 | ) 25 | 26 | // BotKeyName defines a custom type for bot key names 27 | type BotKeyName string 28 | 29 | // Create constants for the bot key names 30 | const ( 31 | BridgeExecutorKeyName = "weave_bridge_executor" 32 | OutputSubmitterKeyName = "weave_output_submitter" 33 | BatchSubmitterKeyName = "weave_batch_submitter" 34 | ChallengerKeyName = "weave_challenger" 35 | OracleBridgeExecutorKeyName = "weave_oracle_bridge_executor" 36 | ) 37 | 38 | // BotNames to hold all bot names 39 | var BotNames = []BotName{ 40 | BridgeExecutor, 41 | OutputSubmitter, 42 | BatchSubmitter, 43 | Challenger, 44 | OracleBridgeExecutor, 45 | } 46 | 47 | var BotNameToKeyName = map[BotName]BotKeyName{ 48 | BridgeExecutor: BridgeExecutorKeyName, 49 | OutputSubmitter: OutputSubmitterKeyName, 50 | BatchSubmitter: BatchSubmitterKeyName, 51 | Challenger: ChallengerKeyName, 52 | OracleBridgeExecutor: OracleBridgeExecutorKeyName, 53 | } 54 | 55 | // BotInfo struct to hold all relevant bot information 56 | type BotInfo struct { 57 | BotName BotName 58 | IsSetup bool 59 | KeyName string 60 | Mnemonic string 61 | IsNotExist bool // Indicates if the key doesn't exist in the `initiad keys list` output 62 | IsGenerateKey bool 63 | DALayer string 64 | } 65 | 66 | // BotInfos for all bots with key names filled in 67 | var BotInfos = []BotInfo{ 68 | { 69 | BotName: BridgeExecutor, 70 | IsSetup: false, // Default isn't set up 71 | KeyName: BridgeExecutorKeyName, 72 | Mnemonic: "", // Add mnemonic if needed 73 | IsNotExist: false, 74 | }, 75 | { 76 | BotName: OutputSubmitter, 77 | IsSetup: false, // Default isn't set up 78 | KeyName: OutputSubmitterKeyName, 79 | Mnemonic: "", // Add mnemonic if needed 80 | IsNotExist: false, 81 | }, 82 | { 83 | BotName: BatchSubmitter, 84 | IsSetup: false, // Default isn't set up 85 | KeyName: BatchSubmitterKeyName, 86 | Mnemonic: "", // Add mnemonic if needed 87 | IsNotExist: false, 88 | }, 89 | { 90 | BotName: Challenger, 91 | IsSetup: false, // Default isn't set up 92 | KeyName: ChallengerKeyName, 93 | Mnemonic: "", // Add mnemonic if needed 94 | IsNotExist: false, 95 | }, 96 | { 97 | BotName: OracleBridgeExecutor, 98 | IsSetup: false, // Default isn't set up 99 | KeyName: OracleBridgeExecutorKeyName, 100 | Mnemonic: "", // Add mnemonic if needed 101 | IsNotExist: false, 102 | }, 103 | } 104 | 105 | func (b BotInfo) IsNewKey() bool { 106 | return b.Mnemonic != "" || b.IsGenerateKey 107 | } 108 | 109 | func GetBotInfo(botInfos []BotInfo, name BotName) BotInfo { 110 | for _, botInfo := range botInfos { 111 | if botInfo.BotName == name { 112 | return botInfo 113 | } 114 | } 115 | return BotInfo{} 116 | } 117 | 118 | // CheckIfKeysExist checks the output of `initiad keys list` and sets IsNotExist for missing keys 119 | func CheckIfKeysExist(botInfos []BotInfo) ([]BotInfo, error) { 120 | userHome, err := os.UserHomeDir() 121 | if err != nil { 122 | return nil, fmt.Errorf("could not get user home dir: %v", err) 123 | } 124 | version, _, err := cosmosutils.GetLatestOPInitBotVersion() 125 | if err != nil { 126 | return nil, fmt.Errorf("could not get latest opinitd version: %v", err) 127 | } 128 | binaryPath := filepath.Join(userHome, common.WeaveDataDirectory, fmt.Sprintf("opinitd@%s", version), AppName) 129 | cmd := exec.Command(binaryPath, "keys", "list", "weave-dummy") 130 | outputBytes, err := cmd.Output() 131 | if err != nil { 132 | for i := range botInfos { 133 | botInfos[i].IsNotExist = true 134 | } 135 | return botInfos, nil 136 | } 137 | output := string(outputBytes) 138 | 139 | // Split the output by line and check if the KeyName exists 140 | for i := range botInfos { 141 | if !strings.Contains(output, botInfos[i].KeyName) { 142 | botInfos[i].IsNotExist = true 143 | } 144 | } 145 | return botInfos, nil 146 | } 147 | -------------------------------------------------------------------------------- /models/opinit_bots/config.go: -------------------------------------------------------------------------------- 1 | package opinit_bots 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | weaveio "github.com/initia-labs/weave/io" 8 | ) 9 | 10 | type NodeConfig struct { 11 | ChainID string `json:"chain_id"` 12 | Bech32Prefix string `json:"bech32_prefix"` 13 | RPCAddress string `json:"rpc_address"` 14 | } 15 | 16 | type ChallengerConfig struct { 17 | Version int `json:"version"` 18 | Server ServerConfig `json:"server"` 19 | L1Node NodeConfig `json:"l1_node"` 20 | L2Node NodeConfig `json:"l2_node"` 21 | L1StartHeight int `json:"l1_start_height"` 22 | L2StartHeight int `json:"l2_start_height"` 23 | DisableAutoSetL1Height bool `json:"disable_auto_set_l1_height"` 24 | } 25 | 26 | type NodeSettings struct { 27 | ChainID string `json:"chain_id"` 28 | Bech32Prefix string `json:"bech32_prefix"` 29 | RPCAddress string `json:"rpc_address"` 30 | GasPrice string `json:"gas_price"` 31 | GasAdjustment float64 `json:"gas_adjustment"` 32 | TxTimeout int `json:"tx_timeout"` 33 | } 34 | 35 | type ServerConfig struct { 36 | Address string `json:"address"` 37 | AllowOrigins string `json:"allow_origins"` 38 | AllowHeaders string `json:"allow_headers"` 39 | AllowMethods string `json:"allow_methods"` 40 | } 41 | 42 | type ExecutorConfig struct { 43 | Version int `json:"version"` 44 | Server ServerConfig `json:"server"` 45 | L1Node NodeSettings `json:"l1_node"` 46 | L2Node NodeSettings `json:"l2_node"` 47 | DANode NodeSettings `json:"da_node"` 48 | BridgeExecutor string `json:"bridge_executor"` 49 | OracleBridgeExecutor string `json:"oracle_bridge_executor"` 50 | DisableOutputSubmitter bool `json:"disable_output_submitter"` 51 | DisableBatchSubmitter bool `json:"disable_batch_submitter"` 52 | MaxChunks int `json:"max_chunks"` 53 | MaxChunkSize int `json:"max_chunk_size"` 54 | MaxSubmissionTime int `json:"max_submission_time"` 55 | DisableAutoSetL1Height bool `json:"disable_auto_set_l1_height"` 56 | L1StartHeight int `json:"l1_start_height"` 57 | L2StartHeight int `json:"l2_start_height"` 58 | BatchStartHeight int `json:"batch_start_height"` 59 | DisableDeleteFutureWithdrawal bool `json:"disable_delete_future_withdrawal"` 60 | } 61 | 62 | func GenerateMnemonicKeyfile(rawConfig []byte, botName string) (weaveio.KeyFile, error) { 63 | keyFile := weaveio.NewKeyFile() 64 | 65 | switch botName { 66 | case "executor": 67 | var config ExecutorConfig 68 | err := json.Unmarshal(rawConfig, &config) 69 | if err != nil { 70 | return nil, fmt.Errorf("failed to unmarshal executor config: %v", err) 71 | } 72 | 73 | bridgeExecutor, err := weaveio.GenerateKey("init") 74 | if err != nil { 75 | return nil, fmt.Errorf("failed to generate bridge executor mnemonic: %w", err) 76 | } 77 | keyFile.AddKey(BridgeExecutorKeyName, bridgeExecutor) 78 | 79 | outputSubmitter, err := weaveio.GenerateKey("init") 80 | if err != nil { 81 | return nil, fmt.Errorf("failed to generate output submitter mnemonic: %w", err) 82 | } 83 | keyFile.AddKey(OutputSubmitterKeyName, outputSubmitter) 84 | 85 | batchSubmitter, err := weaveio.GenerateKey(config.DANode.Bech32Prefix) 86 | if err != nil { 87 | return nil, fmt.Errorf("failed to generate batch submitter mnemonic: %w", err) 88 | } 89 | keyFile.AddKey(BatchSubmitterKeyName, batchSubmitter) 90 | 91 | oracleBridgeExecutor, err := weaveio.GenerateKey("init") 92 | if err != nil { 93 | return nil, fmt.Errorf("failed to generate oracle bridge executor mnemonic: %w", err) 94 | } 95 | keyFile.AddKey(OracleBridgeExecutorKeyName, oracleBridgeExecutor) 96 | 97 | return keyFile, nil 98 | case "challenger": 99 | challenger, err := weaveio.GenerateKey("init") 100 | if err != nil { 101 | return nil, fmt.Errorf("failed to generate challenger mnemonic: %w", err) 102 | } 103 | keyFile.AddKey(ChallengerKeyName, challenger) 104 | 105 | return keyFile, nil 106 | default: 107 | return nil, fmt.Errorf("unsupported bot name: %s", botName) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /models/opinit_bots/constants.go: -------------------------------------------------------------------------------- 1 | package opinit_bots 2 | 3 | const ( 4 | AppName string = "opinitd" 5 | 6 | DefaultInitiaGasDenom string = "uinit" 7 | 8 | DefaultCelestiaGasDenom string = "utia" 9 | DefaultCelestiaGasPrices = "0.04" + DefaultCelestiaGasDenom 10 | ) 11 | -------------------------------------------------------------------------------- /models/opinit_bots/field_input_model.go: -------------------------------------------------------------------------------- 1 | package opinit_bots 2 | 3 | import ( 4 | "context" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | weavecontext "github.com/initia-labs/weave/context" 9 | ) 10 | 11 | type FieldInputModel struct { 12 | currentIndex int // The index of the current active submodel 13 | weavecontext.BaseModel 14 | newTerminalModel func(context.Context) (tea.Model, error) 15 | subModels []SubModel 16 | } 17 | 18 | // NewFieldInputModel initializes the parent model with the submodels 19 | func NewFieldInputModel(ctx context.Context, fields []*Field, newTerminalModel func(context.Context) (tea.Model, error)) *FieldInputModel { 20 | subModels := make([]SubModel, len(fields)) 21 | 22 | // Create submodels based on the field types 23 | for idx, field := range fields { 24 | subModels[idx] = NewSubModel(*field) 25 | } 26 | 27 | return &FieldInputModel{ 28 | currentIndex: 0, 29 | BaseModel: weavecontext.BaseModel{Ctx: ctx}, 30 | newTerminalModel: newTerminalModel, 31 | subModels: subModels, 32 | } 33 | } 34 | 35 | func (m *FieldInputModel) Init() tea.Cmd { 36 | return nil 37 | } 38 | 39 | // Update delegates the update logic to the current active submodel 40 | func (m *FieldInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 41 | if model, cmd, handled := weavecontext.HandleCommonCommands[OPInitBotsState](m, msg); handled { 42 | if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() != "ctrl+t" { 43 | m.subModels[m.currentIndex].Text = "" 44 | m.subModels[m.currentIndex].Cursor = 0 45 | m.currentIndex-- 46 | } 47 | return model, cmd 48 | } 49 | 50 | currentModel := m.subModels[m.currentIndex] 51 | ctx, updatedModel, cmd := currentModel.UpdateWithContext(m.Ctx, m, msg) 52 | m.Ctx = ctx 53 | if updatedModel == nil { 54 | m.currentIndex++ 55 | if m.currentIndex < len(m.subModels) { 56 | return m, cmd 57 | } 58 | 59 | model, err := m.newTerminalModel(m.Ctx) 60 | if err != nil { 61 | return m, m.HandlePanic(err) 62 | } 63 | return model, model.Init() 64 | } 65 | 66 | m.subModels[m.currentIndex] = *updatedModel 67 | return m, cmd 68 | } 69 | 70 | // View delegates the view logic to the current active submodel 71 | func (m *FieldInputModel) View() string { 72 | state := weavecontext.GetCurrentState[OPInitBotsState](m.Ctx) 73 | m.subModels[m.currentIndex].ViewTooltip(m.Ctx) 74 | return m.WrapView(state.weave.Render() + m.subModels[m.currentIndex].ViewWithContext(m.Ctx)) 75 | } 76 | -------------------------------------------------------------------------------- /models/opinit_bots/state.go: -------------------------------------------------------------------------------- 1 | package opinit_bots 2 | 3 | import ( 4 | "github.com/initia-labs/weave/types" 5 | ) 6 | 7 | // OPInitBotsState is the structure holding the bot state and related configurations 8 | type OPInitBotsState struct { 9 | BotInfos []BotInfo 10 | SetupOpinitResponses map[BotName]string 11 | OPInitBotVersion string 12 | OPInitBotEndpoint string 13 | MinitiaConfig *types.MinitiaConfig 14 | weave types.WeaveState 15 | InitExecutorBot bool 16 | InitChallengerBot bool 17 | ReplaceBotConfig bool 18 | Version string 19 | ListenAddress string 20 | L1ChainId string 21 | L1RPCAddress string 22 | L1GasPrice string 23 | botConfig map[string]string 24 | daIsCelestia bool 25 | dbPath string 26 | isDeleteDB bool 27 | AddMinitiaConfig bool 28 | UsePrefilledMinitia bool 29 | L1StartHeight int 30 | } 31 | 32 | // NewOPInitBotsState initializes OPInitBotsState with default values 33 | func NewOPInitBotsState() OPInitBotsState { 34 | botInfos, err := CheckIfKeysExist(BotInfos) 35 | if err != nil { 36 | panic(err) 37 | } 38 | return OPInitBotsState{ 39 | BotInfos: botInfos, 40 | SetupOpinitResponses: make(map[BotName]string), 41 | weave: types.NewWeaveState(), 42 | MinitiaConfig: nil, 43 | botConfig: make(map[string]string), 44 | AddMinitiaConfig: false, 45 | UsePrefilledMinitia: false, 46 | L1StartHeight: 0, 47 | } 48 | } 49 | 50 | // Clone creates a deep copy of the OPInitBotsState to ensure state independence 51 | func (state OPInitBotsState) Clone() OPInitBotsState { 52 | clone := OPInitBotsState{ 53 | BotInfos: make([]BotInfo, len(state.BotInfos)), 54 | SetupOpinitResponses: make(map[BotName]string), 55 | OPInitBotVersion: state.OPInitBotVersion, 56 | OPInitBotEndpoint: state.OPInitBotEndpoint, 57 | MinitiaConfig: state.MinitiaConfig, // Assuming this can be reused or cloned if necessary 58 | weave: state.weave, // Assuming weave can be reused or cloned if necessary 59 | InitExecutorBot: state.InitExecutorBot, 60 | InitChallengerBot: state.InitChallengerBot, 61 | ReplaceBotConfig: state.ReplaceBotConfig, 62 | Version: state.Version, 63 | ListenAddress: state.ListenAddress, 64 | L1ChainId: state.L1ChainId, 65 | L1RPCAddress: state.L1RPCAddress, 66 | L1GasPrice: state.L1GasPrice, 67 | botConfig: make(map[string]string), 68 | dbPath: state.dbPath, 69 | isDeleteDB: state.isDeleteDB, 70 | AddMinitiaConfig: state.AddMinitiaConfig, 71 | L1StartHeight: state.L1StartHeight, 72 | } 73 | 74 | if state.MinitiaConfig != nil { 75 | clone.MinitiaConfig = state.MinitiaConfig.Clone() 76 | } 77 | clone.weave = state.weave.Clone() 78 | // Copy slice data 79 | copy(clone.BotInfos, state.BotInfos) 80 | 81 | // Copy map data 82 | for k, v := range state.SetupOpinitResponses { 83 | clone.SetupOpinitResponses[k] = v 84 | } 85 | for k, v := range state.botConfig { 86 | clone.botConfig[k] = v 87 | } 88 | 89 | return clone 90 | } 91 | -------------------------------------------------------------------------------- /models/opinit_bots/submodel.go: -------------------------------------------------------------------------------- 1 | package opinit_bots 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | 11 | weavecontext "github.com/initia-labs/weave/context" 12 | "github.com/initia-labs/weave/styles" 13 | "github.com/initia-labs/weave/ui" 14 | ) 15 | 16 | type FieldType int 17 | 18 | const ( 19 | StringField FieldType = iota 20 | NumberField 21 | // Add other types as needed 22 | ) 23 | 24 | type Field struct { 25 | Name string 26 | Type FieldType 27 | Question string 28 | Placeholder string 29 | DefaultValue string 30 | PrefillValue string 31 | ValidateFn func(string) error 32 | Tooltip *ui.Tooltip 33 | Highlights []string 34 | } 35 | 36 | type SubModel struct { 37 | weavecontext.BaseModel 38 | ui.TextInput 39 | field Field 40 | CannotBack bool 41 | highlights []string 42 | } 43 | 44 | func NewSubModel(field Field) SubModel { 45 | textInput := ui.NewTextInput(false) 46 | textInput.WithPlaceholder(field.Placeholder) 47 | textInput.WithDefaultValue(field.DefaultValue) 48 | textInput.WithPrefillValue(field.PrefillValue) 49 | textInput.WithValidatorFn(field.ValidateFn) 50 | textInput.WithTooltip(field.Tooltip) 51 | 52 | switch field.Type { 53 | case NumberField: 54 | textInput.WithValidatorFn(func(input string) error { 55 | if _, err := strconv.Atoi(input); err != nil { 56 | return fmt.Errorf("please enter a valid number") 57 | } 58 | return nil 59 | }) 60 | } 61 | 62 | s := SubModel{ 63 | TextInput: textInput, 64 | field: field, 65 | } 66 | if field.Highlights != nil { 67 | s.highlights = field.Highlights 68 | } else { 69 | s.highlights = []string{"L1", "Rollup", "rollup"} 70 | } 71 | 72 | return s 73 | } 74 | 75 | // Init is a common Init method for all field models 76 | func (m *SubModel) Init() tea.Cmd { 77 | return nil 78 | } 79 | 80 | func (m *SubModel) Update(_ tea.Msg) (tea.Model, tea.Cmd) { 81 | return m, nil 82 | } 83 | 84 | func (m *SubModel) UpdateWithContext(ctx context.Context, parent weavecontext.BaseModelInterface, msg tea.Msg) (context.Context, *SubModel, tea.Cmd) { 85 | input, cmd, done := m.TextInput.Update(msg) 86 | if done { 87 | state := weavecontext.PushPageAndGetState[OPInitBotsState](parent) 88 | res := strings.TrimSpace(input.Text) 89 | state.botConfig[m.field.Name] = res 90 | state.weave.PushPreviousResponse(styles.RenderPreviousResponse(styles.DotsSeparator, m.field.Question, m.highlights, res)) 91 | ctx = weavecontext.SetCurrentState(ctx, state) 92 | return ctx, nil, nil // Done with this field, signal completion 93 | } 94 | m.TextInput = input 95 | return ctx, m, cmd 96 | } 97 | 98 | func (m *SubModel) View() string { 99 | return "" 100 | } 101 | 102 | // ViewWithContext is a common View method for all field models 103 | func (m *SubModel) ViewWithContext(ctx context.Context) string { 104 | m.Ctx = ctx 105 | return m.WrapView(styles.RenderPrompt(m.field.Question, m.highlights, styles.Question) + m.TextInput.View()) 106 | } 107 | -------------------------------------------------------------------------------- /models/relayer/constants.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | const ( 4 | HermesVersion = "v1.10.4" 5 | 6 | DefaultGasPriceDenom = "uinit" 7 | DefaultGasPriceAmount = "0.15" 8 | InitiaTestnetChainId = "initiation-2" 9 | InitiaMainnetChainId = "interwoven-1" 10 | 11 | DefaultL1RelayerBalance = "1000000" 12 | DefaultL2RelayerBalance = "1000000" 13 | DefaultL1RelayerKeyName = "weave_l1_relayer" 14 | DefaultL2RelayerKeyName = "weave_l2_relayer" 15 | ) 16 | -------------------------------------------------------------------------------- /models/relayer/field_input_model.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | import ( 4 | "context" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | weavecontext "github.com/initia-labs/weave/context" 9 | ) 10 | 11 | type FieldInputModel struct { 12 | currentIndex int // The index of the current active submodel 13 | weavecontext.BaseModel 14 | newTerminalModel func(context.Context) (tea.Model, error) 15 | subModels []SubModel 16 | } 17 | 18 | // NewFieldInputModel initializes the parent model with the submodels 19 | func NewFieldInputModel(ctx context.Context, fields []*Field, newTerminalModel func(context.Context) (tea.Model, error)) *FieldInputModel { 20 | subModels := make([]SubModel, len(fields)) 21 | 22 | // Create submodels based on the field types 23 | for idx, field := range fields { 24 | subModels[idx] = NewSubModel(*field) 25 | } 26 | 27 | return &FieldInputModel{ 28 | currentIndex: 0, 29 | BaseModel: weavecontext.BaseModel{Ctx: ctx}, 30 | newTerminalModel: newTerminalModel, 31 | subModels: subModels, 32 | } 33 | } 34 | 35 | func (m *FieldInputModel) Init() tea.Cmd { 36 | return nil 37 | } 38 | 39 | // Update delegates the update logic to the current active submodel 40 | func (m *FieldInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 41 | if model, cmd, handled := weavecontext.HandleCommonCommands[State](m, msg); handled { 42 | if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() != "ctrl+t" { 43 | m.subModels[m.currentIndex].Text = "" 44 | m.subModels[m.currentIndex].Cursor = 0 45 | m.currentIndex-- 46 | } 47 | return model, cmd 48 | } 49 | 50 | currentModel := m.subModels[m.currentIndex] 51 | ctx, updatedModel, cmd := currentModel.UpdateWithContext(m.Ctx, m, msg) 52 | m.Ctx = ctx 53 | if updatedModel == nil { 54 | m.currentIndex++ 55 | if m.currentIndex < len(m.subModels) { 56 | return m, cmd 57 | } 58 | 59 | model, err := m.newTerminalModel(m.Ctx) 60 | if err != nil { 61 | return m, m.HandlePanic(err) 62 | } 63 | return model, model.Init() 64 | } 65 | 66 | m.subModels[m.currentIndex] = *updatedModel 67 | return m, cmd 68 | } 69 | 70 | // View delegates the view logic to the current active submodel 71 | func (m *FieldInputModel) View() string { 72 | state := weavecontext.GetCurrentState[State](m.Ctx) 73 | m.subModels[m.currentIndex].ViewTooltip(m.Ctx) 74 | return state.weave.Render() + m.subModels[m.currentIndex].View() 75 | } 76 | -------------------------------------------------------------------------------- /models/relayer/state.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | import ( 4 | "github.com/initia-labs/weave/types" 5 | ) 6 | 7 | type State struct { 8 | weave types.WeaveState 9 | Config map[string]string 10 | IBCChannels []types.IBCChannelPair 11 | 12 | l1KeyMethod string 13 | l1RelayerAddress string 14 | l1RelayerMnemonic string 15 | l1NeedsFunding bool 16 | l1FundingAmount string 17 | l1FundingTxHash string 18 | 19 | l2KeyMethod string 20 | l2RelayerAddress string 21 | l2RelayerMnemonic string 22 | l2NeedsFunding bool 23 | l2FundingAmount string 24 | l2FundingTxHash string 25 | 26 | hermesBinaryPath string 27 | 28 | minitiaConfig *types.MinitiaConfig 29 | 30 | feeWhitelistAccounts []string 31 | } 32 | 33 | func NewRelayerState() State { 34 | return State{ 35 | weave: types.NewWeaveState(), 36 | Config: make(map[string]string), 37 | IBCChannels: make([]types.IBCChannelPair, 0), 38 | feeWhitelistAccounts: make([]string, 0), 39 | } 40 | } 41 | 42 | func (state State) Clone() State { 43 | config := make(map[string]string) 44 | for k, v := range state.Config { 45 | config[k] = v 46 | } 47 | clone := State{ 48 | weave: state.weave, 49 | Config: config, 50 | IBCChannels: state.IBCChannels, 51 | 52 | l1KeyMethod: state.l1KeyMethod, 53 | l1RelayerAddress: state.l1RelayerAddress, 54 | l1RelayerMnemonic: state.l1RelayerMnemonic, 55 | l1NeedsFunding: state.l1NeedsFunding, 56 | l1FundingAmount: state.l1FundingAmount, 57 | l1FundingTxHash: state.l1FundingTxHash, 58 | 59 | l2KeyMethod: state.l2KeyMethod, 60 | l2RelayerAddress: state.l2RelayerAddress, 61 | l2RelayerMnemonic: state.l2RelayerMnemonic, 62 | l2NeedsFunding: state.l2NeedsFunding, 63 | l2FundingAmount: state.l2FundingAmount, 64 | l2FundingTxHash: state.l2FundingTxHash, 65 | 66 | hermesBinaryPath: state.hermesBinaryPath, 67 | 68 | feeWhitelistAccounts: state.feeWhitelistAccounts, 69 | } 70 | 71 | if state.minitiaConfig != nil { 72 | clone.minitiaConfig = state.minitiaConfig.Clone() 73 | } 74 | 75 | return clone 76 | } 77 | -------------------------------------------------------------------------------- /models/relayer/submodel.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | 11 | weavecontext "github.com/initia-labs/weave/context" 12 | "github.com/initia-labs/weave/styles" 13 | "github.com/initia-labs/weave/ui" 14 | ) 15 | 16 | type FieldType int 17 | 18 | const ( 19 | StringField FieldType = iota 20 | NumberField 21 | // Add other types as needed 22 | ) 23 | 24 | type Field struct { 25 | Name string 26 | Type FieldType 27 | Question string 28 | Placeholder string 29 | DefaultValue string 30 | PrefillValue string 31 | ValidateFn func(string) error 32 | Tooltip *ui.Tooltip 33 | Highlights []string 34 | } 35 | 36 | type SubModel struct { 37 | ui.TextInput 38 | field Field 39 | CannotBack bool 40 | highlights []string 41 | } 42 | 43 | func NewSubModel(field Field) SubModel { 44 | textInput := ui.NewTextInput(false) 45 | textInput.WithPlaceholder(field.Placeholder) 46 | textInput.WithDefaultValue(field.DefaultValue) 47 | textInput.WithPrefillValue(field.PrefillValue) 48 | textInput.WithValidatorFn(field.ValidateFn) 49 | textInput.WithTooltip(field.Tooltip) 50 | switch field.Type { 51 | case NumberField: 52 | textInput.WithValidatorFn(func(input string) error { 53 | if _, err := strconv.Atoi(input); err != nil { 54 | return fmt.Errorf("please enter a valid number") 55 | } 56 | return nil 57 | }) 58 | } 59 | 60 | s := SubModel{ 61 | TextInput: textInput, 62 | field: field, 63 | } 64 | if field.Highlights != nil { 65 | s.highlights = field.Highlights 66 | } else { 67 | s.highlights = []string{"L1", "Rollup", "rollup"} 68 | } 69 | 70 | return s 71 | } 72 | 73 | // Init is a common Init method for all field models 74 | func (m *SubModel) Init() tea.Cmd { 75 | return nil 76 | } 77 | 78 | func (m *SubModel) Update(_ tea.Msg) (tea.Model, tea.Cmd) { 79 | return m, nil 80 | } 81 | 82 | func (m *SubModel) UpdateWithContext(ctx context.Context, parent weavecontext.BaseModelInterface, msg tea.Msg) (context.Context, *SubModel, tea.Cmd) { 83 | input, cmd, done := m.TextInput.Update(msg) 84 | if done { 85 | state := weavecontext.PushPageAndGetState[State](parent) 86 | res := strings.TrimSpace(input.Text) 87 | state.Config[m.field.Name] = res 88 | state.weave.PushPreviousResponse(styles.RenderPreviousResponse(styles.DotsSeparator, m.field.Question, m.highlights, res)) 89 | ctx = weavecontext.SetCurrentState(ctx, state) 90 | return ctx, nil, nil // Done with this field, signal completion 91 | } 92 | m.TextInput = input 93 | return ctx, m, cmd 94 | } 95 | 96 | // View is a common View method for all field models 97 | func (m *SubModel) View() string { 98 | return styles.RenderPrompt(m.field.Question, m.highlights, styles.Question) + m.TextInput.View() 99 | } 100 | 101 | func GetField(fields []*Field, name string) (*Field, error) { 102 | for _, field := range fields { 103 | if field.Name == name { 104 | return field, nil 105 | } 106 | } 107 | return nil, fmt.Errorf("field %s not found", name) 108 | } 109 | -------------------------------------------------------------------------------- /models/relayer/update_client.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/BurntSushi/toml" 9 | 10 | "github.com/initia-labs/weave/common" 11 | "github.com/initia-labs/weave/cosmosutils" 12 | "github.com/initia-labs/weave/registry" 13 | ) 14 | 15 | func UpdateClientFromConfig() error { 16 | userHome, err := os.UserHomeDir() 17 | if err != nil { 18 | return err 19 | } 20 | configPath := filepath.Join(userHome, common.HermesHome, "config.toml") 21 | weaveDataPath := filepath.Join(userHome, common.WeaveDataDirectory) 22 | hermesBinaryPath := filepath.Join(weaveDataPath, "hermes") 23 | 24 | tomlData, err := os.ReadFile(configPath) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | var config Config 30 | err = toml.Unmarshal(tomlData, &config) 31 | if err != nil { 32 | return err 33 | } 34 | if len(config.Chains) < 2 { 35 | return fmt.Errorf("invalid configuration: missing chain configuration") 36 | } 37 | 38 | var chainRegistry *registry.ChainRegistry 39 | 40 | // Avoid panic until mainnet launches 41 | testnetRegistry, err := registry.GetChainRegistry(registry.InitiaL1Testnet) 42 | if err != nil { 43 | return fmt.Errorf("error loading testnet registry: %v", err) 44 | } 45 | if config.Chains[0].ID == testnetRegistry.GetChainId() { 46 | chainRegistry = testnetRegistry 47 | } else { 48 | mainnetRegistry, err := registry.GetChainRegistry(registry.InitiaL1Mainnet) 49 | if err != nil { 50 | return fmt.Errorf("error loading mainnet registry: %v", err) 51 | } 52 | if config.Chains[0].ID == mainnetRegistry.GetChainId() { 53 | chainRegistry = mainnetRegistry 54 | } 55 | } 56 | 57 | if chainRegistry == nil { 58 | return fmt.Errorf("chain registry not found") 59 | } 60 | 61 | clientIds := make(map[string]bool) 62 | for _, channel := range config.Chains[0].PacketFilter.List { 63 | connection, err := chainRegistry.GetCounterpartyClientId(channel[0], channel[1]) 64 | if err != nil { 65 | return err 66 | } 67 | clientIds[connection.Connection.Counterparty.ClientID] = true 68 | } 69 | te := cosmosutils.NewHermesTxExecutor(hermesBinaryPath) 70 | 71 | for clientId := range clientIds { 72 | fmt.Printf("Updating IBC client: %s of network: %s\n", clientId, config.Chains[1].ID) 73 | _, err := te.UpdateClient(clientId, config.Chains[1].ID) 74 | if err != nil { 75 | return err 76 | } 77 | fmt.Printf("Successfully updated IBC client: %s of network: %s\n", clientId, config.Chains[1].ID) 78 | } 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /models/weaveinit/weaveinit_test.go: -------------------------------------------------------------------------------- 1 | package weaveinit 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetWeaveInitOptions(t *testing.T) { 10 | options := GetWeaveInitOptions() 11 | 12 | assert.Equal(t, 4, len(options)) 13 | // order of options is important 14 | assert.Equal(t, RunL1NodeOption, options[0]) 15 | assert.Equal(t, LaunchNewRollupOption, options[1]) 16 | assert.Equal(t, RunOPBotsOption, options[2]) 17 | assert.Equal(t, RunRelayerOption, options[3]) 18 | } 19 | -------------------------------------------------------------------------------- /registry/constants.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | CelestiaTestnet ChainType = iota 9 | CelestiaMainnet 10 | InitiaL1Testnet 11 | InitiaL1Mainnet 12 | ) 13 | 14 | const ( 15 | InitiaRegistryEndpoint string = "https://raw.githubusercontent.com/initia-labs/initia-registry/refs/heads/main/%s/chain.json" 16 | CelestiaRegistryEndpoint string = "https://raw.githubusercontent.com/cosmos/chain-registry/refs/heads/master/%s/chain.json" 17 | OPInitBotsSpecEndpoint string = "https://raw.githubusercontent.com/initia-labs/opinit-bots/refs/heads/main/spec_version.json" 18 | 19 | InitiaTestnetRegistryAPI string = "https://registry.testnet.initia.xyz/chains.json" 20 | InitiaMainnetRegistryAPI string = "https://registry.initia.xyz/chains.json" 21 | InitiaL1PrettyName string = "Initia" 22 | 23 | InitiaTestnetGraphQLAPI string = "https://graphql.testnet.initia.xyz/v1/graphql" 24 | InitiaMainnetGraphQLAPI string = "https://graphql.initia.xyz/v1/graphql" 25 | ) 26 | 27 | var ( 28 | ChainTypeToEndpoint = map[ChainType]string{ 29 | CelestiaTestnet: CelestiaRegistryEndpoint, 30 | CelestiaMainnet: CelestiaRegistryEndpoint, 31 | InitiaL1Testnet: InitiaRegistryEndpoint, 32 | InitiaL1Mainnet: InitiaRegistryEndpoint, 33 | } 34 | ChainTypeToEndpointSlug = map[ChainType]string{ 35 | CelestiaTestnet: "testnets/celestiatestnet3", 36 | CelestiaMainnet: "celestia", 37 | InitiaL1Testnet: "testnets/initia", 38 | InitiaL1Mainnet: "mainnets/initia", 39 | } 40 | ChainTypeToInitiaRegistryAPI = map[ChainType]string{ 41 | InitiaL1Testnet: InitiaTestnetRegistryAPI, 42 | InitiaL1Mainnet: InitiaMainnetRegistryAPI, 43 | } 44 | ChainTypeToInitiaGraphQLAPI = map[ChainType]string{ 45 | InitiaL1Testnet: InitiaTestnetGraphQLAPI, 46 | InitiaL1Mainnet: InitiaMainnetGraphQLAPI, 47 | } 48 | ) 49 | 50 | func GetRegistryEndpoint(chainType ChainType) string { 51 | return fmt.Sprintf(ChainTypeToEndpoint[chainType], ChainTypeToEndpointSlug[chainType]) 52 | } 53 | -------------------------------------------------------------------------------- /registry/types.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | type ChainType int 4 | 5 | func (ct ChainType) String() string { 6 | switch ct { 7 | case CelestiaTestnet: 8 | return "Celestia Testnet" 9 | case CelestiaMainnet: 10 | return "Celestia Mainnet" 11 | case InitiaL1Testnet: 12 | return "Initia L1 Testnet" 13 | case InitiaL1Mainnet: 14 | return "Initia L1 Mainnet" 15 | default: 16 | return "Unknown" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "runtime" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | type Service interface { 13 | Create(binaryVersion, appHome string) error 14 | Log(n int) error 15 | Start(optionalArgs ...string) error 16 | Stop() error 17 | Restart() error 18 | PruneLogs() error 19 | 20 | GetServiceFile() (string, error) 21 | GetServiceBinaryAndHome() (string, string, error) 22 | } 23 | 24 | func NewService(commandName CommandName) (Service, error) { 25 | switch runtime.GOOS { 26 | case "linux": 27 | return NewSystemd(commandName), nil 28 | case "darwin": 29 | return NewLaunchd(commandName), nil 30 | default: 31 | return nil, fmt.Errorf("unsupported OS: %s", runtime.GOOS) 32 | } 33 | } 34 | 35 | func NonDetachStart(s Service, optionalArgs ...string) error { 36 | signalChan := make(chan os.Signal, 1) 37 | signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) 38 | defer signal.Stop(signalChan) 39 | 40 | go func() { 41 | err := s.Start(optionalArgs...) 42 | if err != nil { 43 | _ = s.Stop() 44 | panic(err) 45 | } 46 | time.Sleep(1 * time.Second) 47 | err = s.Log(100) 48 | if err != nil { 49 | _ = s.Stop() 50 | panic(err) 51 | } 52 | }() 53 | 54 | <-signalChan 55 | return s.Stop() 56 | } 57 | -------------------------------------------------------------------------------- /service/types.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "fmt" 4 | 5 | type CommandName string 6 | 7 | const ( 8 | UpgradableInitia CommandName = "upgradable_initia" 9 | NonUpgradableInitia CommandName = "non_upgradable_initia" 10 | Minitia CommandName = "minitia" 11 | OPinitExecutor CommandName = "executor" 12 | OPinitChallenger CommandName = "challenger" 13 | Relayer CommandName = "relayer" 14 | ) 15 | 16 | func (cmd CommandName) GetPrettyName() (string, error) { 17 | switch cmd { 18 | case UpgradableInitia, NonUpgradableInitia: 19 | return "initia", nil 20 | case Minitia: 21 | return "rollup", nil 22 | case OPinitExecutor, OPinitChallenger: 23 | return "opinit", nil 24 | case Relayer: 25 | return "relayer", nil 26 | default: 27 | return "", fmt.Errorf("unsupported command %s", cmd) 28 | } 29 | } 30 | 31 | func (cmd CommandName) GetInitCommand() (string, error) { 32 | switch cmd { 33 | case UpgradableInitia, NonUpgradableInitia: 34 | return "initia init", nil 35 | case Minitia: 36 | return "rollup launch", nil 37 | case OPinitExecutor, OPinitChallenger: 38 | return "opinit init", nil 39 | case Relayer: 40 | return "relayer init", nil 41 | default: 42 | return "", fmt.Errorf("unsupported command %s", cmd) 43 | } 44 | } 45 | 46 | func (cmd CommandName) GetBinaryName() (string, error) { 47 | switch cmd { 48 | case UpgradableInitia, NonUpgradableInitia: 49 | return "cosmovisor", nil 50 | case Minitia: 51 | return "minitiad", nil 52 | case OPinitExecutor, OPinitChallenger: 53 | return "opinitd", nil 54 | case Relayer: 55 | return "hermes", nil 56 | default: 57 | return "", fmt.Errorf("unsupported command: %v", cmd) 58 | } 59 | } 60 | 61 | func (cmd CommandName) GetServiceSlug() (string, error) { 62 | switch cmd { 63 | case UpgradableInitia: 64 | return "cosmovisor", nil 65 | case NonUpgradableInitia: 66 | return "cosmovisor", nil 67 | case Minitia: 68 | return "minitiad", nil 69 | case OPinitExecutor: 70 | return "opinitd.executor", nil 71 | case OPinitChallenger: 72 | return "opinitd.challenger", nil 73 | case Relayer: 74 | return "hermes", nil 75 | default: 76 | return "", fmt.Errorf("unsupported command: %v", cmd) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /styles/color.go: -------------------------------------------------------------------------------- 1 | package styles 2 | 3 | type HexColor string 4 | 5 | const ( 6 | White HexColor = "#F5F5F5" 7 | Ivory HexColor = "#CBCBCB" 8 | Cyan HexColor = "#27D8FF" 9 | DarkCyan HexColor = "#178299" 10 | Green HexColor = "#B0EE5F" 11 | LightGray HexColor = "#AAAAAA" 12 | Gray HexColor = "#808080" 13 | Black HexColor = "#000000" 14 | Yellow HexColor = "#FFBE5E" 15 | ) 16 | -------------------------------------------------------------------------------- /styles/wordwrap.go: -------------------------------------------------------------------------------- 1 | package styles 2 | 3 | import ( 4 | "github.com/muesli/reflow/wordwrap" 5 | ) 6 | 7 | var ( 8 | defaultBreakpoints = []rune{'-', ','} 9 | defaultNewline = []rune{'\n'} 10 | ) 11 | 12 | func NewWordwrapWriter(limit int) *wordwrap.WordWrap { 13 | return &wordwrap.WordWrap{ 14 | Limit: limit, 15 | Breakpoints: defaultBreakpoints, 16 | Newline: defaultNewline, 17 | KeepNewlines: true, 18 | } 19 | } 20 | 21 | func WordwrapBytes(b []byte, limit int) []byte { 22 | f := NewWordwrapWriter(limit) 23 | _, _ = f.Write(b) 24 | _ = f.Close() 25 | 26 | return f.Bytes() 27 | } 28 | 29 | func WordwrapString(s string, limit int) string { 30 | return string(WordwrapBytes([]byte(s), limit)) 31 | } 32 | -------------------------------------------------------------------------------- /testutil/alias.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "time" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | ) 8 | 9 | var ( 10 | PressEnter = InputStep{Msg: tea.KeyMsg{Type: tea.KeyEnter}} 11 | PressSpace = InputStep{Msg: tea.KeyMsg{Type: tea.KeySpace}} 12 | PressTab = InputStep{Msg: tea.KeyMsg{Type: tea.KeyTab}} 13 | PressUp = InputStep{Msg: tea.KeyMsg{Type: tea.KeyUp}} 14 | PressDown = InputStep{Msg: tea.KeyMsg{Type: tea.KeyDown}} 15 | 16 | WaitFetching = WaitStep{Check: func() bool { 17 | time.Sleep(5 * time.Second) 18 | return true 19 | }} 20 | ) 21 | 22 | func TypeText(text string) InputStep { 23 | return InputStep{Msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(text)}} 24 | } 25 | 26 | // WaitFor receives waitCondition as a parameter, which should return true if the wait should be over. 27 | func WaitFor(waitCondition func() bool) WaitStep { 28 | return WaitStep{Check: waitCondition} 29 | } 30 | -------------------------------------------------------------------------------- /tooltip/common.go: -------------------------------------------------------------------------------- 1 | package tooltip 2 | 3 | import "github.com/initia-labs/weave/ui" 4 | 5 | var ( 6 | MonikerTooltip = ui.NewTooltip("Moniker", "A unique identifier among nodes in a network.", "", []string{}, []string{}, []string{}) 7 | ) 8 | -------------------------------------------------------------------------------- /tooltip/description.go: -------------------------------------------------------------------------------- 1 | package tooltip 2 | 3 | import "fmt" 4 | 5 | func ChainIDDescription(networkType string) string { 6 | return fmt.Sprintf("Identifier for the %s network.", networkType) 7 | } 8 | 9 | func RPCEndpointDescription(networkType string) string { 10 | return fmt.Sprintf("The network address and port that the %s RPC node will listen to. This endpoint is used by the rollup bots to communicate with the %s network and for users to submit transactions.", networkType, networkType) 11 | } 12 | 13 | func GRPCEndpointDescription(networkType string) string { 14 | return fmt.Sprintf("The network address and port that an %s GRPC node will listen to. This allows the rollup bots to query additional data from the %s network.", networkType, networkType) 15 | } 16 | 17 | func WebSocketEndpointDescription(networkType string) string { 18 | return fmt.Sprintf("The network address and port that an %s WebSocket node will listen to. This allows the rollup bots to listen to events from the %s network.", networkType, networkType) 19 | } 20 | 21 | func GasDenomDescription(networkType string) string { 22 | return fmt.Sprintf("The gas token denom to be used for submitting transactions to the %s node.", networkType) 23 | } 24 | 25 | func GasPriceDescription(networkType string) string { 26 | return fmt.Sprintf("The gas price to be used for submitting transactions to the %s node. This value should be set to the minimum gas price for the %s node.", networkType, networkType) 27 | } 28 | 29 | func MinGasPriceDescription(networkType string) string { 30 | return fmt.Sprintf("The minimum gas price for transactions submitted on the %s network. Any transactions submitted to the %s network with a lower gas price will be rejected.", networkType, networkType) 31 | } 32 | -------------------------------------------------------------------------------- /tooltip/gas_station.go: -------------------------------------------------------------------------------- 1 | package tooltip 2 | 3 | import "github.com/initia-labs/weave/ui" 4 | 5 | var ( 6 | GasStationMnemonicTooltip = ui.NewTooltip("Gas station account", "Gas station account is the account from which Weave will use to fund necessary system accounts and bots to simplify the setup process.", "", []string{}, []string{}, []string{}) 7 | ) 8 | -------------------------------------------------------------------------------- /tooltip/l1.go: -------------------------------------------------------------------------------- 1 | package tooltip 2 | 3 | import ( 4 | "github.com/initia-labs/weave/ui" 5 | ) 6 | 7 | var ( 8 | L1ChainIdTooltip = ui.NewTooltip("L1 chain ID", ChainIDDescription("L1"), "", []string{}, []string{}, []string{}) 9 | L1RPCEndpointTooltip = ui.NewTooltip("L1 RPC endpoint", RPCEndpointDescription("L1"), "", []string{}, []string{}, []string{}) 10 | L1NetworkSelectTooltip = ui.NewTooltip("Network to connect to", "Available options are Testnet (initiation-2) and local (no network participation).", "", []string{}, []string{}, []string{}) 11 | L1InitiadVersionTooltip = ui.NewTooltip("Initiad version", "Initiad version refers to the version of the Initia daemon CLI used to run the Initia L1 node.", "", []string{}, []string{}, []string{}) 12 | L1ExistingAppTooltip = ui.NewTooltip("app.toml / config.toml", "The app.toml file contains the node's configuration, including transaction limits, gas price, and state pruning strategy.\n\nThe config.toml file includes core network and protocol settings for the node, such as peers to connect to, timeouts, and consensus configurations.", "", []string{"app.toml", "config.toml"}, []string{}, []string{}) 13 | L1MinGasPriceTooltip = ui.NewTooltip("Minimum Gas Price", MinGasPriceDescription("L1"), "", []string{}, []string{}, []string{}) 14 | 15 | // Enable Features Tooltips 16 | L1EnableRESTTooltip = ui.NewTooltip("REST", "Enabling this option allows REST API calls to query data and submit transactions to your node. (Recommended)", "", []string{}, []string{}, []string{}) 17 | L1EnablegRPCTooltip = ui.NewTooltip("gRPC", "Enabling this option allows gRPC calls to your node. (Recommended)", "", []string{}, []string{}, []string{}) 18 | 19 | L1SeedsTooltip = ui.NewTooltip("Seeds", "Enter a list of known node addresses (@:) to be used as initial contact points to discover other nodes. If you don't need your node to participate in the network (e.g. local development), seeds are not required.", "", []string{}, []string{}, []string{}) 20 | L1PersistentPeersTooltip = ui.NewTooltip("Persistent Peers", "Enter a list of known node addresses (@:) to maintain constant connections to. This is particularly useful for fast syncing if you have access to a trusted, reliable node.", "", []string{}, []string{}, []string{}) 21 | L1GenesisEndpointTooltip = ui.NewTooltip("genesis.json", "Provide the URL or network address where the genesis.json file can be accessed. This file contains the initial state and configuration of the blockchain network, which is essential for new nodes to sync and participate in the network correctly.", "", []string{}, []string{}, []string{}) 22 | 23 | // Sync Method Tooltips 24 | L1SnapshotSyncTooltip = ui.NewTooltip("Snapshot", "Downloads a recent snapshot of the chain state to quickly catch up without replaying the entire chain history. This is faster than full state sync but relies on a trusted source for the snapshot.\n\nThis is necessary to participate in an existing network.", "", []string{}, []string{}, []string{}) 25 | L1StateSyncTooltip = ui.NewTooltip("State Sync", "Retrieves the latest blockchain state from peers without downloading the entire history. It's faster than syncing from genesis but may miss some historical data.\n\nThis is necessary to participate in an existing network.", "", []string{}, []string{}, []string{}) 26 | L1NoSyncTooltip = ui.NewTooltip("No Sync", "The node will not download data from any sources to replace the existing (if any). The node will start syncing from its current state, potentially genesis state if this is the first run.\n\nThis is best for local development / testing.", "", []string{}, []string{}, []string{}) 27 | 28 | // Cosmovisor Tooltips 29 | L1CosmovisorAutoUpgradeEnableTooltip = ui.NewTooltip("Enable", "Enable automatic downloading of new binaries and upgrades via Cosmovisor. \nSee more: https://docs.initia.xyz/run-initia-node/automating-software-updates-with-cosmovisor", "", []string{}, []string{}, []string{}) 30 | L1CosmovisorAutoUpgradeDisableTooltip = ui.NewTooltip("Disable", "Disable automatic downloading of new binaries and upgrades via Cosmovisor. You will need to manually upgrade the binaries and restart the node to apply the upgrades.", "", []string{}, []string{}, []string{}) 31 | 32 | L1DefaultPruningStrategiesTooltip = ui.NewTooltip("Default", "Keep the last 100 states in addition to every 500th state, and prune on 10-block intervals. This configuration is safe to use on all types of nodes, especially validator nodes.", "", []string{}, []string{"recommended"}, []string{}) 33 | L1NothingPruningStrategiesTooltip = ui.NewTooltip("Nothing", "Disable node state pruning, essentially making your node an archival node. This mode consumes the highest disk usage.", "", []string{}, []string{"disable"}, []string{}) 34 | L1EverythingPruningStrategiesTooltip = ui.NewTooltip("Everything", "Keep the current state and also prune on 10 blocks intervals. This settings is useful for nodes such as seed/sentry nodes, as long as they are not used to query RPC/REST API requests. This mode is not recommended when running validator nodes.", "", []string{}, []string{"not recommended "}, []string{}) 35 | ) 36 | -------------------------------------------------------------------------------- /tooltip/opinit.go: -------------------------------------------------------------------------------- 1 | package tooltip 2 | 3 | import "github.com/initia-labs/weave/ui" 4 | 5 | var ( 6 | ListenAddressTooltip = ui.NewTooltip("Listen address", "The network address and port where the bot listens for incoming queries regarding deposits, withdrawals, and challenges.", "", []string{}, []string{}, []string{}) 7 | InitiaDALayerTooltip = ui.NewTooltip("Initia", "Ideal for projects that require close integration within the Initia network, offering streamlined communication and data handling within the Initia ecosystem.", "", []string{}, []string{}, []string{}) 8 | CelestiaMainnetDALayerTooltip = ui.NewTooltip("Celestia Mainnet", "Suitable for production environments that need reliable and secure data availability with Celestia's decentralized architecture, ensuring robust support for live applications.", "", []string{}, []string{}, []string{}) 9 | CelestiaTestnetDALayerTooltip = ui.NewTooltip("Celestia Testnet", "Best for testing purposes, allowing you to validate functionality and performance in a non-production setting before deploying to a mainnet environment.", "", []string{}, []string{}, []string{}) 10 | L1StartHeightTooltip = ui.NewTooltip("L1 start height", "The L1 block height from which the bot should start processing. If no deposit has bridged to the rollup yet, the block height should be the one at which the bridge creation transaction occurred. Otherwise, it should be the block height of the most recent deposit on L1.", "", []string{}, []string{}, []string{}) 11 | ) 12 | -------------------------------------------------------------------------------- /tooltip/relayer.go: -------------------------------------------------------------------------------- 1 | package tooltip 2 | 3 | import ( 4 | "github.com/initia-labs/weave/ui" 5 | ) 6 | 7 | var ( 8 | // Rollup select 9 | RelayerRollupSelectLocalTooltip = ui.NewTooltip("Local rollup", "Run a relayer for the rollup that you just launched locally. Using artifacts from .minitia/artifacts to setup the relayer.", "", []string{}, []string{}, []string{}) 10 | RelayerRollupSelectWhitelistedTooltip = ui.NewTooltip("Whitelisted rollup", "Run a relayer for any live rollup in https://github.com/initia-labs/initia-registry.", "", []string{}, []string{}, []string{}) 11 | RelayerRollupSelectManualTooltip = ui.NewTooltip("Manual Relayer Setup", "Setup the relayer manually by providing the L1 and rollup chain IDs, RPC endpoints, GRPC endpoints, and more.", "", []string{}, []string{}, []string{}) 12 | 13 | // L1 network select 14 | RelayerL1NetworkSelectTooltip = ui.NewTooltip("L1 Network to relay messages", "Testnet (initiation-2) is the only supported network for now.", "", []string{}, []string{}, []string{}) 15 | 16 | // Rollup LCD endpoint 17 | RelayerRollupLCDTooltip = ui.NewTooltip("Rollup LCD endpoint", "LCD endpoint to your rollup node server. By providing this, relayer will be able to fetch the IBC channels and ports from the rollup node server.", "", []string{}, []string{}, []string{}) 18 | 19 | // IBC channels setup 20 | RelayerIBCMinimalSetupTooltip = ui.NewTooltip("Minimal setup", "Subscribe to only `transfer` and `nft-transfer` IBC Channels created when launching the rollup with `minitiad launch` or `weave rollup launch`. This is recommended for new networks or local testing.", "", []string{}, []string{}, []string{}) 21 | RelayerIBCFillFromLCDTooltip = ui.NewTooltip("Get all available IBC Channels", "By filling in the rollup LCD endpoint, Weave will be able to detect all available IBC Channels and show you all the IBC Channel pairs.", "", []string{}, []string{}, []string{}) 22 | RelayerIBCManualSetupTooltip = ui.NewTooltip("Manual setup", "Setup each IBC Channel manually by specifying the port ID and channel ID.", "", []string{}, []string{}, []string{}) 23 | RelayerIBCChannelsTooltip = ui.NewTooltip("IBC Channels", "Relayer will listen to the selected channels (and ports) and relay messages between L1 and rollup network. Relay all option is recommended if you don't want to miss any messages.\n\nRefer to https://ibc.cosmos.network/main/ibc/overview.html#channels for more information.", "", []string{}, []string{}, []string{}) 24 | RelayerL1IBCPortIDTooltip = ui.NewTooltip("Port ID on L1", "Port Identifier for the IBC channel on L1 that the relayer should relay messages. Refer to https://ibc.cosmos.network/main/ibc/overview/#ports for more information.", "", []string{}, []string{}, []string{}) 25 | RelayerL1IBCChannelIDTooltip = ui.NewTooltip("Channel ID on L1", "Channel Identifier for the IBC channel on L1 that the relayer should relay messages. Refer to https://ibc.cosmos.network/main/ibc/overview/#channels for more information.", "", []string{}, []string{}, []string{}) 26 | RelayerRollupIBCPortIDTooltip = ui.NewTooltip("Port ID on rollup", "Port Identifier for the IBC channel on rollup that the relayer should relay messages. Refer to https://ibc.cosmos.network/main/ibc/overview/#ports for more information.", "", []string{}, []string{}, []string{}) 27 | RelayerRollupIBCChannelIDTooltip = ui.NewTooltip("Channel ID on rollup", "Channel Identifier for the IBC channel on rollup that the relayer should relay messages. Refer to https://ibc.cosmos.network/main/ibc/overview/#channels for more information.", "", []string{}, []string{}, []string{}) 28 | 29 | // Relayer key 30 | RelayerChallengerKeyTooltip = ui.NewTooltip("Relayer with Challenger Key", "This is recommended because challenger account is exempted from gas fees on the rollup and able to stop other relayers from relaying when it detects a malicious message coming from it. If you want to setup relayer with a separate key, select No.", "", []string{}, []string{}, []string{}) 31 | RelayerL1KeySelectTooltip = ui.NewTooltip("L1 Relayer Key", "The key/account that the relayer will use to interact with the L1 network.", "", []string{}, []string{}, []string{}) 32 | RelayerRollupKeySelectTooltip = ui.NewTooltip("Rollup Relayer Key", "The key/account that the relayer will use to interact with the rollup network.", "", []string{}, []string{}, []string{}) 33 | 34 | // Funding amount select 35 | RelayerFundingAmountSelectTooltip = ui.NewTooltip("Funding the relayer accounts", "Relayer accounts on both the Initia L1 and the rollup requires gas token funding to ensure the relayer can function properly.", "", []string{}, []string{}, []string{}) 36 | ) 37 | -------------------------------------------------------------------------------- /types/minitia_config.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type MinitiaConfig struct { 4 | L1Config *L1Config `json:"l1_config,omitempty"` 5 | L2Config *L2Config `json:"l2_config,omitempty"` 6 | OpBridge *OpBridge `json:"op_bridge,omitempty"` 7 | SystemKeys *SystemKeys `json:"system_keys,omitempty"` 8 | GenesisAccounts *GenesisAccounts `json:"genesis_accounts,omitempty"` 9 | } 10 | 11 | type L1Config struct { 12 | ChainID string `json:"chain_id,omitempty"` 13 | RpcUrl string `json:"rpc_url,omitempty"` 14 | GasPrices string `json:"gas_prices,omitempty"` 15 | } 16 | 17 | type L2Config struct { 18 | ChainID string `json:"chain_id,omitempty"` 19 | Denom string `json:"denom,omitempty"` 20 | Moniker string `json:"moniker,omitempty"` 21 | BridgeID uint64 `json:"bridge_id,omitempty"` 22 | } 23 | 24 | type OpBridge struct { 25 | OutputSubmissionInterval string `json:"output_submission_interval,omitempty"` 26 | OutputFinalizationPeriod string `json:"output_finalization_period,omitempty"` 27 | OutputSubmissionStartHeight uint64 `json:"output_submission_start_height,omitempty"` 28 | BatchSubmissionTarget string `json:"batch_submission_target"` 29 | EnableOracle bool `json:"enable_oracle"` 30 | } 31 | 32 | type SystemAccount struct { 33 | L1Address string `json:"l1_address,omitempty"` 34 | L2Address string `json:"l2_address,omitempty"` 35 | DAAddress string `json:"da_address,omitempty"` 36 | Mnemonic string `json:"mnemonic,omitempty"` 37 | } 38 | 39 | func NewSystemAccount(mnemonic, addresses string) *SystemAccount { 40 | account := &SystemAccount{ 41 | Mnemonic: mnemonic, 42 | L1Address: addresses, 43 | L2Address: addresses, 44 | } 45 | 46 | return account 47 | } 48 | 49 | func NewBatchSubmitterAccount(mnemonic, address string) *SystemAccount { 50 | account := &SystemAccount{ 51 | DAAddress: address, 52 | Mnemonic: mnemonic, 53 | } 54 | 55 | return account 56 | } 57 | 58 | type GenesisAccount struct { 59 | Address string `json:"address,omitempty"` 60 | Coins string `json:"coins,omitempty"` 61 | } 62 | 63 | type GenesisAccounts []GenesisAccount 64 | 65 | type SystemKeys struct { 66 | Validator *SystemAccount `json:"validator,omitempty"` 67 | BridgeExecutor *SystemAccount `json:"bridge_executor,omitempty"` 68 | OutputSubmitter *SystemAccount `json:"output_submitter,omitempty"` 69 | BatchSubmitter *SystemAccount `json:"batch_submitter,omitempty"` 70 | Challenger *SystemAccount `json:"challenger,omitempty"` 71 | } 72 | 73 | // Artifacts define the structure for the JSON data 74 | type Artifacts struct { 75 | BridgeID string `json:"BRIDGE_ID"` 76 | ExecutorL1MonitorHeight string `json:"EXECUTOR_L1_MONITOR_HEIGHT"` 77 | ExecutorL2MonitorHeight string `json:"EXECUTOR_L2_MONITOR_HEIGHT"` 78 | } 79 | 80 | // Clone returns a deep copy of MinitiaConfig. 81 | // Returns nil if the receiver is nil. 82 | func (m *MinitiaConfig) Clone() *MinitiaConfig { 83 | if m == nil { 84 | return nil 85 | } 86 | 87 | clone := &MinitiaConfig{ 88 | L1Config: nil, 89 | L2Config: nil, 90 | OpBridge: nil, 91 | SystemKeys: nil, 92 | GenesisAccounts: nil, 93 | } 94 | 95 | if m.L1Config != nil { 96 | clone.L1Config = &L1Config{ 97 | ChainID: m.L1Config.ChainID, 98 | RpcUrl: m.L1Config.RpcUrl, 99 | GasPrices: m.L1Config.GasPrices, 100 | } 101 | } 102 | // Similar deep copy for other fields... 103 | if m.L2Config != nil { 104 | clone.L2Config = &L2Config{ 105 | ChainID: m.L2Config.ChainID, 106 | Denom: m.L2Config.Denom, 107 | Moniker: m.L2Config.Moniker, 108 | BridgeID: m.L2Config.BridgeID, 109 | } 110 | } 111 | 112 | if m.OpBridge != nil { 113 | clone.OpBridge = &OpBridge{ 114 | OutputSubmissionInterval: m.OpBridge.OutputSubmissionInterval, 115 | OutputFinalizationPeriod: m.OpBridge.OutputFinalizationPeriod, 116 | OutputSubmissionStartHeight: m.OpBridge.OutputSubmissionStartHeight, 117 | BatchSubmissionTarget: m.OpBridge.BatchSubmissionTarget, 118 | EnableOracle: m.OpBridge.EnableOracle, 119 | } 120 | } 121 | 122 | if m.SystemKeys != nil { 123 | clone.SystemKeys = &SystemKeys{ 124 | Validator: cloneSystemAccount(m.SystemKeys.Validator), 125 | BridgeExecutor: cloneSystemAccount(m.SystemKeys.BridgeExecutor), 126 | OutputSubmitter: cloneSystemAccount(m.SystemKeys.OutputSubmitter), 127 | BatchSubmitter: cloneSystemAccount(m.SystemKeys.BatchSubmitter), 128 | Challenger: cloneSystemAccount(m.SystemKeys.Challenger), 129 | } 130 | } 131 | if m.GenesisAccounts != nil { 132 | accs := make(GenesisAccounts, len(*m.GenesisAccounts)) 133 | copy(accs, *m.GenesisAccounts) 134 | clone.GenesisAccounts = &accs 135 | } 136 | return clone 137 | } 138 | 139 | func cloneSystemAccount(acc *SystemAccount) *SystemAccount { 140 | if acc == nil { 141 | return nil 142 | } 143 | return &SystemAccount{ 144 | L1Address: acc.L1Address, 145 | L2Address: acc.L2Address, 146 | DAAddress: acc.DAAddress, 147 | Mnemonic: acc.Mnemonic, 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /types/minitia_config_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // Test the creation of a new SystemAccount using NewSystemAccount function 10 | func TestNewSystemAccount(t *testing.T) { 11 | // Create a new system account with both L1 and L2 balances 12 | account := NewSystemAccount("some-mnemonic", "l1-address") 13 | 14 | // Validate account has the correct L1 and L2 addresses 15 | assert.Equal(t, "l1-address", account.L1Address, "Expected L1 address to be 'l1-address'") 16 | assert.Equal(t, "some-mnemonic", account.Mnemonic, "Expected mnemonic to be 'some-mnemonic'") 17 | assert.Equal(t, "l1-address", account.L2Address, "Expected L2 address to be 'l1-address'") 18 | 19 | // Create a system account with only L1 balance 20 | accountL1 := NewSystemAccount("mnemonic-l1", "l1-only-address") 21 | assert.Equal(t, "l1-only-address", accountL1.L1Address, "Expected L1 address to be 'l1-only-address'") 22 | assert.Equal(t, "l1-only-address", accountL1.L2Address, "Expected L2 address to be empty") 23 | 24 | // Create a system account with only L2 balance 25 | accountL2 := NewSystemAccount("mnemonic-l2", "l2-only-address") 26 | assert.Equal(t, "l2-only-address", accountL2.L2Address, "Expected L2 address to be 'l2-only-address'") 27 | assert.Equal(t, "l2-only-address", accountL2.L1Address, "Expected L1 address to be empty") 28 | } 29 | 30 | // Test the behavior of the GenesisAccounts struct 31 | func TestGenesisAccounts(t *testing.T) { 32 | // Create a slice of GenesisAccounts 33 | accounts := GenesisAccounts{ 34 | {Address: "address1", Coins: "100coins"}, 35 | {Address: "address2", Coins: "200coins"}, 36 | } 37 | 38 | // Validate the genesis accounts 39 | assert.Equal(t, 2, len(accounts), "Expected 2 genesis accounts") 40 | assert.Equal(t, "address1", accounts[0].Address, "Expected first account address to be 'address1'") 41 | assert.Equal(t, "100coins", accounts[0].Coins, "Expected first account coins to be '100coins'") 42 | assert.Equal(t, "address2", accounts[1].Address, "Expected second account address to be 'address2'") 43 | assert.Equal(t, "200coins", accounts[1].Coins, "Expected second account coins to be '200coins'") 44 | } 45 | -------------------------------------------------------------------------------- /types/relayer.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | type BatchInfo struct { 10 | Submitter string `json:"submitter"` 11 | ChainType string `json:"chain_type"` 12 | } 13 | 14 | type BridgeConfig struct { 15 | Challenger string `json:"challenger"` 16 | Proposer string `json:"proposer"` 17 | BatchInfo BatchInfo `json:"batch_info"` 18 | SubmissionInterval string `json:"submission_interval"` 19 | FinalizationPeriod string `json:"finalization_period"` 20 | SubmissionStartHeight string `json:"submission_start_height"` 21 | OracleEnabled bool `json:"oracle_enabled"` 22 | Metadata string `json:"metadata"` 23 | } 24 | 25 | type Bridge struct { 26 | BridgeID string `json:"bridge_id"` 27 | BridgeAddr string `json:"bridge_addr"` 28 | BridgeConfig BridgeConfig `json:"bridge_config"` 29 | } 30 | 31 | type Channel struct { 32 | PortID string `json:"port_id"` 33 | ChannelID string `json:"channel_id"` 34 | } 35 | 36 | type Metadata struct { 37 | PermChannels []Channel `json:"perm_channels"` 38 | } 39 | 40 | type IBCChannelPair struct { 41 | L1 Channel 42 | L2 Channel 43 | } 44 | 45 | // MinimalIBCChannelResponse define a minimal struct to parse just the counterparty field 46 | type MinimalIBCChannelResponse struct { 47 | Channel struct { 48 | Counterparty Channel `json:"counterparty"` 49 | } `json:"channel"` 50 | } 51 | 52 | type ChannelInfo struct { 53 | PortID string `json:"port_id"` 54 | ChannelID string `json:"channel_id"` 55 | Counterparty Channel `json:"counterparty"` 56 | } 57 | 58 | type ChannelsResponse struct { 59 | Channels []ChannelInfo `json:"channels"` 60 | } 61 | 62 | func DecodeBridgeMetadata(base64Str string) (Metadata, error) { 63 | // Decode the Base64 string 64 | jsonData, err := base64.StdEncoding.DecodeString(base64Str) 65 | if err != nil { 66 | return Metadata{}, err 67 | } 68 | 69 | // Struct to hold the decoded JSON 70 | var metadata Metadata 71 | 72 | // Unmarshal the JSON into the struct 73 | err = json.Unmarshal(jsonData, &metadata) 74 | if err != nil { 75 | fmt.Printf("Error decoding JSON: %v %s\n", err, base64Str) 76 | return Metadata{}, err 77 | } 78 | 79 | return metadata, nil 80 | } 81 | -------------------------------------------------------------------------------- /types/state.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type WeaveState struct { 4 | PreviousResponse []string 5 | } 6 | 7 | // NewWeaveState initializes a new WeaveState with an empty PreviousResponse slice. 8 | func NewWeaveState() WeaveState { 9 | return WeaveState{ 10 | PreviousResponse: make([]string, 0), 11 | } 12 | } 13 | 14 | // Clone creates a deep copy of WeaveState, duplicating the PreviousResponse slice. 15 | func (w WeaveState) Clone() WeaveState { 16 | // Create a copy of the PreviousResponse slice 17 | clonedResponses := make([]string, len(w.PreviousResponse)) 18 | copy(clonedResponses, w.PreviousResponse) 19 | 20 | return WeaveState{ 21 | PreviousResponse: clonedResponses, 22 | } 23 | } 24 | 25 | // Render concatenates all responses into a single string. 26 | func (w *WeaveState) Render() string { 27 | render := "" 28 | for _, r := range w.PreviousResponse { 29 | render += r 30 | } 31 | return render 32 | } 33 | 34 | // PopPreviousResponse removes the last response in the PreviousResponse slice. 35 | func (w *WeaveState) PopPreviousResponse() { 36 | l := len(w.PreviousResponse) 37 | if l == 0 { 38 | return 39 | } 40 | w.PreviousResponse = w.PreviousResponse[:l-1] 41 | } 42 | 43 | // PushPreviousResponse adds a response to the end of the PreviousResponse slice. 44 | func (w *WeaveState) PushPreviousResponse(s string) { 45 | w.PreviousResponse = append(w.PreviousResponse, s) 46 | } 47 | 48 | // PopPreviousResponseAtIndex removes a response at a specific index. Use with care. 49 | func (w *WeaveState) PopPreviousResponseAtIndex(index int) { 50 | l := len(w.PreviousResponse) 51 | if index < 0 || index >= l { 52 | return 53 | } 54 | w.PreviousResponse = append(w.PreviousResponse[:index], w.PreviousResponse[index+1:]...) 55 | } 56 | -------------------------------------------------------------------------------- /ubuntu-script.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | sudo apt install make 4 | sudo apt-get install lz4 5 | sudo snap install go --classic 6 | export GOPATH=$HOME/go 7 | export PATH=$PATH:$GOROOT/bin:$GOPATH/bin 8 | 9 | git clone https://github.com/initia-labs/weave.git 10 | cd weave 11 | make install 12 | -------------------------------------------------------------------------------- /ui/checkbox_test.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/initia-labs/weave/analytics" 11 | ) 12 | 13 | func TestMain(m *testing.M) { 14 | analytics.Client = &analytics.NoOpClient{} 15 | exitCode := m.Run() 16 | os.Exit(exitCode) 17 | } 18 | 19 | func TestCheckBoxNavigationAndSelection(t *testing.T) { 20 | options := []string{"Option 1", "Option 2", "Option 3"} 21 | cb := NewCheckBox(options) 22 | 23 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")}) 24 | assert.Equal(t, cb.Cursor, 1) 25 | 26 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")}) 27 | assert.Equal(t, cb.Cursor, 0) 28 | 29 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeySpace}) 30 | assert.Equal(t, cb.GetSelected(), []string{"Option 1"}) 31 | 32 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeySpace}) 33 | assert.Equal(t, cb.GetSelected(), []string{}) 34 | 35 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeySpace}) 36 | 37 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")}) 38 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeySpace}) 39 | assert.Equal(t, cb.GetSelected(), []string{"Option 1", "Option 2"}) 40 | 41 | _, _, entered := cb.Select(tea.KeyMsg{Type: tea.KeyEnter}) 42 | assert.True(t, entered) 43 | } 44 | 45 | func TestCheckBoxQuit(t *testing.T) { 46 | options := []string{"Option 1", "Option 2", "Option 3"} 47 | cb := NewCheckBox(options) 48 | 49 | _, cmd, _ := cb.Select(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")}) 50 | assert.NotNil(t, cmd) 51 | } 52 | 53 | func TestCheckBoxNavigationWrapping(t *testing.T) { 54 | options := []string{"Option 1", "Option 2", "Option 3"} 55 | cb := NewCheckBox(options) 56 | 57 | for i := 0; i < len(options)+1; i++ { 58 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")}) 59 | } 60 | assert.Equal(t, 1, cb.Cursor) 61 | 62 | cb.Cursor = 0 63 | for i := 0; i < len(options)+1; i++ { 64 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")}) 65 | } 66 | assert.Equal(t, len(options)-1, cb.Cursor) 67 | } 68 | 69 | func TestCheckBoxSimultaneousSelectionsAndDeselections(t *testing.T) { 70 | options := []string{"Option 1", "Option 2", "Option 3"} 71 | cb := NewCheckBox(options) 72 | 73 | for i := 0; i < len(options); i++ { 74 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("j")}) 75 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeySpace}) 76 | } 77 | 78 | expectedAllSelected := []string{"Option 1", "Option 2", "Option 3"} 79 | assert.ElementsMatch(t, expectedAllSelected, cb.GetSelected()) 80 | 81 | for i := 0; i < len(options); i++ { 82 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("k")}) 83 | cb, _, _ = cb.Select(tea.KeyMsg{Type: tea.KeySpace}) 84 | } 85 | 86 | var expectedNoneSelected []string 87 | assert.ElementsMatch(t, expectedNoneSelected, cb.GetSelected()) 88 | } 89 | -------------------------------------------------------------------------------- /ui/clickable.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | 9 | "github.com/initia-labs/weave/styles" 10 | ) 11 | 12 | type ClickableItem struct { 13 | displayText map[bool]string 14 | clicked bool 15 | handleFn func() error 16 | x int 17 | y int 18 | w map[bool]int 19 | } 20 | 21 | func NewClickableItem(displayText map[bool]string, handleFn func() error) *ClickableItem { 22 | w := map[bool]int{ 23 | true: len(displayText[true]), 24 | false: len(displayText[false]), 25 | } 26 | return &ClickableItem{ 27 | displayText: displayText, 28 | clicked: false, 29 | handleFn: handleFn, 30 | x: 0, 31 | y: 0, 32 | w: w, 33 | } 34 | } 35 | 36 | func (ci *ClickableItem) Update(msg tea.Msg) error { 37 | switch msg := msg.(type) { 38 | case tea.MouseMsg: 39 | if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft && 40 | msg.X >= ci.x && msg.X < ci.x+ci.w[ci.clicked] && 41 | msg.Y == ci.y { 42 | ci.clicked = !ci.clicked 43 | if ci.handleFn != nil { 44 | return ci.handleFn() 45 | } 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func (ci *ClickableItem) View() string { 52 | return ci.displayText[ci.clicked] 53 | } 54 | 55 | func (ci *ClickableItem) UpdatePosition(cleanText string) error { 56 | lines := strings.Split(cleanText, "\n") 57 | for y, line := range lines { 58 | if strings.Contains(line, ci.displayText[ci.clicked]) { 59 | ci.y = y 60 | ci.x = strings.Index(line, ci.displayText[ci.clicked]) 61 | return nil 62 | } 63 | } 64 | return fmt.Errorf("text '%s' not found in rendered lines", ci.displayText[ci.clicked]) 65 | } 66 | 67 | type Clickable struct { 68 | Items []*ClickableItem 69 | } 70 | 71 | func NewClickable(items ...*ClickableItem) *Clickable { 72 | return &Clickable{Items: items} 73 | } 74 | 75 | func (c *Clickable) Init() tea.Cmd { 76 | return tea.EnableMouseCellMotion 77 | } 78 | 79 | func (c *Clickable) ClickableUpdate(msg tea.Msg) error { 80 | for _, item := range c.Items { 81 | if err := item.Update(msg); err != nil { 82 | return err 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func (c *Clickable) ClickableView(index int) string { 89 | item := c.Items[index] 90 | return item.displayText[item.clicked] 91 | } 92 | 93 | func (c *Clickable) PostUpdate() tea.Cmd { 94 | return tea.DisableMouse 95 | } 96 | 97 | func (c *Clickable) ClickableUpdatePositions(text string) error { 98 | cleanText := styles.StripANSI(text) 99 | lines := strings.Split(cleanText, "\n") 100 | 101 | remainingItems := make(map[string][]*ClickableItem) 102 | for _, item := range c.Items { 103 | display := item.displayText[item.clicked] 104 | remainingItems[display] = append(remainingItems[display], item) 105 | } 106 | 107 | for y, line := range lines { 108 | for display, items := range remainingItems { 109 | startIndex := 0 110 | for { 111 | idx := strings.Index(line[startIndex:], display) 112 | if idx == -1 { 113 | break 114 | } 115 | if len(items) > 0 { 116 | item := items[0] 117 | item.x = startIndex + idx 118 | item.y = y 119 | 120 | remainingItems[display] = items[1:] 121 | items = remainingItems[display] 122 | } 123 | startIndex += idx + len(display) 124 | } 125 | } 126 | } 127 | 128 | for display, items := range remainingItems { 129 | if len(items) > 0 { 130 | return fmt.Errorf("text '%s' not found for all occurrences in rendered lines", display) 131 | } 132 | } 133 | 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /ui/downloader.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/charmbracelet/bubbles/progress" 8 | tea "github.com/charmbracelet/bubbletea" 9 | 10 | "github.com/initia-labs/weave/analytics" 11 | "github.com/initia-labs/weave/client" 12 | "github.com/initia-labs/weave/styles" 13 | ) 14 | 15 | type Downloader struct { 16 | progress progress.Model 17 | total int64 18 | current int64 19 | text string 20 | url string 21 | dest string 22 | done bool 23 | err error 24 | validateFn func(string) error 25 | } 26 | 27 | func NewDownloader(text, url, dest string, validateFn func(string) error) *Downloader { 28 | return &Downloader{ 29 | progress: progress.New(progress.WithGradient(string(styles.Cyan), string(styles.DarkCyan))), 30 | total: 0, 31 | text: text, 32 | url: url, 33 | dest: dest, 34 | err: nil, 35 | validateFn: validateFn, 36 | } 37 | } 38 | 39 | func (m *Downloader) GetError() error { 40 | return m.err 41 | } 42 | 43 | func (m *Downloader) startDownload() tea.Cmd { 44 | return func() tea.Msg { 45 | httpClient := client.NewHTTPClient() 46 | if err := httpClient.DownloadAndValidateFile(m.url, m.dest, &m.current, &m.total, m.validateFn); err != nil { 47 | m.SetError(err) 48 | return nil 49 | } 50 | 51 | // Set completion when the download finishes successfully 52 | m.SetCompletion(true) 53 | return nil 54 | } 55 | } 56 | 57 | func (m *Downloader) Init() tea.Cmd { 58 | return tea.Batch(m.tick(), m.startDownload()) 59 | } 60 | 61 | func (m *Downloader) Update(msg tea.Msg) (*Downloader, tea.Cmd) { 62 | switch msg := msg.(type) { 63 | case TickMsg: 64 | return m, m.tick() 65 | 66 | case tea.KeyMsg: 67 | if msg.String() == "q" || msg.String() == "ctrl+c" { 68 | analytics.TrackEvent(analytics.Interrupted, analytics.NewEmptyEvent()) 69 | return m, tea.Quit 70 | } 71 | } 72 | 73 | model, cmd := m.progress.Update(msg) 74 | m.progress = model.(progress.Model) 75 | return m, cmd 76 | } 77 | 78 | func (m *Downloader) View() string { 79 | if m.err != nil { 80 | return "" 81 | } 82 | 83 | if m.done { 84 | return fmt.Sprintf("%sDownload Complete!\nTotal Size: %d bytes\n", styles.CorrectMark, m.total) 85 | } 86 | percentage := float64(m.current) / float64(m.total) 87 | return fmt.Sprintf("\n %s: %s / %s \n %s", m.text, ByteCountSI(m.current), ByteCountSI(m.total), m.progress.ViewAs(percentage)) 88 | } 89 | 90 | func (m *Downloader) GetCompletion() bool { 91 | return m.done 92 | } 93 | 94 | func (m *Downloader) tick() tea.Cmd { 95 | return tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg { 96 | return TickMsg(t) 97 | }) 98 | } 99 | 100 | func ByteCountSI(b int64) string { 101 | const unit = 1000 102 | if b < unit { 103 | return fmt.Sprintf("%d B", b) 104 | } 105 | div, exp := int64(unit), 0 106 | for n := b / unit; n >= unit; n /= unit { 107 | div *= unit 108 | exp++ 109 | } 110 | return fmt.Sprintf("%.3f %cB", float64(b)/float64(div), "kMGTPE"[exp]) 111 | } 112 | 113 | // SetCompletion allows you to manually set the completion state for testing purposes. 114 | func (m *Downloader) SetCompletion(complete bool) { 115 | m.done = complete 116 | } 117 | 118 | // SetError allows you to manually set an error for testing purposes. 119 | func (m *Downloader) SetError(err error) { 120 | m.err = err 121 | m.done = true // Mark as done when an error occurs 122 | } 123 | -------------------------------------------------------------------------------- /ui/loading.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "runtime/debug" 10 | "time" 11 | 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/charmbracelet/lipgloss" 14 | 15 | "github.com/initia-labs/weave/analytics" 16 | "github.com/initia-labs/weave/styles" 17 | ) 18 | 19 | const ( 20 | ShowCursor = "\x1b[?25h" 21 | ) 22 | 23 | type TickMsg time.Time 24 | 25 | func DoTick() tea.Cmd { 26 | return tea.Tick(time.Second, func(t time.Time) tea.Msg { 27 | return TickMsg(t) 28 | }) 29 | } 30 | 31 | type Spinner struct { 32 | Frames []string 33 | Complete string 34 | FPS time.Duration 35 | } 36 | 37 | var Dot = Spinner{ 38 | Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "}, 39 | Complete: styles.CorrectMark, 40 | FPS: time.Second / 10, //nolint:gomnd 41 | } 42 | 43 | type Loading struct { 44 | Spinner Spinner 45 | Style lipgloss.Style 46 | Text string 47 | Completing bool 48 | quitting bool 49 | frame int 50 | executeFn tea.Cmd 51 | Err error 52 | EndContext context.Context 53 | NonRetryableErr error 54 | } 55 | 56 | func NewLoading(text string, executeFn tea.Cmd) Loading { 57 | return Loading{ 58 | Spinner: Dot, 59 | Style: lipgloss.NewStyle().Foreground(lipgloss.Color(styles.Cyan)), 60 | Text: text, 61 | executeFn: executeFn, 62 | } 63 | } 64 | 65 | func restoreTerminal() { 66 | _, err := io.WriteString(os.Stdout, ShowCursor) 67 | if err != nil { 68 | return 69 | } 70 | cmd := exec.Command("stty", "sane") 71 | cmd.Stdin = os.Stdin 72 | _ = cmd.Run() 73 | } 74 | 75 | func (m Loading) Init() tea.Cmd { 76 | wrappedExecuteFn := func() tea.Msg { 77 | defer func() { 78 | if r := recover(); r != nil { 79 | restoreTerminal() 80 | fmt.Printf("\nCaught panic in loading:\n\n%s\n\nRestoring terminal...\n\n", r) 81 | debug.PrintStack() 82 | os.Exit(1) 83 | } 84 | }() 85 | return m.executeFn() 86 | } 87 | return tea.Batch(m.tick(), wrappedExecuteFn) 88 | } 89 | 90 | func (m Loading) Update(msg tea.Msg) (Loading, tea.Cmd) { 91 | switch msg := msg.(type) { 92 | case tea.KeyMsg: 93 | switch msg.String() { 94 | case "ctrl+c": 95 | analytics.TrackEvent(analytics.Interrupted, analytics.NewEmptyEvent()) 96 | m.quitting = true 97 | return m, tea.Quit 98 | default: 99 | return m, nil 100 | } 101 | case TickMsg: 102 | m.frame++ 103 | if m.frame >= len(m.Spinner.Frames) { 104 | m.frame = 0 105 | } 106 | return m, m.tick() 107 | case EndLoading: 108 | m.Completing = true 109 | m.EndContext = msg.Ctx 110 | return m, tea.WindowSize() 111 | case ErrorLoading: 112 | m.Err = msg.Err 113 | return m, nil 114 | case NonRetryableErrorLoading: 115 | m.NonRetryableErr = msg.Err 116 | return m, nil 117 | default: 118 | return m, nil 119 | } 120 | } 121 | 122 | func (m Loading) View() string { 123 | if m.frame >= len(m.Spinner.Frames) { 124 | return "(error)" 125 | } 126 | spinner := m.Style.Render(m.Spinner.Frames[m.frame]) 127 | 128 | if m.Completing { 129 | return "" 130 | } 131 | str := fmt.Sprintf("%s%s\n", spinner, m.Text) 132 | return str 133 | } 134 | 135 | func (m Loading) tick() tea.Cmd { 136 | return tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg { 137 | return TickMsg(t) 138 | }) 139 | } 140 | 141 | type EndLoading struct { 142 | Ctx context.Context 143 | } 144 | 145 | type NonRetryableErrorLoading struct { 146 | Err error 147 | } 148 | 149 | func DefaultWait() tea.Cmd { 150 | return func() tea.Msg { 151 | time.Sleep(1500 * time.Millisecond) 152 | return EndLoading{} 153 | } 154 | } 155 | 156 | type ErrorLoading struct { 157 | Err error 158 | } 159 | -------------------------------------------------------------------------------- /ui/selector.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | 9 | "github.com/initia-labs/weave/analytics" 10 | weavecontext "github.com/initia-labs/weave/context" 11 | "github.com/initia-labs/weave/cosmosutils" 12 | "github.com/initia-labs/weave/styles" 13 | ) 14 | 15 | type Selector[T any] struct { 16 | Options []T 17 | Cursor int 18 | CannotBack bool 19 | ToggleTooltip bool 20 | Tooltips *[]Tooltip 21 | TooltipWidth int 22 | } 23 | 24 | func (s *Selector[T]) Select(msg tea.Msg) (*T, tea.Cmd) { 25 | switch msg := msg.(type) { 26 | case tea.KeyMsg: 27 | switch msg.String() { 28 | case "down", "j": 29 | s.Cursor = (s.Cursor + 1) % len(s.Options) 30 | return nil, nil 31 | case "up", "k": 32 | s.Cursor = (s.Cursor - 1 + len(s.Options)) % len(s.Options) 33 | return nil, nil 34 | case "q", "ctrl+c": 35 | analytics.TrackEvent(analytics.Interrupted, analytics.NewEmptyEvent()) 36 | return nil, tea.Quit 37 | case "enter": 38 | return &s.Options[s.Cursor], nil 39 | } 40 | } 41 | return nil, nil 42 | } 43 | 44 | // GetFooter returns the footer text based on the CannotBack flag. 45 | func (s *Selector[T]) GetFooter() string { 46 | footer := "" 47 | if s.CannotBack { 48 | footer += "\n" + styles.RenderFooter("Enter to select, Ctrl+c or q to quit.") + "\n" 49 | } else { 50 | footer += "\n" + styles.RenderFooter("Enter to select, Ctrl+z to go back, Ctrl+c or q to quit.") + "\n" 51 | } 52 | 53 | if s.Tooltips != nil { 54 | if s.ToggleTooltip { 55 | tooltip := *s.Tooltips 56 | footer += styles.RenderFooter("Ctrl+t to hide information.") + "\n" + tooltip[s.Cursor].View(s.TooltipWidth) 57 | } else { 58 | footer += styles.RenderFooter("Ctrl+t to see more info for each option.") + "\n" 59 | } 60 | } 61 | 62 | return footer 63 | 64 | } 65 | 66 | func (s *Selector[T]) View() string { 67 | view := "\n\n" 68 | for i, option := range s.Options { 69 | if i == s.Cursor { 70 | view += styles.SelectorCursor + styles.BoldText(fmt.Sprintf("%v", option), styles.White) + "\n" 71 | } else { 72 | view += " " + styles.Text(fmt.Sprintf("%v", option), styles.Ivory) + "\n" 73 | } 74 | } 75 | 76 | return view + s.GetFooter() 77 | } 78 | 79 | func (s *Selector[T]) ViewTooltip(ctx context.Context) { 80 | s.ToggleTooltip = weavecontext.GetTooltip(ctx) 81 | s.TooltipWidth = weavecontext.GetWindowWidth(ctx) 82 | } 83 | 84 | type VersionSelector struct { 85 | Selector[string] 86 | currentVersion string 87 | } 88 | 89 | func NewVersionSelector(urlMap cosmosutils.BinaryVersionWithDownloadURL, currentVersion string, cannotBack bool) VersionSelector { 90 | return VersionSelector{ 91 | Selector: Selector[string]{ 92 | Options: cosmosutils.SortVersions(urlMap), 93 | CannotBack: cannotBack, 94 | }, 95 | currentVersion: currentVersion, 96 | } 97 | } 98 | 99 | func (v *VersionSelector) View() string { 100 | view := "\n\n" 101 | for i, option := range v.Options { 102 | if i == v.Cursor { 103 | view += styles.SelectorCursor + styles.BoldText(fmt.Sprintf("%v", option), styles.White) 104 | } else { 105 | view += " " + styles.Text(fmt.Sprintf("%v", option), styles.Ivory) 106 | } 107 | 108 | if option == v.currentVersion { 109 | currentVersionText := " (your current version)" 110 | if i == v.Cursor { 111 | view += styles.BoldText(currentVersionText, styles.White) 112 | } else { 113 | view += styles.Text(currentVersionText, styles.Ivory) 114 | } 115 | } 116 | 117 | view += "\n" 118 | } 119 | 120 | return view + v.GetFooter() 121 | } 122 | -------------------------------------------------------------------------------- /ui/tooltip.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/initia-labs/weave/styles" 9 | ) 10 | 11 | const ( 12 | DefaultTooltipPadding = 5 13 | MaxTooltipWidth = 108 14 | ) 15 | 16 | type Tooltip struct { 17 | title string 18 | body string 19 | warning string 20 | 21 | boldTexts []string 22 | links []string 23 | highlightTexts []string 24 | } 25 | 26 | func NewTooltip(title, body, warning string, boldTexts, links, highlightTexts []string) Tooltip { 27 | return Tooltip{ 28 | title: title, 29 | body: body, 30 | warning: warning, 31 | boldTexts: boldTexts, 32 | links: links, 33 | highlightTexts: highlightTexts, 34 | } 35 | } 36 | 37 | func NewTooltipSlice(tooltip Tooltip, size int) []Tooltip { 38 | tooltips := make([]Tooltip, size) 39 | for i := range tooltips { 40 | tooltips[i] = tooltip 41 | } 42 | return tooltips 43 | } 44 | 45 | func visibleLength(text string) int { 46 | // Use regex to strip out ANSI escape codes (e.g., for colors) 47 | ansi := regexp.MustCompile(`\x1b\[[0-9;]*m`) 48 | return len(ansi.ReplaceAllString(text, "")) 49 | } 50 | 51 | func (t *Tooltip) createFrame(width int) string { 52 | if width > MaxTooltipWidth { 53 | width = MaxTooltipWidth 54 | } else { 55 | width -= DefaultTooltipPadding 56 | if width < 0 { 57 | width = DefaultTooltipPadding 58 | } 59 | } 60 | 61 | var text string 62 | if t.warning != "" { 63 | text = styles.WordwrapString(t.body, width) + "\n" + styles.TextWithoutOverridingStyledText(styles.WordwrapString(t.warning, width), styles.Yellow) 64 | } else { 65 | text = styles.WordwrapString(t.body, width) 66 | } 67 | lines := strings.Split(text, "\n") 68 | 69 | titleLength := visibleLength(t.title) 70 | titleWidth := width - titleLength 71 | if titleWidth < 0 { 72 | titleWidth = 0 73 | } 74 | top := "┌ " + styles.BoldText(t.title, styles.White) + " " + strings.Repeat("─", titleWidth) + "┐" 75 | bottom := "└" + strings.Repeat("─", width+2) + "┘" 76 | 77 | var framedContent strings.Builder 78 | for _, line := range lines { 79 | for _, boldText := range t.boldTexts { 80 | if strings.Contains(line, boldText) { 81 | line = strings.ReplaceAll(line, boldText, styles.BoldText(boldText, styles.Ivory)) 82 | } 83 | } 84 | linePadding := width - visibleLength(line) 85 | if linePadding < 0 { 86 | linePadding = 0 87 | } 88 | framedContent.WriteString(fmt.Sprintf("│ %s%s │\n", line, strings.Repeat(" ", linePadding))) 89 | } 90 | 91 | return fmt.Sprintf("%s\n%s%s", top, framedContent.String(), bottom) 92 | } 93 | 94 | func (t *Tooltip) View(width int) string { 95 | return "\n" + t.createFrame(width) + "\n" 96 | } 97 | --------------------------------------------------------------------------------