diff --git a/.github/workflows/portal-loop.yml b/.github/workflows/portal-loop.yml index 1c83a2854c2..b81957b22db 100644 --- a/.github/workflows/portal-loop.yml +++ b/.github/workflows/portal-loop.yml @@ -1,13 +1,17 @@ name: portal-loop on: + pull_request: + branches: + - master push: paths: - "misc/loop/**" - ".github/workflows/portal-loop.yml" branches: - "master" - - "ops/portal-loop" + # NOTE(albttx): branch name to simplify tests for this workflow + - "ci/portal-loop" tags: - "v*" @@ -46,3 +50,57 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + + test-portal-loop-docker-compose: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Setup the images" + run: | + cd misc/loop + + docker compose build + docker compose pull + docker compose up -d + + - name: "Test1 - Portal loop start gnoland" + run: | + while + block_height=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height') + echo "Current block height: $block_height" + [[ "$block_height" -lt 10 ]] + do + sleep 1 + done + + curl -s localhost:26657/status | jq + + - name: "Buid new gnolang/gno image" + run: | + docker build -t ghcr.io/gnolang/gno/gnoland:master -f Dockerfile --target gnoland . + + - name: "Wait for new docker image" + run: | + ip_addr=$(cat misc/loop/traefik/gno.yml | grep -o "http://.*:26657") + while + new_ip_addr=$(cat misc/loop/traefik/gno.yml | grep -o "http://.*:26657") + echo "${ip_addr} -> ${new_ip_addr}" + [[ "${ip_addr}" == ${new_ip_addr} ]] + do + sleep 5 + done + + - name: "Test2 - Wait portal-loop start new image" + run: | + while + block_height=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height') + echo "Current block height: $block_height" + [[ "$block_height" -lt 10 ]] + do + sleep 5 + done + docker ps -a + curl -s localhost:26657/status | jq diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..fa5a9e47270 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# build gno +FROM golang:1.22-alpine AS build-gno +RUN go env -w GOMODCACHE=/root/.cache/go-build +WORKDIR /gnoroot +ENV GNOROOT="/gnoroot" +COPY . ./ +RUN --mount=type=cache,target=/root/.cache/go-build go mod download +RUN --mount=type=cache,target=/root/.cache/go-build go build -o ./build/gnoland ./gno.land/cmd/gnoland +RUN --mount=type=cache,target=/root/.cache/go-build go build -o ./build/gnokey ./gno.land/cmd/gnokey +RUN --mount=type=cache,target=/root/.cache/go-build go build -o ./build/gnoweb ./gno.land/cmd/gnoweb +RUN --mount=type=cache,target=/root/.cache/go-build go build -o ./build/gno ./gnovm/cmd/gno + +# Base image +FROM alpine:3.17 AS base +WORKDIR /gnoroot +ENV GNOROOT="/gnoroot" +RUN apk add ca-certificates +CMD [ "" ] + +# alpine images +# gnoland +FROM base AS gnoland +COPY --from=build-gno /gnoroot/build/gnoland /usr/bin/gnoland +COPY --from=build-gno /gnoroot/examples /gnoroot/examples +COPY --from=build-gno /gnoroot/gnovm/stdlibs /gnoroot/gnovm/stdlibs +COPY --from=build-gno /gnoroot/gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl +COPY --from=build-gno /gnoroot/gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt +EXPOSE 26656 26657 +ENTRYPOINT ["/usr/bin/gnoland"] + +# gnokey +FROM base AS gnokey +COPY --from=build-gno /gnoroot/build/gnokey /usr/bin/gnokey +# gofmt is required by `gnokey maketx addpkg` +COPY --from=build-gno /usr/local/go/bin/gofmt /usr/bin/gofmt +ENTRYPOINT ["/usr/bin/gnokey"] + +# gno +FROM base AS gno +COPY --from=build-gno /gnoroot/build/gno /usr/bin/gno +ENTRYPOINT ["/usr/bin/gno"] + +# gnoweb +FROM base AS gnoweb +COPY --from=build-gno /gnoroot/build/gnoweb /usr/bin/gnoweb +COPY --from=build-gno /opt/gno/src/gno.land/cmd/gnoweb /opt/gno/src/gnoweb +EXPOSE 8888 +ENTRYPOINT ["/usr/bin/gnoweb"] + +# all, contains everything. +FROM base AS all +COPY --from=build-gno /gnoroot/build/* /usr/bin/ +COPY --from=build-gno /gnoroot/examples /gnoroot/examples +COPY --from=build-gno /gnoroot/gnovm/stdlibs /gnoroot/gnovm/stdlibs +COPY --from=build-gno /gnoroot/gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl +COPY --from=build-gno /gnoroot/gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt +# gofmt is required by `gnokey maketx addpkg` +COPY --from=build-gno /usr/local/go/bin/gofmt /usr/bin diff --git a/contribs/gnodev/Makefile b/contribs/gnodev/Makefile index 01801064d1f..df57040d92d 100644 --- a/contribs/gnodev/Makefile +++ b/contribs/gnodev/Makefile @@ -5,11 +5,13 @@ GOTEST_FLAGS ?= $(GOBUILD_FLAGS) -v -p 1 -timeout=5m rundep := go run -modfile ../../misc/devdeps/go.mod golangci_lint := $(rundep) github.com/golangci/golangci-lint/cmd/golangci-lint -install: +install: install.gnodev +install.gnodev: go install $(GOBUILD_FLAGS) ./cmd/gnodev -build: - go build $(GOBUILD_FLAGS) -o build/gnodev ./cmd/gnodev +# keep gnobro out the default install for now +install.gnobro: + go install $(GOBUILD_FLAGS) ./cmd/gnobro lint: $(golangci_lint) --config ../../.github/golangci.yml run ./... diff --git a/contribs/gnodev/README.md b/contribs/gnodev/README.md index 6da9e7b1ebc..3b26903c7eb 100644 --- a/contribs/gnodev/README.md +++ b/contribs/gnodev/README.md @@ -1,30 +1,67 @@ -## `gnodev`: Your Gno Companion Tool +## `gnodev`: Your Gno Development Companion -`gnodev` is designed to be a robust and user-friendly tool in your realm package development journey, streamlining your workflow and enhancing productivity. +`gnodev` is a robust tool designed to streamline your Gno package development process, enhancing productivity +by providing immediate feedback on code changes. -We will only give a quick overview below. You may find the official documentation at [docs/gno-tooling/gnodev.md](../../docs/gno-tooling/cli/gnodev.md). +Please note that this is a quick overview. For a more detailed guide, refer to the official documentation at +[docs/gno-tooling/gnodev.md](../../docs/gno-tooling/cli/gnodev.md). ### Synopsis -**gnodev** [**-minimal**] [**-no-watch**] [**PKG_PATH ...**] +**gnodev** [**options**] [**PKG_PATH ...**] ### Features -- **In-Memory Node**: Gnodev starts an in-memory node, and automatically loads - the **examples** folder and any user-specified paths. -- **Web Interface Server**: Starts a `gnoweb` server on `localhost:8888`. -- **Hot Reload**: Monitors the example packages folder and specified directories for file changes, - reloading the package and automatically restarting the node as needed. -- **State Maintenance**: Ensures the current state is preserved by replaying all transactions. +- **In-Memory Node**: Gnodev starts an in-memory node, automatically loading the **examples** folder and any + user-specified paths. +- **Web Interface Server**: Gnodev starts a `gnoweb` server on [`localhost:8888`](https://localhost:8888). +- **Balances and Keybase Customization**: Set account balances, load them from a file, or add new accounts via a flag. +- **Hot Reload**: Monitors the **examples** folder and specified directories for file changes, reloading the + package and automatically restarting the node as needed. +- **State Maintenance**: Ensures the previous node state is preserved by replaying all transactions. +- **Transaction Manipulation**: Allows for interactive cancellation and redoing of transactions. +- **State Export**: Export the current state at any time in a genesis doc format. ### Commands -While `gnodev` is running, the user can trigger specific actions by pressing -the following combinations: -- **H**: Display help information. -- **R**: Reload the node, without resetting the state. -- **Ctrl+R**: Reset the current node state. -- **Ctrl+C**: Exit `gnodev`. +While `gnodev` is running, trigger specific actions by pressing the following combinations: +- **H**: Display help information. +- **A**: Display account balances. +- **R**: Reload the node manually. +- **P**: Cancel the last action. +- **N**: Redo the last cancelled action. +- **Ctrl+S**: Save the current state. +- **Ctrl+R**: Restore the saved state. +- **E**: Export the current state to a genesis file. +- **Cmd+R**: Reset the current node state. +- **Cmd+C**: Exit `gnodev`. + +### Usage +Run `gnodev` followed by any specific options and/or package paths. The **examples** directory is loaded +automatically. Use `--minimal` to prevent this. + +Example: +``` +gnodev --add-account [:] ./myrealm +``` + +### `gnobro`: realm interface +`gnobro` is a terminal user interface (TUI) that allows you to browse realms within your terminal. It +automatically connects to `gnodev` for real-time development. In addition to hot reload, it also has the +ability to execute commands and interact with your realm. + + +#### Usage +**gnobro** [**options**] [**PKG_PATH **] + +Run gnobro followed by any specific options and/or a target pacakge path. + +Use `gnobro -h` for a detailed list of options. + +Example: +``` +gnobro gno.land/r/demo/home +``` -### Loading 'examples' -The **examples** directory is loaded automatically. If working within this folder, you don't have to specify any additional paths to `gnodev`. Use `--minimal` to prevent this. ### Installation Run `make install` to install `gnodev`. + +Run `make install.gnobro` to install `gnobro`. diff --git a/contribs/gnodev/cmd/gnobro/assets/banner_land_1.txt b/contribs/gnodev/cmd/gnobro/assets/banner_land_1.txt new file mode 100644 index 00000000000..9a5931625ca --- /dev/null +++ b/contribs/gnodev/cmd/gnobro/assets/banner_land_1.txt @@ -0,0 +1,19 @@ + . + + + . Hello %s, Welcome to + . ++ .,-:::::/ :::. :::. ... . + . ,;;-'````' `;;;;, `;;; .;;;;;;;. + [[[ [[[[[[/ [[[[[. '[[ ,[[ \[[, + "$$c. "$$ + $$$ "Y$c$$ $$$, $$$ + `Y8bo,,,o88o 888 Y88 "888,_ _,88P + . `'YMUP"YMM MMM . YM "YMMMMMP" + + . . + ::: + :::. :::. :::. :::::::-. + ;;; ;;`;; `;;;;, `;;; ;;, `';, + + [[[ ,[[ '[[, + [[[[[. '[[ `[[ [[ + $$' c$$$cc$$$c $$$ "Y$c$$ $$, $$ + o88oo,.__ 888 888, 888 Y88 888_,o8P' + """"YUMMM YMM ""` MMM + YM MMMMP"` + + . + + + press to continue diff --git a/contribs/gnodev/cmd/gnobro/assets/gn_hc1.utf8ans b/contribs/gnodev/cmd/gnobro/assets/gn_hc1.utf8ans new file mode 100644 index 00000000000..b50762d0abb --- /dev/null +++ b/contribs/gnodev/cmd/gnobro/assets/gn_hc1.utf8ans @@ -0,0 +1,25 @@ + · . · · + . * . . * . · + · · · · . . . + · . . . . · * + . · . . · + ░░ ░░ ░ ░░ ░ ░░ ░ ░░ ░░░ ░░░░░░ ░░░ ░░░░ ░ ░░ +░░▒▒░░░▒▒░░░░░▒░░░░░░▒▒░░▒░░▒▒░▒░░░░░░░ ░░░░░░▒▒░▒▒▒░▒▒▒▒▒▒░░▒▒▒░░░▒▒▒▒░▒░░▒▒░░ +▒▒▓▓▒▒▒▓▓▒▒▒▒▒▓▒▒▒▒▒▒▓▓▒▒▓▒▒▓▓▒▓▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓▒▓▓▓▒▓▓▓▓▓▓▒▒▓▓▓▒▒▒▓▓▓▓▒▓▒▒▓▓▒▒ +▓▓██▓▓▓██▓▓▓▓▓█▓▓▓▓▓▓██▓▓█▓▓██▓█▓▓▓▓▓▓▓▒▓▓▓▓▓▓██▓███▓██████▓▓███▓▓▓████▓█▓▓██▓▓ +▀██▒███ █████ ██████▌▐██▒██▌██▒███████▓███▓██▌▐██▒██▌██▒█████▒████ ███▒███ ██ + ▐▒▒▒█ █▒█▌ ▐▒███▌ ▐▒▒▒▌ ▐▒▒███▌█▒███▌███▌ ▐▒▒▒▌ ▐▒▒███▌█▒███ █▒▒▒█▌ ▐▒ + ▒▒▒▒▄ ▄▒█ ▒█▒█▄ ▒▒▒░ ▀▒▒█▌ ▐▒██▐ █▒█▄ ▒▒▒░ ▀▒▒█▌ ▐▒ █▄ ▄▒▒ ▒ ▒ + ░ ░ ▄ ▄▄▒▌ ▒█▒▀ ▀▒▒ ▒▒▒▀▄ ▄▒▄▀▀▄▐▒▀ ▀▒▒ ▒▒▒▀▄ ▄▒ █ ▄ ▄▄▄▀▀▄ ▒ + ▀░ ▒░▒▄░ ▀▄▒▀▒▒ ▀█▒▄▄ ▄■▄▒▒▀ ▒▌░░▐░▀ ▒ ▀ ▄ ■▒░▀ ▒ ▀░ ▒░▌░░▐░ ▀▄ + ▌░░▐ ▌░░▐ + ▄▄▀▀▀▀▀▀▀▄▄▀▀▄▀▀▀▀▀▄ ▄▀▀▀▀▀▄ ▌▒▒▐ ▄▄▀▀▀▀▀▄ ▄▀▄▄▀▀▀▀▀▄ ▄▄▄▄▄▌▒▒▐ + ▌▄▓▓▓▓▓▓▓▄ ▓▓▄▓▓▓▓▓▄▀▀▄▓▓▓▓▓▄▀▄ ▌▓▓▐ ▌▄▓▓▓▓▓▄▀▌▓▌▄▓▓▓▓▓▄▀▀▄▄▄▄▄▄▓▓▐ + ▌█▌ ██ ███▀▄▄▀██ ▐██▀▀▀██▌▐ ▌██▐ ▐▐██▀▀▀██▌ ███▀▄▄▀██ ▐██▀▀▀███▐ + ▌▀░░░░░░░░ ░░▌▌ ▌░░ ░░ ░░▐ ▌░░▐ ▌░░ ░░ ░░▌█ ▌░░ ░░▌ ▐░░▐ + ▐ ▒▒ ▒▒▌▌ ▌▒▒ ▒▒▌ ▐▒▒▐▄▀▄▌▒▒▐ ▌▒▒▌ ▐▒▒ ▒▒▌▌ ▌▒▒ ▒▒▌ ▐▒▒▐ + ▌▄▄ ▓▓ ▓▓▌▌ ▌▓▓ ▓▓▓ ▓▓▓ ▄▓▄ ▓▓ ▀▀ ▓▓▓ ▐▓▓ ▓▓▌▌ ▌▓▓ ▓▓▓ ▐▓▓▐ +░ ▌▀███████▀ ███▐ ▐▐██ ▄▀█████▀▄▄▀█▀▄▀████ ▄▀████▀██ ███▐ ▐▐██ ▄▀████▀██▐ ░ +▒ ▓▓▄▄▄▄▄▄▄▄▀▄▄▄▀ ▀▄▄▀ ▀▄▄▄▄▄▀ ▀▄▀ ▀▄▄▄▄▀ ▀▄▄▄▄▀▄▄▀▄▄▄▀ ▀▄▄▀ ▀▄▄▄▄▀▄▄▓ ▒ +▓ ▓ +█▓▒░ ░▒▓█ \ No newline at end of file diff --git a/contribs/gnodev/cmd/gnobro/assets/gn_hc2.utf8ans b/contribs/gnodev/cmd/gnobro/assets/gn_hc2.utf8ans new file mode 100644 index 00000000000..12a3a9a236f --- /dev/null +++ b/contribs/gnodev/cmd/gnobro/assets/gn_hc2.utf8ans @@ -0,0 +1,25 @@ + · . · · + . + . . + . · + · · · · . . . + · . . . . · + + . · . . · + ░░ ░░ ░ ░░ ░ ░░ ░ ░░ ░░░ ░░░░░░ ░░░ ░░░░ ░ ░░ +░░▒▒░░░▒▒░░░░░▒░░░░░░▒▒░░▒░░▒▒░▒░░░░░░░ ░░░░░░▒▒░▒▒▒░▒▒▒▒▒▒░░▒▒▒░░░▒▒▒▒░▒░░▒▒░░ +▒▒▓▓▒▒▒▓▓▒▒▒▒▒▓▒▒▒▒▒▒▓▓▒▒▓▒▒▓▓▒▓▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓▒▓▓▓▒▓▓▓▓▓▓▒▒▓▓▓▒▒▒▓▓▓▓▒▓▒▒▓▓▒▒ +▓▓██▓▓▓██▓▓▓▓▓█▓▓▓▓▓▓██▓▓█▓▓██▓█▓▓▓▓▓▓▓▒▓▓▓▓▓▓██▓███▓██████▓▓███▓▓▓████▓█▓▓██▓▓ +▀██▒███ █████ ██████▌▐██▒██▌██▒███████▓███▓██▌▐██▒██▌██▒█████▒████ ███▒███ ██ + ▐▒▒▒█ █▒█▌ ▐▒███▌ ▐▒▒▒▌ ▐▒▒███▌█▒███▌███▌ ▐▒▒▒▌ ▐▒▒███▌█▒███ █▒▒▒█▌ ▐▒ + ▒▒▒▒▄ ▄▒█ ▒█▒█▄ ▒▒▒░ ▀▒▒█▌ ▐▒██▐ █▒█▄ ▒▒▒░ ▀▒▒█▌ ▐▒ █▄ ▄▒▒ ▒ ▒ + ░ ░ ▄ ▄▄▒▌ ▒█▒▀ ▀▒▒ ▒▒▒▀▄ ▄▒▄▀▀▄▐▒▀ ▀▒▒ ▒▒▒▀▄ ▄▒ █ ▄ ▄▄▄▀▀▄ ▒ + ▀░ ▒░▒▄░ ▀▄▒▀▒▒ ▀█▒▄▄ ▄■▄▒▒▀ ▒▌░░▐░▀ ▒ ▀ ▄ ■▒░▀ ▒ ▀░ ▒░▌░░▐░ ▀▄ + ▌░░▐ ▌░░▐ + ▄▄▀▀▀▀▀▀▀▄▄▀▀▄▀▀▀▀▀▄ ▄▀▀▀▀▀▄ ▌▒▒▐ ▄▄▀▀▀▀▀▄ ▄▀▄▄▀▀▀▀▀▄ ▄▄▄▄▄▌▒▒▐ + ▌▄▓▓▓▓▓▓▓▄ ▓▓▄▓▓▓▓▓▄▀▀▄▓▓▓▓▓▄▀▄ ▌▓▓▐ ▌▄▓▓▓▓▓▄▀▌▓▌▄▓▓▓▓▓▄▀▀▄▄▄▄▄▄▓▓▐ + ▌█▌ ██ ███▀▄▄▀██ ▐██▀▀▀██▌▐ ▌██▐ ▐▐██▀▀▀██▌ ███▀▄▄▀██ ▐██▀▀▀███▐ + ▌▀░░░░░░░░ ░░▌▌ ▌░░ ░░ ░░▐ ▌░░▐ ▌░░ ░░ ░░▌█ ▌░░ ░░▌ ▐░░▐ + ▐ ▒▒ ▒▒▌▌ ▌▒▒ ▒▒▌ ▐▒▒▐▄▀▄▌▒▒▐ ▌▒▒▌ ▐▒▒ ▒▒▌▌ ▌▒▒ ▒▒▌ ▐▒▒▐ + ▌▄▄ ▓▓ ▓▓▌▌ ▌▓▓ ▓▓▓ ▓▓▓ ▄▓▄ ▓▓ ▀▀ ▓▓▓ ▐▓▓ ▓▓▌▌ ▌▓▓ ▓▓▓ ▐▓▓▐ +░ ▌▀███████▀ ███▐ ▐▐██ ▄▀█████▀▄▄▀█▀▄▀████ ▄▀████▀██ ███▐ ▐▐██ ▄▀████▀██▐ ░ +▒ ▓▓▄▄▄▄▄▄▄▄▀▄▄▄▀ ▀▄▄▀ ▀▄▄▄▄▄▀ ▀▄▀ ▀▄▄▄▄▀ ▀▄▄▄▄▀▄▄▀▄▄▄▀ ▀▄▄▀ ▀▄▄▄▄▀▄▄▓ ▒ +▓ ▓ +█▓▒░ ░▒▓█ \ No newline at end of file diff --git a/contribs/gnodev/cmd/gnobro/assets/gn_hc3.utf8ans b/contribs/gnodev/cmd/gnobro/assets/gn_hc3.utf8ans new file mode 100644 index 00000000000..ff26c68f964 --- /dev/null +++ b/contribs/gnodev/cmd/gnobro/assets/gn_hc3.utf8ans @@ -0,0 +1,25 @@ + · . · · + . · . . · . · + · · · · . . . + · . . . . · · + . · . . · + ░░ ░░ ░ ░░ ░ ░░ ░ ░░ ░░░ ░░░░░░ ░░░ ░░░░ ░ ░░ +░░▒▒░░░▒▒░░░░░▒░░░░░░▒▒░░▒░░▒▒░▒░░░░░░░ ░░░░░░▒▒░▒▒▒░▒▒▒▒▒▒░░▒▒▒░░░▒▒▒▒░▒░░▒▒░░ +▒▒▓▓▒▒▒▓▓▒▒▒▒▒▓▒▒▒▒▒▒▓▓▒▒▓▒▒▓▓▒▓▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓▒▓▓▓▒▓▓▓▓▓▓▒▒▓▓▓▒▒▒▓▓▓▓▒▓▒▒▓▓▒▒ +▓▓██▓▓▓██▓▓▓▓▓█▓▓▓▓▓▓██▓▓█▓▓██▓█▓▓▓▓▓▓▓▒▓▓▓▓▓▓██▓███▓██████▓▓███▓▓▓████▓█▓▓██▓▓ +▀██▒███ █████ ██████▌▐██▒██▌██▒███████▓███▓██▌▐██▒██▌██▒█████▒████ ███▒███ ██ + ▐▒▒▒█ █▒█▌ ▐▒███▌ ▐▒▒▒▌ ▐▒▒███▌█▒███▌███▌ ▐▒▒▒▌ ▐▒▒███▌█▒███ █▒▒▒█▌ ▐▒ + ▒▒▒▒▄ ▄▒█ ▒█▒█▄ ▒▒▒░ ▀▒▒█▌ ▐▒██▐ █▒█▄ ▒▒▒░ ▀▒▒█▌ ▐▒ █▄ ▄▒▒ ▒ ▒ + ░ ░ ▄ ▄▄▒▌ ▒█▒▀ ▀▒▒ ▒▒▒▀▄ ▄▒▄▀▀▄▐▒▀ ▀▒▒ ▒▒▒▀▄ ▄▒ █ ▄ ▄▄▄▀▀▄ ▒ + ▀░ ▒░▒▄░ ▀▄▒▀▒▒ ▀█▒▄▄ ▄■▄▒▒▀ ▒▌░░▐░▀ ▒ ▀ ▄ ■▒░▀ ▒ ▀░ ▒░▌░░▐░ ▀▄ + ▌░░▐ ▌░░▐ + ▄▄▀▀▀▀▀▀▀▄▄▀▀▄▀▀▀▀▀▄ ▄▀▀▀▀▀▄ ▌▒▒▐ ▄▄▀▀▀▀▀▄ ▄▀▄▄▀▀▀▀▀▄ ▄▄▄▄▄▌▒▒▐ + ▌▄▓▓▓▓▓▓▓▄ ▓▓▄▓▓▓▓▓▄▀▀▄▓▓▓▓▓▄▀▄ ▌▓▓▐ ▌▄▓▓▓▓▓▄▀▌▓▌▄▓▓▓▓▓▄▀▀▄▄▄▄▄▄▓▓▐ + ▌█▌ ██ ███▀▄▄▀██ ▐██▀▀▀██▌▐ ▌██▐ ▐▐██▀▀▀██▌ ███▀▄▄▀██ ▐██▀▀▀███▐ + ▌▀░░░░░░░░ ░░▌▌ ▌░░ ░░ ░░▐ ▌░░▐ ▌░░ ░░ ░░▌█ ▌░░ ░░▌ ▐░░▐ + ▐ ▒▒ ▒▒▌▌ ▌▒▒ ▒▒▌ ▐▒▒▐▄▀▄▌▒▒▐ ▌▒▒▌ ▐▒▒ ▒▒▌▌ ▌▒▒ ▒▒▌ ▐▒▒▐ + ▌▄▄ ▓▓ ▓▓▌▌ ▌▓▓ ▓▓▓ ▓▓▓ ▄▓▄ ▓▓ ▀▀ ▓▓▓ ▐▓▓ ▓▓▌▌ ▌▓▓ ▓▓▓ ▐▓▓▐ +░ ▌▀███████▀ ███▐ ▐▐██ ▄▀█████▀▄▄▀█▀▄▀████ ▄▀████▀██ ███▐ ▐▐██ ▄▀████▀██▐ ░ +▒ ▓▓▄▄▄▄▄▄▄▄▀▄▄▄▀ ▀▄▄▀ ▀▄▄▄▄▄▀ ▀▄▀ ▀▄▄▄▄▀ ▀▄▄▄▄▀▄▄▀▄▄▄▀ ▀▄▄▀ ▀▄▄▄▄▀▄▄▓ ▒ +▓ ▓ +█▓▒░ ░▒▓█ \ No newline at end of file diff --git a/contribs/gnodev/cmd/gnobro/assets/gn_hc4.utf8ans b/contribs/gnodev/cmd/gnobro/assets/gn_hc4.utf8ans new file mode 100644 index 00000000000..b92dee86843 --- /dev/null +++ b/contribs/gnodev/cmd/gnobro/assets/gn_hc4.utf8ans @@ -0,0 +1,25 @@ + · . · · + . · . . · . · + · · · · . . . + · . . . . · · + . · . . · + ░░ ░░ ░ ░░ ░ ░░ ░ ░░ ░░░ ░░░░░░ ░░░ ░░░░ ░ ░░ +░░▒▒░░░▒▒░░░░░▒░░░░░░▒▒░░▒░░▒▒░▒░░░░░░░ ░░░░░░▒▒░▒▒▒░▒▒▒▒▒▒░░▒▒▒░░░▒▒▒▒░▒░░▒▒░░ +▒▒▓▓▒▒▒▓▓▒▒▒▒▒▓▒▒▒▒▒▒▓▓▒▒▓▒▒▓▓▒▓▒▒▒▒▒▒▒░▒▒▒▒▒▒▓▓▒▓▓▓▒▓▓▓▓▓▓▒▒▓▓▓▒▒▒▓▓▓▓▒▓▒▒▓▓▒▒ +▓▓██▓▓▓██▓▓▓▓▓█▓▓▓▓▓▓██▓▓█▓▓██▓█▓▓▓▓▓▓▓▒▓▓▓▓▓▓██▓███▓██████▓▓███▓▓▓████▓█▓▓██▓▓ +▀██▒███ █████ ██████▌▐██▒██▌██▒███████▓███▓██▌▐██▒██▌██▒█████▒████ ███▒███ ██ + ▐▒▒▒█ █▒█▌ ▐▒███▌ ▐▒▒▒▌ ▐▒▒███▌█▒███▌███▌ ▐▒▒▒▌ ▐▒▒███▌█▒███ █▒▒▒█▌ ▐▒ + ▒▒▒▒▄ ▄▒█ ▒█▒█▄ ▒▒▒░ ▀▒▒█▌ ▐▒██▐ █▒█▄ ▒▒▒░ ▀▒▒█▌ ▐▒ █▄ ▄▒▒ ▒ ▒ + ░ ░ ▄ ▄▄▒▌ ▒█▒▀ ▀▒▒ ▒▒▒▀▄ ▄▒▄▀▀▄▐▒▀ ▀▒▒ ▒▒▒▀▄ ▄▒ █ ▄ ▄▄▄▀▀▄ ▒ + ▀░ ▒░▒▄░ ▀▄▒▀▒▒ ▀█▒▄▄ ▄■▄▒▒▀ ▒▌░░▐░▀ ▒ ▀ ▄ ■▒░▀ ▒ ▀░ ▒░▌░░▐░ ▀▄ + ▌░░▐ ▌░░▐ + ▄▄▀▀▀▀▀▀▀▄▄▀▀▄▀▀▀▀▀▄ ▄▀▀▀▀▀▄ ▌▒▒▐ ▄▄▀▀▀▀▀▄ ▄▀▄▄▀▀▀▀▀▄ ▄▄▄▄▄▌▒▒▐ + ▌▄▓▓▓▓▓▓▓▄ ▓▓▄▓▓▓▓▓▄▀▀▄▓▓▓▓▓▄▀▄ ▌▓▓▐ ▌▄▓▓▓▓▓▄▀▌▓▌▄▓▓▓▓▓▄▀▀▄▄▄▄▄▄▓▓▐ + ▌█▌ ██ ███▀▄▄▀██ ▐██▀▀▀██▌▐ ▌██▐ ▐▐██▀▀▀██▌ ███▀▄▄▀██ ▐██▀▀▀███▐ + ▌▀░░░░░░░░ ░░▌▌ ▌░░ ░░ ░░▐ ▌░░▐ ▌░░ ░░ ░░▌█ ▌░░ ░░▌ ▐░░▐ + ▐ ▒▒ ▒▒▌▌ ▌▒▒ ▒▒▌ ▐▒▒▐▄▀▄▌▒▒▐ ▌▒▒▌ ▐▒▒ ▒▒▌▌ ▌▒▒ ▒▒▌ ▐▒▒▐ + ▌▄▄ ▓▓ ▓▓▌▌ ▌▓▓ ▓▓▓ ▓▓▓ ▄▓▄ ▓▓ ▀▀ ▓▓▓ ▐▓▓ ▓▓▌▌ ▌▓▓ ▓▓▓ ▐▓▓▐ +░ ▌▀███████▀ ███▐ ▐▐██ ▄▀█████▀▄▄▀█▀▄▀████ ▄▀████▀██ ███▐ ▐▐██ ▄▀████▀██▐ ░ +▒ ▓▓▄▄▄▄▄▄▄▄▀▄▄▄▀ ▀▄▄▀ ▀▄▄▄▄▄▀ ▀▄▀ ▀▄▄▄▄▀ ▀▄▄▄▄▀▄▄▀▄▄▄▀ ▀▄▄▀ ▀▄▄▄▄▀▄▄▓ ▒ +▓ ▓ +█▓▒░ ░▒▓█ \ No newline at end of file diff --git a/contribs/gnodev/cmd/gnobro/banner.go b/contribs/gnodev/cmd/gnobro/banner.go new file mode 100644 index 00000000000..311ee9bdb2a --- /dev/null +++ b/contribs/gnodev/cmd/gnobro/banner.go @@ -0,0 +1,37 @@ +package main + +import ( + "embed" + "path/filepath" + "time" + + "github.com/gnolang/gno/contribs/gnodev/pkg/browser" +) + +//go:embed assets/*.utf8ans +var gnoland_banner embed.FS + +func NewGnoLandBanner() browser.ModelBanner { + const assets = "assets" + + entries, err := gnoland_banner.ReadDir(assets) + if err != nil { + panic("unable to banner dir: " + err.Error()) + } + + frames := make([]string, len(entries)) + for i, entry := range entries { + if entry.IsDir() { + continue + } + + frame, err := gnoland_banner.ReadFile(filepath.Join(assets, entry.Name())) + if err != nil { + panic("unable to read banner frame: " + err.Error()) + } + + frames[i] = string(frame) + } + + return browser.NewModelBanner(time.Second/3, frames) +} diff --git a/contribs/gnodev/cmd/gnobro/main.go b/contribs/gnodev/cmd/gnobro/main.go new file mode 100644 index 00000000000..6bb6bfc2396 --- /dev/null +++ b/contribs/gnodev/cmd/gnobro/main.go @@ -0,0 +1,462 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "log/slog" + "net" + "net/url" + "os" + "os/signal" + "path/filepath" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + charmlog "github.com/charmbracelet/log" + "github.com/charmbracelet/ssh" + "github.com/charmbracelet/wish" + "github.com/charmbracelet/wish/activeterm" + "github.com/charmbracelet/wish/bubbletea" + "github.com/charmbracelet/wish/logging" + "golang.org/x/sync/errgroup" + + "github.com/gnolang/gno/contribs/gnodev/pkg/browser" + "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/gno.land/pkg/gnoclient" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" +) + +const gnoPrefix = "gno.land" + +type broCfg struct { + readonly bool + remote string + dev bool + devRemote string + chainID string + defaultAccount string + defaultRealm string + sshListener string + sshHostKeyPath string + banner bool +} + +var defaultBroOptions = broCfg{ + remote: "127.0.0.1:26657", + dev: true, + devRemote: "", + sshListener: "", + defaultRealm: "gno.land/r/gnoland/home", + chainID: "dev", + sshHostKeyPath: ".ssh/id_ed25519", +} + +func main() { + cfg := &broCfg{} + + stdio := commands.NewDefaultIO() + cmd := commands.NewCommand( + commands.Metadata{ + Name: "gnobro", + ShortUsage: "gnobro [flags] [pkg_path]", + ShortHelp: "Gno Browser, a realm explorer", + LongHelp: `Gnobro is a terminal user interface (TUI) that allows you to browse realms within your +terminal. It automatically connects to Gnodev for real-time development. In +addition to hot reload, it also has the ability to execute commands and interact +with your realm. +`, + }, + cfg, + func(_ context.Context, args []string) error { + return execBrowser(cfg, args, stdio) + }) + + cmd.Execute(context.Background(), os.Args[1:]) +} + +func (c *broCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.remote, + "remote", + defaultBroOptions.remote, + "remote gno.land URL", + ) + + fs.StringVar( + &c.chainID, + "chainid", + defaultBroOptions.chainID, + "chainid", + ) + + fs.StringVar( + &c.defaultAccount, + "account", + defaultBroOptions.defaultAccount, + "default local account to use", + ) + + fs.StringVar( + &c.defaultRealm, + "default-realm", + defaultBroOptions.defaultRealm, + "default realm to display when gnobro starts and no argument is provided", + ) + + fs.StringVar( + &c.sshListener, + "ssh", + defaultBroOptions.sshListener, + "ssh server listener address", + ) + + fs.StringVar( + &c.sshHostKeyPath, + "ssh-key", + defaultBroOptions.sshHostKeyPath, + "ssh host key path", + ) + + fs.BoolVar( + &c.dev, + "dev", + defaultBroOptions.dev, + "enable dev mode and connect to gnodev for realtime update", + ) + + fs.StringVar( + &c.devRemote, + "dev-remote", + defaultBroOptions.devRemote, + "dev endpoint, if empty will default to `ws://:8888`", + ) + + fs.BoolVar( + &c.banner, + "banner", + defaultBroOptions.banner, + "if enabled, display a banner", + ) + + fs.BoolVar( + &c.readonly, + "readonly", + defaultBroOptions.readonly, + "readonly mode, no commands allowed", + ) +} + +func execBrowser(cfg *broCfg, args []string, cio commands.IO) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + home := gnoenv.HomeDir() + + var address string + var kb keys.Keybase + if cfg.defaultAccount != "" { + address = cfg.defaultAccount + + var err error + kb, err = keys.NewKeyBaseFromDir(home) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + } else { + // create a inmemory keybase + kb = keys.NewInMemory() + kb.CreateAccount(integration.DefaultAccount_Name, integration.DefaultAccount_Seed, "", "", 0, 0) + address = integration.DefaultAccount_Name + } + + signer, err := getSignerForAccount(cio, address, kb, cfg) + if err != nil { + return fmt.Errorf("unable to get signer for account %q: %w", address, err) + } + + cl, err := client.NewHTTPClient(cfg.remote) + if err != nil { + return fmt.Errorf("unable to create http client for %q: %w", cfg.remote, err) + } + + gnocl := &gnoclient.Client{ + RPCClient: cl, + Signer: signer, + } + + var path string + switch { + case len(args) > 0: + path = strings.TrimSpace(args[0]) + path = strings.TrimPrefix(path, gnoPrefix) + case cfg.defaultRealm != "": + path = strings.TrimLeft(cfg.defaultRealm, gnoPrefix) + } + + bcfg := browser.DefaultConfig() + bcfg.Readonly = cfg.readonly + bcfg.Renderer = lipgloss.DefaultRenderer() + bcfg.URLDefaultValue = path + bcfg.URLPrefix = gnoPrefix + bcfg.URLPrefix = gnoPrefix + + if cfg.sshListener == "" { + if cfg.banner { + bcfg.Banner = NewGnoLandBanner() + } + + return runLocal(ctx, gnocl, cfg, bcfg, cio) + } + + return runServer(ctx, gnocl, cfg, bcfg, cio) +} + +func runLocal(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg browser.Config, io commands.IO) error { + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) + defer cancel() + + model := browser.New(bcfg, gnocl) + p := tea.NewProgram(model, + tea.WithContext(ctx), + tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer" + tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel + ) + + var errgs errgroup.Group + + if cfg.dev { + devpoint, err := getDevEndpoint(cfg) + if err != nil { + return fmt.Errorf("unable to parse dev endpoint: %w", err) + } + + var devcl browser.DevClient + devcl.Handler = func(typ events.Type, data any) error { + switch typ { + case events.EvtReload, events.EvtReset, events.EvtTxResult: + p.Send(browser.RefreshRealm()) + default: + } + + return nil + } + + errgs.Go(func() error { + defer cancel() + + if err := devcl.Run(ctx, devpoint, nil); err != nil { + return fmt.Errorf("dev connection failed: %w", err) + } + + return nil + }) + } + + errgs.Go(func() error { + defer cancel() + + _, err := p.Run() + return err + }) + + if err := errgs.Wait(); err != nil && !errors.Is(err, context.Canceled) { + return err + } + + io.Println("Bye!") + return nil +} + +func runServer(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg browser.Config, io commands.IO) error { + // setup logger + charmlogger := charmlog.New(io.Out()) + charmlogger.SetLevel(charmlog.DebugLevel) + logger := slog.New(charmlogger) + + teaHandler := func(s ssh.Session) (tea.Model, []tea.ProgramOption) { + shortid := fmt.Sprintf("%.10s", s.Context().SessionID()) + + bcfgCopy := bcfg // copy config + + bcfgCopy.Logger = logger.WithGroup(shortid) + bcfgCopy.Renderer = bubbletea.MakeRenderer(s) + + if cfg.banner { + bcfgCopy.Banner = NewGnoLandBanner() + } + + pval := s.Context().Value("path") + if path, ok := pval.(string); ok && len(path) > 0 { + // Erase banner on specifc command + bcfgCopy.Banner = browser.ModelBanner{} + // Set up url + bcfgCopy.URLDefaultValue = path + } + + bcfgCopy.Logger.Info("session started", + "time", time.Now(), + "path", bcfgCopy.URLDefaultValue, + "sid", s.Context().SessionID(), + "user", s.User()) + model := browser.New(bcfgCopy, gnocl) + + return model, []tea.ProgramOption{ + tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer" + tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel + } + } + + sshaddr, err := net.ResolveTCPAddr("", cfg.sshListener) + if err != nil { + return fmt.Errorf("unable to resolve address: %w", err) + } + + s, err := wish.NewServer( + wish.WithAddress(sshaddr.String()), + wish.WithHostKeyPath(cfg.sshHostKeyPath), + wish.WithMiddleware( + bubbletea.Middleware(teaHandler), + activeterm.Middleware(), // ensure PTY + ValidatePathCommandMiddleware(bcfg.URLPrefix), + logging.StructuredMiddlewareWithLogger( + charmlogger, charmlog.DebugLevel, + ), + // XXX: add ip throttler + ), + ) + + var errgs errgroup.Group + + errgs.Go(func() error { + logger.Info("starting SSH server", "addr", sshaddr.String()) + return s.ListenAndServe() + }) + + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) + defer cancel() + + errgs.Go(func() error { + <-ctx.Done() + + logger.Info("stopping SSH server... (5s timeout)") + + sctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + return s.Shutdown(sctx) + }) + + if err := errgs.Wait(); err != nil && !errors.Is(err, ssh.ErrServerClosed) { + return err + } + + io.Println("Bye!") + return nil +} + +func getDevEndpoint(cfg *broCfg) (string, error) { + var err error + + // use remote address as default + host, port := cfg.remote, "8888" + if cfg.devRemote != "" { + // if any dev endpoint as been set, fallback on this + host, port, err = net.SplitHostPort(cfg.devRemote) + if err != nil { + return "", fmt.Errorf("unable to parse dev endpoint: %w", err) + } + } + + // ensure having a (any) protocol scheme + if !strings.Contains(host, "://") { + host = "http://" + host + } + + // parse full host including port + devpoint, err := url.Parse(host) + if err != nil { + return "", fmt.Errorf("unable to construct devaddr: %w", err) + } + + host, _, _ = net.SplitHostPort(devpoint.Host) + if port != "" { + devpoint.Host = host + ":" + port + } else { + devpoint.Host = host + } + + switch devpoint.Scheme { + case "ws", "wss": // already good + case "https": + devpoint.Scheme = "wss" + default: + devpoint.Scheme = "ws" + } + devpoint.Path = "_events" + + return devpoint.String(), nil +} + +func getSignerForAccount(io commands.IO, address string, kb keys.Keybase, cfg *broCfg) (gnoclient.Signer, error) { + var signer gnoclient.SignerFromKeybase + + signer.Keybase = kb + signer.Account = address + signer.ChainID = cfg.chainID + + if ok, err := kb.HasByNameOrAddress(address); !ok || err != nil { + if err != nil { + return nil, fmt.Errorf("invalid name: %w", err) + } + + return nil, fmt.Errorf("unknown name/address: %q", address) + } + + // try empty password first + if _, err := kb.ExportPrivKeyUnsafe(address, ""); err != nil { + prompt := fmt.Sprintf("[%.10s] Enter password:", address) + signer.Password, err = io.GetPassword(prompt, true) + if err != nil { + return nil, fmt.Errorf("error while reading password: %w", err) + } + + if _, err := kb.ExportPrivKeyUnsafe(address, signer.Password); err != nil { + return nil, fmt.Errorf("invalid password: %w", err) + } + } + + return signer, nil +} + +func ValidatePathCommandMiddleware(pathPrefix string) wish.Middleware { + return func(next ssh.Handler) ssh.Handler { + return func(s ssh.Session) { + switch cmd := s.Command(); len(cmd) { + case 0: // ok + next(s) + return + case 1: // check for valid path + path := cmd[0] + if strings.HasPrefix(path, pathPrefix) && filepath.Clean(path) == path { + s.Context().SetValue("path", path) + next(s) + return + } + + fmt.Fprintln(s.Stderr(), "provided path is invalid") + default: + fmt.Fprintln(s.Stderr(), "too many arguments") + } + + s.Exit(1) + } + } +} diff --git a/contribs/gnodev/cmd/gnodev/accounts.go b/contribs/gnodev/cmd/gnodev/accounts.go index b263cc44f70..95c2c3efffc 100644 --- a/contribs/gnodev/cmd/gnodev/accounts.go +++ b/contribs/gnodev/cmd/gnodev/accounts.go @@ -10,6 +10,7 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/address" "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/std" @@ -50,7 +51,7 @@ func (va varPremineAccounts) String() string { func generateBalances(bk *address.Book, cfg *devCfg) (gnoland.Balances, error) { bls := gnoland.NewBalances() - premineBalance := std.Coins{std.NewCoin("ugnot", 10e12)} + premineBalance := std.Coins{std.NewCoin(ugnot.Denom, 10e12)} entries := bk.List() diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index 53973b15fbf..80e9867ab27 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -2,32 +2,54 @@ module github.com/gnolang/gno/contribs/gnodev go 1.22 -toolchain go1.22.4 - replace github.com/gnolang/gno => ../.. require ( - github.com/charmbracelet/lipgloss v0.9.1 - github.com/charmbracelet/log v0.3.1 + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.26.6 + github.com/charmbracelet/glamour v0.7.0 + github.com/charmbracelet/lipgloss v0.11.0 + github.com/charmbracelet/log v0.4.0 + github.com/charmbracelet/ssh v0.0.0-20240604154955-a40c6a0d028f + github.com/charmbracelet/wish v1.4.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gnolang/gno v0.0.0-00010101000000-000000000000 github.com/gorilla/websocket v1.5.3 + github.com/lrstanley/bubblezone v0.0.0-20240624011428-67235275f80c + github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.15.2 + github.com/sahilm/fuzzy v0.1.1 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 + golang.org/x/sync v0.7.0 golang.org/x/term v0.22.0 ) require ( dario.cat/mergo v1.0.0 // indirect + github.com/alecthomas/chroma/v2 v2.8.0 // indirect + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/charmbracelet/keygen v0.5.0 // indirect + github.com/charmbracelet/x/ansi v0.1.2 // indirect + github.com/charmbracelet/x/conpty v0.1.0 // indirect + github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 // indirect + github.com/charmbracelet/x/input v0.1.2 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/termios v0.1.0 // indirect + github.com/charmbracelet/x/windows v0.1.2 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect + github.com/creack/pty v1.1.21 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -35,6 +57,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect @@ -42,9 +65,13 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/muesli/reflow v0.3.0 // indirect + github.com/microcosm-cc/bluemonday v1.0.25 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/peterbourgon/ff/v3 v3.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -54,6 +81,9 @@ require ( github.com/rs/cors v1.11.0 // indirect github.com/rs/xid v1.5.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.5.4 // indirect + github.com/yuin/goldmark-emoji v1.0.2 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.etcd.io/bbolt v1.3.10 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index 2b5c964a014..19fb24d2ebb 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -1,8 +1,20 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= +github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd h1:js1gPwhcFflTZ7Nzl7WHaOTlTr5hIrR4n1NM4v9n4Kw= @@ -30,14 +42,42 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtE github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= -github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= -github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw= -github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= +github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= +github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= +github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= +github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc= +github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8= +github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= +github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= +github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= +github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/ssh v0.0.0-20240604154955-a40c6a0d028f h1:DnNHMcvpjh51pFVuYCxf+pVNdfZ3w51gGAtDiuVmFEk= +github.com/charmbracelet/ssh v0.0.0-20240604154955-a40c6a0d028f/go.mod h1:LmMZag2g7ILMmWtDmU7dIlctUopwmb73KpPzj0ip1uk= +github.com/charmbracelet/wish v1.4.0 h1:pL1uVP/YuYgJheHEj98teZ/n6pMYnmlZq/fcHvomrfc= +github.com/charmbracelet/wish v1.4.0/go.mod h1:ew4/MjJVfW/akEO9KmrQHQv1F7bQRGscRMrA+KtovTk= +github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= +github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/input v0.1.2 h1:QJAZr33eOhDowkkEQ24rsJy4Llxlm+fRDf/cQrmqJa0= +github.com/charmbracelet/x/input v0.1.2/go.mod h1:LGBim0maUY4Pitjn/4fHnuXb4KirU3DODsyuHuXdOyA= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k= +github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U= +github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= +github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/cosmos/ledger-cosmos-go v0.13.3 h1:7ehuBGuyIytsXbd4MP43mLeoN2LTOEnk5nvue4rK+yM= github.com/cosmos/ledger-cosmos-go v0.13.3/go.mod h1:HENcEP+VtahZFw38HZ3+LS3Iv5XV6svsnkk9vdJtLr8= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -49,6 +89,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -85,6 +129,8 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= @@ -99,6 +145,8 @@ github.com/gotuna/gotuna v0.6.0 h1:N1lQKXEi/lwRp8u3sccTYLhzOffA4QasExz/1M5Riws= github.com/gotuna/gotuna v0.6.0/go.mod h1:F/ecRt29ChB6Ycy1AFIBpBiMNK0j7Heq+gFbLWquhjc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -108,17 +156,30 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/lrstanley/bubblezone v0.0.0-20240624011428-67235275f80c h1:hu82xYs8yOIM1TSq+L5VIZeRsHVROpe3gL0qscUlXJA= +github.com/lrstanley/bubblezone v0.0.0-20240624011428-67235275f80c/go.mod h1:fMHACHXouhQO+NLAFvHEeKdVSzG7L/O1khqsvswCTmk= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= +github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= @@ -126,6 +187,8 @@ github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1n github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= @@ -154,12 +217,21 @@ github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= +github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= +github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= @@ -218,6 +290,7 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/contribs/gnodev/pkg/browser/client_dev.go b/contribs/gnodev/pkg/browser/client_dev.go new file mode 100644 index 00000000000..3d63b3abba7 --- /dev/null +++ b/contribs/gnodev/pkg/browser/client_dev.go @@ -0,0 +1,113 @@ +package browser + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gorilla/websocket" +) + +const MaxBackoff = time.Second * 20 + +var ErrHandlerNotSet = errors.New("handler not set") + +type DevClient struct { + Logger *slog.Logger + Handler func(typ events.Type, data any) error + + conn *websocket.Conn +} + +func (c *DevClient) Run(ctx context.Context, addr string, header http.Header) error { + if c.Handler == nil { + return ErrHandlerNotSet + } + + if c.Logger == nil { + c.Logger = log.NewNoopLogger() + } + + for ctx.Err() == nil { + if err := c.dialBackoff(ctx, addr, nil); err != nil { + return err + } + + c.Logger.Info("connected to server", "addr", addr) + + err := c.handleEvents(ctx) + if err == nil { + return nil + } + + var closeError *websocket.CloseError + if errors.As(err, &closeError) { + c.Logger.Error("connection has been closed, reconnecting...", "err", closeError) + continue + } + + return fmt.Errorf("unexpected error: %w", err) + } + + return context.Cause(ctx) +} + +func (c *DevClient) dialBackoff(ctx context.Context, addr string, header http.Header) error { + dialer := websocket.DefaultDialer + backoff := time.Second + for { + var err error + + c.Logger.Debug("connecting to dev events endpoint", addr, "addr") + c.conn, _, err = dialer.DialContext(ctx, addr, header) + + if ctx.Err() != nil { + return context.Cause(ctx) + } + + if err == nil { + return nil + } + + switch { + case backoff > MaxBackoff: + backoff = MaxBackoff + case backoff < MaxBackoff: + backoff *= 2 + default: + } + + c.Logger.Info("could not connect to server", "err", err, "next_attempt", backoff) + select { + case <-ctx.Done(): + return context.Cause(ctx) + case <-time.After(backoff): + } + } +} + +func (c *DevClient) handleEvents(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + go func() { + <-ctx.Done() + c.conn.Close() + }() + + for { + var evt emitter.EventJSON + if err := c.conn.ReadJSON(&evt); err != nil { + return fmt.Errorf("unable to read json event: %w", err) + } + + if err := c.Handler(evt.Type, evt.Data); err != nil { + return fmt.Errorf("unable to handle event: %w", err) + } + } +} diff --git a/contribs/gnodev/pkg/browser/client_node.go b/contribs/gnodev/pkg/browser/client_node.go new file mode 100644 index 00000000000..2ffa26aa08f --- /dev/null +++ b/contribs/gnodev/pkg/browser/client_node.go @@ -0,0 +1,124 @@ +package browser + +import ( + "errors" + "fmt" + "log/slog" + "regexp" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/gnoclient" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/amino" +) + +var ( + ErrInternalError = errors.New("internal error") + ErrRenderNotFound = errors.New("render not found") +) + +type NodeClient struct { + base gnoclient.BaseTxCfg + client *gnoclient.Client + logger *slog.Logger +} + +func NewNodeClient(logger *slog.Logger, base gnoclient.BaseTxCfg, client *gnoclient.Client) *NodeClient { + return &NodeClient{ + base: base, + client: client, + logger: logger, + } +} + +func (ncl *NodeClient) Call(path, call string) ([]byte, error) { + method, args, err := parseMethodToArgs(call) + if err != nil { + return nil, fmt.Errorf("unable to parse method/args: %w", err) + } + + if len(args) == 0 { + args = nil + } + + infos, err := ncl.client.Signer.Info() + if err != nil { + return nil, fmt.Errorf("unable to get signer infos: %w", err) + } + + cm, err := ncl.client.Call(ncl.base, vm.MsgCall{ + Caller: infos.GetAddress(), + PkgPath: path, + Func: method, + Args: args, + }) + if err != nil { + return nil, err + } + + if cm.CheckTx.Error != nil { + return nil, fmt.Errorf("check error: %w", err) + } + + if cm.DeliverTx.Error != nil { + return nil, fmt.Errorf("delivry error: %w", err) + } + + return cm.DeliverTx.Data, nil +} + +func (ncl *NodeClient) Funcs(path string) (vm.FunctionSignatures, error) { + res, err := ncl.client.Query(gnoclient.QueryCfg{ + Path: "vm/qfuncs", + Data: []byte(path), + }) + if err != nil { + return nil, err + } + + if err := res.Response.Error; err != nil { + return nil, err + } + + var fsigs vm.FunctionSignatures + if err := amino.UnmarshalJSON(res.Response.Data, &fsigs); err != nil { + return nil, fmt.Errorf("unable to unmarshal response: %w", err) + } + + return fsigs, nil +} + +func (ncl *NodeClient) Render(path, args string) ([]byte, error) { + data, res, err := ncl.client.Render(path, args) + if err != nil { + return nil, err + } + if err := res.Response.Error; err != nil { + return nil, err + } + + return []byte(data), nil +} + +var reMethod = regexp.MustCompile(`([^(]+)\(([^)]*)\)`) + +func parseMethodToArgs(call string) (method string, args []string, err error) { + matches := reMethod.FindStringSubmatch(call) + if len(matches) == 0 { + return "", nil, fmt.Errorf("invalid call: %w", err) + } + + method = matches[1] + sargs := matches[2] + if sargs == "" { + return method, args, err + } + + // Splitting arguments by comma + args = strings.Split(sargs, ",") + for i, arg := range args { + args[i] = strings.Trim(strings.TrimSpace(arg), "\"") + } + + return method, args, err +} diff --git a/contribs/gnodev/pkg/browser/list_model.go b/contribs/gnodev/pkg/browser/list_model.go new file mode 100644 index 00000000000..db4656fbf24 --- /dev/null +++ b/contribs/gnodev/pkg/browser/list_model.go @@ -0,0 +1,154 @@ +package browser + +import ( + "fmt" + "io" + "sort" + "strings" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/sahilm/fuzzy" +) + +var ( + listItemStyle = lipgloss.NewStyle().PaddingLeft(4) + listSelectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) + listPaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) +) + +type FuncListModel struct { + list.Model + items []list.Item +} + +func (m *FuncListModel) SetItems(items []list.Item) { + m.Model.SetItems(items) + m.items = items +} + +func (m FuncListModel) Update(msg tea.Msg) (FuncListModel, tea.Cmd) { + var cmd tea.Cmd + m.Model, cmd = m.Model.Update(msg) + return m, cmd +} + +func (m *FuncListModel) OriginItems() []list.Item { + return m.items +} + +func (m *FuncListModel) Erase() { + m.Model.SetItems([]list.Item{}) +} + +func (m *FuncListModel) Reset() { + m.Model.SetItems(m.items) +} + +func (m *FuncListModel) FilterItems(pattern string) { + if pattern == "" { + m.Reset() + return + } + + i := strings.IndexRune(pattern, '(') + if i > 0 { + pattern = pattern[:i] + } + + data := make([]string, len(m.items)) + for i, item := range m.items { + data[i] = item.FilterValue() + } + + ranks := fuzzy.Find(pattern, data) + sort.Stable(ranks) + if len(ranks) > 0 && i > 0 { + m.Model.SetItems([]list.Item{m.items[ranks[0].Index]}) + return + } + + items := make([]list.Item, len(ranks)) + for i, r := range ranks { + items[i] = m.items[r.Index] + } + + m.Model.SetItems(items) +} + +type itemFunc vm.FunctionSignature + +func (i itemFunc) Name() string { return i.FuncName } +func (i itemFunc) Title() string { return i.Name() } +func (i itemFunc) Description() string { return i.FuncName } +func (i itemFunc) FilterValue() string { return i.FuncName } + +type itemFuncsDelegate struct{} + +func (d itemFuncsDelegate) Height() int { return 1 } +func (d itemFuncsDelegate) Spacing() int { return 0 } +func (d itemFuncsDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d itemFuncsDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + fun, ok := listItem.(itemFunc) + if !ok { + return + } + + maxw := m.Width() - 10 + + var proto strings.Builder + fmt.Fprintf(&proto, "%s(", fun.FuncName) + for j, param := range fun.Params { + if j != 0 { + fmt.Fprint(&proto, ", ") + } + + fmt.Fprintf(&proto, "%s %s", param.Name, param.Type) + } + fmt.Fprint(&proto, ")") + + switch len(fun.Results) { + case 0: // none + case 1: + fmt.Fprintf(&proto, " %s", fun.Results[0].Type) + default: + fmt.Fprint(&proto, " (") + for j, res := range fun.Results { + if j != 0 { + fmt.Fprint(&proto, ", ") + } + + fmt.Fprint(&proto, res.Type) + } + fmt.Fprint(&proto, ")") + } + + fn := listItemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return listSelectedItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + str := proto.String() + if len(str) > maxw { + str = str[:maxw-3] + "..." + } + + fmt.Fprint(w, fn(str)) +} + +func newFuncList() FuncListModel { + l := list.New([]list.Item{}, &itemFuncsDelegate{}, 0, 0) + l.SetShowStatusBar(false) + l.SetFilteringEnabled(true) + l.SetShowHelp(false) + l.SetShowPagination(false) + l.Styles.PaginationStyle = listPaginationStyle + return FuncListModel{ + Model: l, + items: l.Items(), + } +} diff --git a/contribs/gnodev/pkg/browser/model.go b/contribs/gnodev/pkg/browser/model.go new file mode 100644 index 00000000000..bdd7eac9c82 --- /dev/null +++ b/contribs/gnodev/pkg/browser/model.go @@ -0,0 +1,580 @@ +package browser + +import ( + "bytes" + clist "container/list" + "errors" + "fmt" + "log/slog" + "path/filepath" + "regexp" + "strings" + + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + zone "github.com/lrstanley/bubblezone" + "github.com/muesli/reflow/wordwrap" + + "github.com/gnolang/gno/gno.land/pkg/gnoclient" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/log" +) + +var promptStyle = func(r *lipgloss.Renderer) lipgloss.Style { + return r.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#dd7878")) +} + +var ErrEmptyRenderer = errors.New("empty rendrer") + +type Config struct { + URLPrefix string + URLDefaultValue string + Logger *slog.Logger + Renderer *lipgloss.Renderer + Readonly bool + Banner ModelBanner +} + +const DefaultGnoLandPrefix = "gno.land/" + +func DefaultConfig() Config { + return Config{ + Logger: log.NewNoopLogger(), + URLPrefix: DefaultGnoLandPrefix, + Renderer: lipgloss.DefaultRenderer(), + URLDefaultValue: "gnoland/home", + } +} + +type model struct { + render *lipgloss.Renderer + client *NodeClient + logger *slog.Logger + + // misc + banner ModelBanner + bannerDiscarded bool + + // Viewport + zone *zone.Manager + ready bool + viewport viewport.Model + height, width int + readonly bool + messageDisplay bool + + // Nav + taskLoader LoaderModel + + pageurls map[string]string + history *clist.List + current *clist.Element + + // Url + urlInput textinput.Model + urlPrefix string + + // Commands + listFuncs FuncListModel + commandInput textinput.Model + commandFocus bool +} + +func initURLInput(prefix string, r *lipgloss.Renderer) textinput.Model { + ti := textinput.New() + ti.Placeholder = "r/gnoland/blog" // XXX: Use as example, customize this ? + ti.Focus() + ti.CharLimit = 156 + ti.PromptStyle = promptStyle(r) + ti.Prompt = prefix + "/" + + return ti +} + +func initCommandInput(r *lipgloss.Renderer) textinput.Model { + ti := textinput.New() + ti.Placeholder = "" + ti.CharLimit = 156 + ti.PromptStyle = promptStyle(r) + ti.Prompt = "> " + + return ti +} + +func New(cfg Config, client *gnoclient.Client) tea.Model { + renderer := lipgloss.DefaultRenderer() + if cfg.Renderer != nil { + renderer = cfg.Renderer + } + + // Setup url input + urlinput := initURLInput(cfg.URLPrefix, renderer) + + path := cleanupRealmPath(cfg.URLPrefix, cfg.URLDefaultValue) + urlinput.SetValue(path) + + // Setup cmd input + cmdinput := initCommandInput(renderer) + + // XXX: Customize this + base := gnoclient.BaseTxCfg{ + GasFee: "1000000ugnot", + GasWanted: 2000000, + } + + nodeclient := NewNodeClient(cfg.Logger, base, client) + return &model{ + logger: cfg.Logger, + render: cfg.Renderer, + readonly: cfg.Readonly, + client: nodeclient, + taskLoader: newLoaderModel(), + + banner: cfg.Banner, + bannerDiscarded: cfg.Banner.Empty(), + + urlInput: urlinput, + urlPrefix: cfg.URLPrefix, + + commandInput: cmdinput, + listFuncs: newFuncList(), + + zone: zone.New(), + pageurls: map[string]string{}, + history: clist.New(), + } +} + +func (m model) Init() tea.Cmd { + m.history.Init() + return m.banner.Init() +} + +type fetchRealmMsg struct { + realmPath string +} + +func FetchRealm(path string) tea.Cmd { + return func() tea.Msg { return fetchRealmMsg{path} } +} + +func RefreshRealm() tea.Cmd { + return func() tea.Msg { return fetchRealmMsg{""} } +} + +type renderUpdateMsg struct { + Render []byte + Funcs vm.FunctionSignatures + Error error +} + +func (m *model) RenderUpdate(path string) tea.Cmd { + return func() tea.Msg { + var msg renderUpdateMsg + var err error + msg.Render, err = m.fetchRenderView(path) + if err != nil { + msg.Error = fmt.Errorf("unable to fetch view: %w", err) + return msg + } + + msg.Funcs, err = m.fetchFuncsList(path) + if err != nil { + msg.Error = fmt.Errorf("unable to fetch function list: %w", err) + return msg + } + + return msg + } +} + +type execCommandRequestMsg struct { + Path string + Command string +} + +func (m *model) ExecCommandRequest(path, command string) tea.Cmd { + return func() tea.Msg { + return execCommandRequestMsg{path, command} + } +} + +type execCommandMsg struct { + Response []byte + Error error +} + +func (m *model) ExecCommand(path, command string) tea.Cmd { + return func() tea.Msg { + res, err := m.client.Call(path, command) + return execCommandMsg{res, err} + } +} + +func (m *model) ExtendCommandInput() bool { + if !m.commandInput.Focused() { + return false + } + + if item, ok := m.listFuncs.SelectedItem().(itemFunc); ok { + var value string + if len(item.Params) > 0 { + value = item.Title() + "(" + } else { + value = item.Title() + "()" + } + + currentValue := m.commandInput.Value() + if len(value) > len(currentValue) && strings.HasPrefix(value, currentValue) { + m.commandInput.SetValue(value) + return true + } + + // Put cursor at the end + m.commandInput.CursorEnd() + } + + return false +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case fetchRealmMsg: + if msg.realmPath != "" { + return m, tea.Sequence(m.taskLoader.Add(1), m.moveToRealm(msg.realmPath)) + } + + // If no realm path is given simply refresh the current realm + path := m.getCurrentPath() + m.logger.Info("rendering realm", "path", path) + + return m, tea.Sequence(m.taskLoader.Add(1), m.RenderUpdate(path)) + + case execCommandRequestMsg: + m.logger.Info("requesting command", "path", msg.Path, "cmd", msg.Command) + cmd = m.ExecCommand(msg.Path, msg.Command) + return m, tea.Sequence(m.taskLoader.Add(1), cmd) + + case execCommandMsg: + m.taskLoader.Done() + + // If any error, display it as message. + if msg.Error != nil { + m.logger.Warn("command exec", "error", msg.Error) + + content := wordwrap.NewWriter(m.viewport.Width) + fmt.Fprint(content, msg.Error.Error()) + fmt.Fprintf(content, "\n\npress [enter] to dismiss error\n") + m.viewport.SetContent(content.String()) + m.messageDisplay = true + return m, nil + } + + // If any response, display it as message. + if res := bytes.TrimSpace(msg.Response); len(res) > 0 { + m.logger.Info("command exec", "res", string(res)) + + content := wordwrap.NewWriter(m.viewport.Width) + content.Write(res) + fmt.Fprintf(content, "\n\npress [enter] to dismiss message\n") + m.viewport.SetContent(content.String()) + m.messageDisplay = true + return m, nil + } + + // If no error or empty response is returned, simply refresh the page. + m.messageDisplay = false + return m, RefreshRealm() + + case renderUpdateMsg: + m.taskLoader.Done() + + var content string + if err := msg.Error; err != nil { + m.logger.Warn("render", "error", msg.Error) + // Write error to the frame + content = fmt.Sprintf("ERROR: %s", err.Error()) + } else { + content = string(m.findAndMarkURLs(msg.Render)) + } + + if len(msg.Funcs) > 0 { + items := make([]list.Item, 0, len(msg.Funcs)) + for _, fun := range msg.Funcs { + if fun.FuncName != "Render" { + items = append(items, itemFunc(fun)) + } + } + m.listFuncs.SetItems(items) + m.listFuncs.FilterItems(m.commandInput.Value()) + + // Update funcs list + m.listFuncs.Title = m.urlInput.Value() + m.listFuncs.SetSize(m.viewport.Width, 7) + } + + m.viewport.SetContent(content) + return m, cmd + + case SpinnerTickMsg: + if m.taskLoader.Active() { + m.taskLoader, cmd = m.taskLoader.Update(msg) + } + + case tea.MouseMsg: + cmd = m.updateMouse(msg) + + // Fallback on viewport + if cmd == nil { + m.viewport, cmd = m.viewport.Update(msg) + } + + return m, cmd + + case tea.KeyMsg: + cmd = m.updateKey(msg) + m.listFuncs.FilterItems(m.commandInput.Value()) + if !m.readonly && cmd == nil { + m.listFuncs, cmd = m.listFuncs.Update(msg) + } + + // Fallback on list funcs update + if cmd == nil { + m.viewport, cmd = m.viewport.Update(msg) + } + + // Fallback on viewport update + return m, cmd + + case tea.WindowSizeMsg: + m.width = msg.Width + + headerHeight := lipgloss.Height(m.headerView()) + footerHeight := lipgloss.Height(m.footerView()) + verticalMarginHeight := headerHeight + footerHeight + + if !m.ready { + m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) + m.viewport.YPosition = headerHeight + m.viewport.MouseWheelEnabled = true + m.viewport.MouseWheelDelta = 1 + m.ready = true + m.viewport.YPosition = headerHeight + 1 + + if value := m.urlInput.Value(); value != "" { + cmd = RefreshRealm() + m.updateHistory() + } + } else { + m.viewport.Width = msg.Width + m.viewport.Height = msg.Height - verticalMarginHeight + } + + m.height = m.viewport.Height + if !m.urlInput.Focused() && len(m.listFuncs.Items()) > 0 { + m.viewport.Height = m.height - lipgloss.Height(m.listFuncsView()) + } + + return m, cmd + } + + // Update other models + cmds := []tea.Cmd{cmd} + + if !m.bannerDiscarded { + var bannerCmd tea.Cmd + m.banner, bannerCmd = m.banner.Update(msg) + cmds = append(cmds, bannerCmd) + } + + var viewCmd tea.Cmd + m.viewport, viewCmd = m.viewport.Update(msg) + cmds = append(cmds, viewCmd) + + var funcCmd tea.Cmd + m.listFuncs, funcCmd = m.listFuncs.Update(msg) + cmds = append(cmds, funcCmd) + + return m, tea.Batch(cmds...) +} + +func (m *model) updateKey(msg tea.KeyMsg) tea.Cmd { + var cmd tea.Cmd + if !m.bannerDiscarded { + switch key := msg.String(); key { + case "ctrl+c": + return tea.Quit + case "enter": + m.bannerDiscarded = true + } + // Discard other input while banner is active + return nil + } + + switch msg.String() { + case "alt+down": + if m.urlInput.Focused() && !m.readonly { + m.urlInput.Blur() + cmd = m.commandInput.Focus() + m.commandFocus = true + } + case "alt+up": + if m.commandInput.Focused() { + m.commandInput.Blur() + cmd = m.urlInput.Focus() + m.commandFocus = false + } + case "tab": + if m.commandInput.Focused() { + m.ExtendCommandInput() + } + case "alt+r": + cmd = RefreshRealm() + case "enter": + // Update command on focus + if m.commandInput.Focused() && !m.messageDisplay { + if len(m.listFuncs.Items()) == 1 { + path := m.getCurrentPath() + cmd = m.ExecCommand(path, m.commandInput.Value()) + } else { + m.ExtendCommandInput() + } + + break + } + + // Update url on focus + if m.messageDisplay || m.urlInput.Focused() { + m.listFuncs.Erase() + + cmd = m.moveToRealm(m.urlInput.Value()) + if m.current.Value.(string) != m.urlInput.Value() { + m.updateHistory() + } + + // Discard message + m.messageDisplay = false + } + + case "ctrl+c", "esc": + return tea.Quit + default: + // handle url input + if m.urlInput.Focused() { + m.urlInput, cmd = m.urlInput.Update(msg) + } + + if m.commandInput.Focused() { + // handle command input + m.commandInput, cmd = m.commandInput.Update(msg) + } + } + + return cmd +} + +func (m *model) updateMouse(msg tea.MouseMsg) tea.Cmd { + if msg.Action != tea.MouseActionRelease { + return nil + } + + var cmd tea.Cmd + + switch { + case m.zone.Get("prev_button").InBounds(msg): + if path, ok := m.moveHistoryBackward(); ok { + cmd = m.moveToRealm(path) + } + case m.zone.Get("next_button").InBounds(msg): + if path, ok := m.moveHistoryForward(); ok { + cmd = m.moveToRealm(path) + } + case m.zone.Get("home_button").InBounds(msg): + if cmd = m.moveToRealm("gno.land/r/gnoland/home"); cmd != nil { + m.updateHistory() + } + + case m.zone.Get("url_input").InBounds(msg): + m.commandInput.Blur() + cmd = m.urlInput.Focus() + m.commandFocus = false + case !m.readonly && m.zone.Get("command_input").InBounds(msg): + m.urlInput.Blur() + cmd = m.commandInput.Focus() + m.commandFocus = true + default: + for mark := range m.pageurls { + if !m.zone.Get(mark).InBounds(msg) { + continue + } + + if uri := m.pageurls[mark]; uri != "" { + if cmd = m.moveToRealm(uri); cmd != nil { + m.updateHistory() + break + } + } + } + } + + return cmd +} + +// realm path surrounded by ansi escape sequences +var reUrlPattern = regexp.MustCompile(`(?mU)\x1b[^m]*m(?:(?:https?://)?gno.land)?(/[^\s]+)\x1b[^m]*m`) + +func (m model) findAndMarkURLs(body []byte) []byte { + var buf bytes.Buffer + lastIndex := 0 + + indexes := reUrlPattern.FindAllSubmatchIndex(body, -1) + for i, loc := range indexes { + match := string(body[loc[0]:loc[1]]) + uri := string(body[loc[2]:loc[3]]) + markid := fmt.Sprintf("url_%d", i) + + // Write bytes before match + buf.Write(body[lastIndex:loc[0]]) + + // Write quoted URL + buf.WriteString(m.zone.Mark(markid, match)) + m.pageurls[markid] = uri + lastIndex = loc[1] + } + // Write remaining bytes + buf.Write(body[lastIndex:]) + + // Cleanup previous urls + for i := len(indexes); i < len(m.pageurls); i++ { + markid := fmt.Sprintf("url_%d", i) + delete(m.pageurls, markid) + } + + return buf.Bytes() +} + +func (m model) fetchFuncsList(path string) (view vm.FunctionSignatures, err error) { + rlmpath, _, _ := strings.Cut(path, ":") + funcs, err := m.client.Funcs(rlmpath) + if err != nil { + return nil, fmt.Errorf("unable to fetch Render: %w", err) + } + + return funcs, nil +} + +func (m *model) getCurrentPath() string { + path := strings.Trim(m.urlInput.Value(), "/") + if len(path) == 0 { + return m.urlPrefix + } + + return filepath.Join(m.urlPrefix, path) +} diff --git a/contribs/gnodev/pkg/browser/model_banner.go b/contribs/gnodev/pkg/browser/model_banner.go new file mode 100644 index 00000000000..ac983951dab --- /dev/null +++ b/contribs/gnodev/pkg/browser/model_banner.go @@ -0,0 +1,99 @@ +package browser + +import ( + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" +) + +type ModelBanner struct { + Banner string + + offset int + frameIndex int + frames [][]string + + fps time.Duration +} + +func NewModelBanner(fps time.Duration, frames []string) ModelBanner { + splited := make([][]string, len(frames)) + for i, frame := range frames { + lines := strings.Split(frame, "\n") + for j, line := range lines { + lines[j] = line + "\033[0m" + } + splited[i] = lines + } + + return ModelBanner{ + frames: splited, + fps: fps, + } +} + +func (m ModelBanner) Empty() bool { + return m.frames == nil +} + +type ( + tickBannerMsg struct{} + tickBannerOffsetMsg struct{} +) + +func (m ModelBanner) tick() tea.Cmd { + return tea.Tick(m.fps, func(_ time.Time) tea.Msg { + return tickBannerMsg{} + }) +} + +func (m ModelBanner) tickOffset() tea.Cmd { + return tea.Tick(time.Second/10, func(_ time.Time) tea.Msg { + return tickBannerOffsetMsg{} + }) +} + +func (m ModelBanner) Init() tea.Cmd { + if m.Empty() { + return nil + } + + return tea.Batch(m.tickOffset(), m.tick()) +} + +func (m ModelBanner) Update(msg tea.Msg) (ModelBanner, tea.Cmd) { + var cmd tea.Cmd + switch msg.(type) { + case tickBannerOffsetMsg: + frame := m.frames[m.frameIndex] + m.Banner = getFrameLinesOffset(frame, m.offset) + if m.offset < (len(frame) / 2) { + m.offset++ + cmd = m.tickOffset() + } + + case tickBannerMsg: + frame := m.frames[m.frameIndex] + m.Banner = getFrameLinesOffset(frame, m.offset) + m.frameIndex = (m.frameIndex + 1) % len(m.frames) // move to next frame + cmd = m.tick() + // XXX: handle window size + } + return m, cmd +} + +func (m ModelBanner) View() string { + return m.Banner +} + +func getFrameLinesOffset(lines []string, offset int) string { + middle := len(lines) / 2 + if offset < middle { + start := middle - min(middle, offset) + end := middle + min(middle, offset) + lines = lines[start:end] + } + + return strings.Join(lines, "\n") +} diff --git a/contribs/gnodev/pkg/browser/model_nav.go b/contribs/gnodev/pkg/browser/model_nav.go new file mode 100644 index 00000000000..6936f413d94 --- /dev/null +++ b/contribs/gnodev/pkg/browser/model_nav.go @@ -0,0 +1,73 @@ +package browser + +import ( + "fmt" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" +) + +func (m *model) moveToRealm(realm string) tea.Cmd { + path := cleanupRealmPath(m.urlPrefix, realm) + + // Set uri input + m.urlInput.SetValue(path) + m.urlInput.CursorEnd() + + // return command update + return tea.Sequence(RefreshRealm(), m.urlInput.Focus()) +} + +func (m *model) updateHistory() { + v := m.urlInput.Value() + if m.history.Len() == 0 { + m.current = m.history.PushBack(v) + return + } + + m.current = m.history.InsertAfter(v, m.current) + for next := m.current.Next(); next != nil; { + m.history.Remove(next) + next = m.current.Next() + } +} + +func (m *model) moveHistoryForward() (string, bool) { + if next := m.current.Next(); next != nil { + m.current = next + return m.current.Value.(string), true + } + return "", false +} + +func (m *model) moveHistoryBackward() (string, bool) { + if prev := m.current.Prev(); prev != nil { + m.current = prev + return m.current.Value.(string), true + } + return "", false +} + +func (m model) fetchRenderView(path string) (view []byte, err error) { + rlmpath, args, _ := strings.Cut(path, ":") + res, err := m.client.Render(rlmpath, args) + if err != nil { + return nil, fmt.Errorf("unable to fetch Render: %w", err) + } + + r, err := glamour.NewTermRenderer( + glamour.WithStyles(CatppuccinStyleConfig), // XXX: use gno custom theme + glamour.WithWordWrap(m.viewport.Width), + ) + if err != nil { + return nil, fmt.Errorf("unable to get render view: %w", err) + } + + view, err = r.RenderBytes(res) + if err != nil { + return nil, fmt.Errorf("uanble to render markdown view: %w", err) + } + + return view, nil +} diff --git a/contribs/gnodev/pkg/browser/model_style.go b/contribs/gnodev/pkg/browser/model_style.go new file mode 100644 index 00000000000..f3c3a18fd96 --- /dev/null +++ b/contribs/gnodev/pkg/browser/model_style.go @@ -0,0 +1,239 @@ +package browser + +import "github.com/charmbracelet/glamour/ansi" + +const ( + defaultListIndent = 2 + defaultListLevelIndent = 4 + defaultMargin = 2 +) + +// Catpuccin style: https://github.com/catppuccin/catppuccin +// XXX: update this with `gno` colors scheme +var CatppuccinStyleConfig = ansi.StyleConfig{ + Document: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + BlockPrefix: "\n", + BlockSuffix: "\n", + Color: stringPtr("#cad3f5"), + }, + Margin: uintPtr(defaultMargin), + }, + BlockQuote: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr("#cad3f5"), + Italic: boolPtr(true), + }, + Indent: uintPtr(1), + }, + List: ansi.StyleList{ + LevelIndent: defaultListIndent, + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr("#cad3f5"), + }, + }, + }, + Heading: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + BlockSuffix: "\n", + Color: stringPtr("#cad3f5"), + Bold: boolPtr(true), + }, + }, + H1: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: " ", + Suffix: " ", + BackgroundColor: stringPtr("#f0c6c6"), + Color: stringPtr("#181926"), + Bold: boolPtr(true), + }, + }, + H2: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "● ", + Color: stringPtr("#f5a97f"), + }, + }, + H3: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "◉ ", + Color: stringPtr("#eed49f"), + }, + }, + H4: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "○ ", + Color: stringPtr("#a6da95"), + }, + }, + H5: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "◌ ", + Color: stringPtr("#7dc4e4"), + }, + }, + H6: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Prefix: "‣ ", + Color: stringPtr("#b7bdf8"), + }, + }, + Strikethrough: ansi.StylePrimitive{ + CrossedOut: boolPtr(true), + }, + Emph: ansi.StylePrimitive{ + Color: stringPtr("#cad3f5"), + Italic: boolPtr(true), + }, + Strong: ansi.StylePrimitive{ + Bold: boolPtr(true), + Color: stringPtr("#cad3f5"), + }, + HorizontalRule: ansi.StylePrimitive{ + Color: stringPtr("#6e738d"), + Format: "\n--------\n", + }, + Item: ansi.StylePrimitive{ + BlockPrefix: "• ", + }, + Enumeration: ansi.StylePrimitive{ + BlockPrefix: ". ", + Color: stringPtr("#cad3f5"), + }, + Task: ansi.StyleTask{ + StylePrimitive: ansi.StylePrimitive{}, + Ticked: "[✓] ", + Unticked: "[ ] ", + }, + Link: ansi.StylePrimitive{ + Color: stringPtr("#8aadf4"), + Underline: boolPtr(true), + }, + LinkText: ansi.StylePrimitive{ + Color: stringPtr("#b7bdf8"), + }, + Image: ansi.StylePrimitive{ + Color: stringPtr("#8aadf4"), + Underline: boolPtr(true), + }, + ImageText: ansi.StylePrimitive{ + Color: stringPtr("#b7bdf8"), + Format: "Image: {{.text}} →", + }, + Code: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr("#ee99a0"), + }, + }, + CodeBlock: ansi.StyleCodeBlock{ + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{ + Color: stringPtr("#1e2030"), + }, + Margin: uintPtr(defaultMargin), + }, + Chroma: &ansi.Chroma{ + Text: ansi.StylePrimitive{ + Color: stringPtr("#cad3f5"), + }, + Error: ansi.StylePrimitive{ + Color: stringPtr("#cad3f5"), + BackgroundColor: stringPtr("#ed8796"), + }, + Comment: ansi.StylePrimitive{ + Color: stringPtr("#6e738d"), + }, + CommentPreproc: ansi.StylePrimitive{ + Color: stringPtr("#8aadf4"), + }, + Keyword: ansi.StylePrimitive{ + Color: stringPtr("#c6a0f6"), + }, + KeywordReserved: ansi.StylePrimitive{ + Color: stringPtr("#c6a0f6"), + }, + KeywordNamespace: ansi.StylePrimitive{ + Color: stringPtr("#eed49f"), + }, + KeywordType: ansi.StylePrimitive{ + Color: stringPtr("#eed49f"), + }, + Operator: ansi.StylePrimitive{ + Color: stringPtr("#91d7e3"), + }, + Punctuation: ansi.StylePrimitive{ + Color: stringPtr("#939ab7"), + }, + Name: ansi.StylePrimitive{ + Color: stringPtr("#b7bdf8"), + }, + NameBuiltin: ansi.StylePrimitive{ + Color: stringPtr("#f5a97f"), + }, + NameTag: ansi.StylePrimitive{ + Color: stringPtr("#c6a0f6"), + }, + NameAttribute: ansi.StylePrimitive{ + Color: stringPtr("#eed49f"), + }, + NameClass: ansi.StylePrimitive{ + Color: stringPtr("#eed49f"), + }, + NameConstant: ansi.StylePrimitive{ + Color: stringPtr("#eed49f"), + }, + NameDecorator: ansi.StylePrimitive{ + Color: stringPtr("#f5bde6"), + }, + NameFunction: ansi.StylePrimitive{ + Color: stringPtr("#8aadf4"), + }, + LiteralNumber: ansi.StylePrimitive{ + Color: stringPtr("#f5a97f"), + }, + LiteralString: ansi.StylePrimitive{ + Color: stringPtr("#a6da95"), + }, + LiteralStringEscape: ansi.StylePrimitive{ + Color: stringPtr("#f5bde6"), + }, + GenericDeleted: ansi.StylePrimitive{ + Color: stringPtr("#ed8796"), + }, + GenericEmph: ansi.StylePrimitive{ + Color: stringPtr("#cad3f5"), + Italic: boolPtr(true), + }, + GenericInserted: ansi.StylePrimitive{ + Color: stringPtr("#a6da95"), + }, + GenericStrong: ansi.StylePrimitive{ + Color: stringPtr("#cad3f5"), + Bold: boolPtr(true), + }, + GenericSubheading: ansi.StylePrimitive{ + Color: stringPtr("#91d7e3"), + }, + Background: ansi.StylePrimitive{ + BackgroundColor: stringPtr("#1e2030"), + }, + }, + }, + Table: ansi.StyleTable{ + StyleBlock: ansi.StyleBlock{ + StylePrimitive: ansi.StylePrimitive{}, + }, + CenterSeparator: stringPtr("┼"), + ColumnSeparator: stringPtr("│"), + RowSeparator: stringPtr("─"), + }, + DefinitionDescription: ansi.StylePrimitive{ + BlockPrefix: "\n🠶 ", + }, +} + +func boolPtr(b bool) *bool { return &b } +func stringPtr(s string) *string { return &s } +func uintPtr(u uint) *uint { return &u } diff --git a/contribs/gnodev/pkg/browser/model_tasks.go b/contribs/gnodev/pkg/browser/model_tasks.go new file mode 100644 index 00000000000..553d0168b6d --- /dev/null +++ b/contribs/gnodev/pkg/browser/model_tasks.go @@ -0,0 +1,85 @@ +// modified version of ""github.com/charmbracelet/bubbles/spinner" + +package browser + +import ( + "time" + + tea "github.com/charmbracelet/bubbletea" +) + +type Spinner struct { + Frames []string + FPS time.Duration +} + +// TickMsg indicates that the timer has ticked and we should render a frame. +type SpinnerTickMsg time.Time + +var MeterLoader = Spinner{ + Frames: []string{ + "▱▱▱▱▱▱▱▱", "▰▱▱▱▱▱▱▱", "▰▰▱▱▱▱▱▱", "▰▰▰▱▱▱▱▱", + "▰▰▰▰▱▱▱▱", "▰▰▰▰▰▱▱▱", "▰▰▰▰▰▰▱▱", "▰▰▰▰▰▰▰▱", + "▰▰▰▰▰▰▰▰", "▱▰▰▰▰▰▰▰", "▱▱▰▰▰▰▰▰", "▱▱▱▰▰▰▰▰", + "▱▱▱▱▰▰▰▰", "▱▱▱▱▱▰▰▰", "▱▱▱▱▱▱▰▰", "▱▱▱▱▱▱▱▰", + }, + FPS: time.Second / 70, //nolint:gomnd +} + +type LoaderModel struct { + spinner Spinner + frame int + task int +} + +func newLoaderModel() LoaderModel { + return LoaderModel{ + spinner: MeterLoader, + } +} + +func (m LoaderModel) Update(msg tea.Msg) (LoaderModel, tea.Cmd) { + switch msg.(type) { + case SpinnerTickMsg: + m.frame = (m.frame + 1) % len(m.spinner.Frames) + return m, m.tick() + default: + return m, nil + } +} + +func (m LoaderModel) tick() tea.Cmd { + return tea.Tick(m.spinner.FPS, func(t time.Time) tea.Msg { + return SpinnerTickMsg(t) + }) +} + +func (m LoaderModel) Tick() tea.Msg { + return SpinnerTickMsg(time.Now()) +} + +func (m *LoaderModel) Active() bool { + return m.frame > 0 || m.task > 0 +} + +func (m *LoaderModel) Add(i int) tea.Cmd { + var cmd tea.Cmd + if i > 0 { + if m.task == 0 { + cmd = m.Tick + } + + m.task += i + } + return cmd +} + +func (m *LoaderModel) Done() { + if m.task > 0 { + m.task -= 1 + } +} + +func (m *LoaderModel) View() string { + return m.spinner.Frames[m.frame] +} diff --git a/contribs/gnodev/pkg/browser/model_view.go b/contribs/gnodev/pkg/browser/model_view.go new file mode 100644 index 00000000000..40d78d18c6d --- /dev/null +++ b/contribs/gnodev/pkg/browser/model_view.go @@ -0,0 +1,174 @@ +package browser + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" +) + +var ( + boxRoundedStyle = func(r *lipgloss.Renderer) lipgloss.Style { + b := lipgloss.RoundedBorder() + return r.NewStyle(). + BorderStyle(b). + Padding(0, 2) + } + + inputStyleLeft = func(r *lipgloss.Renderer) lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Right = "├" + return r.NewStyle(). + BorderStyle(b). + Padding(0, 2) + } + + infoStyle = func(r *lipgloss.Renderer) lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Left = "┤" + return boxRoundedStyle(r).Copy().BorderStyle(b) + } +) + +func (m model) View() string { + if !m.bannerDiscarded { + return m.bannerView() + } + + if !m.ready { + return "+" + } + + mainView := fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.bodyView(), m.footerView()) + return m.zone.Scan(mainView) +} + +func (m model) bannerView() string { + banner := m.banner.View() + if banner == "" || m.width == 0 || m.height == 0 { + return "" + } + + // XXX: Encapsulate banner to avoid banner glitches + bannerView := m.render.NewStyle().Margin(1). + Render(banner) + widthView := m.width + 1 + + return lipgloss.Place(widthView, m.height, lipgloss.Center, lipgloss.Center, + lipgloss.JoinVertical(lipgloss.Center, + bannerView, + "press to continue", + ), + ) +} + +func (m model) listFuncsView() string { + return boxRoundedStyle(m.render). + Render(m.listFuncs.View()) +} + +func (m model) bodyView() string { + if m.commandInput.Focused() { + // handle command input + if v := m.commandInput.Value(); v != "" { + m.listFuncs.FilterItems(v) + } else { + m.listFuncs.Reset() + } + + if len(m.listFuncs.Items()) > 0 { + m.viewport.Height = m.height - lipgloss.Height(m.listFuncsView()) + } else { + m.viewport.Height = m.height + } + } + + return m.viewport.View() +} + +var ( + loadingStyle = func(r *lipgloss.Renderer) lipgloss.Style { + return r.NewStyle(). + Foreground(lipgloss.Color("#dd7878")). + Bold(true) + } + + navStyleEnable = func(r *lipgloss.Renderer) lipgloss.Style { + return r.NewStyle(). + Foreground(lipgloss.Color("#fab387")) + } + + navStyleDisable = func(r *lipgloss.Renderer) lipgloss.Style { + return r.NewStyle(). + Foreground(lipgloss.Color("240")) + } +) + +func (m model) navView() string { + home := navStyleEnable(m.render).Padding(0, 1).Render("[Home]") + + var style lipgloss.Style + if m.current != nil && m.current.Prev() != nil { + style = navStyleEnable(m.render) + } else { + style = navStyleDisable(m.render) + } + prev := style.Margin(0, 1).Render("") + + title := m.render.NewStyle().Bold(true).Render("Gno.Land") + if m.taskLoader.Active() { + title = loadingStyle(m.render).Render(m.taskLoader.View()) + } + + spaceWidth := m.width / 3 // left middle and right + return lipgloss.JoinHorizontal(lipgloss.Left, + m.render.NewStyle().Width(spaceWidth).Padding(0, 1). + Render(lipgloss.JoinHorizontal(lipgloss.Left, + m.zone.Mark("prev_button", prev), + m.zone.Mark("next_button", next), + )), + m.render.PlaceHorizontal(spaceWidth, lipgloss.Center, title), + m.render.PlaceHorizontal(spaceWidth, lipgloss.Right, + m.zone.Mark("home_button", home)), + ) +} + +func (m model) headerView() string { + return lipgloss.JoinVertical(lipgloss.Left, m.navView(), m.urlView()) +} + +func (m model) urlView() string { + return m.zone.Mark("url_input", boxRoundedStyle(m.render). + Width(m.viewport.Width-2). + Render(m.urlInput.View())) +} + +func (m model) footerView() string { + info := infoStyle(m.render).Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) + + if m.readonly { + // On readonly, simply discard command input interface + line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info))) + return lipgloss.JoinHorizontal(lipgloss.Center, line, info) + } + + command := m.zone.Mark("command_input", inputStyleLeft(m.render). + Width(m.viewport.Width-lipgloss.Width(info)-5). + Render(m.commandInput.View())) + line := strings.Repeat("─", 3) + + powerline := lipgloss.JoinHorizontal(lipgloss.Center, command, line, info) + if m.commandFocus && len(m.listFuncs.Items()) > 0 { + suggestions := m.listFuncsView() + return lipgloss.JoinVertical(lipgloss.Left, suggestions, powerline) + } + + return powerline +} diff --git a/contribs/gnodev/pkg/browser/utils.go b/contribs/gnodev/pkg/browser/utils.go new file mode 100644 index 00000000000..b322bea552a --- /dev/null +++ b/contribs/gnodev/pkg/browser/utils.go @@ -0,0 +1,33 @@ +package browser + +import ( + "path/filepath" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb" +) + +func redirectWebPath(path string) string { + if alias, ok := gnoweb.Aliases[path]; ok { + return alias + } + + if redirect, ok := gnoweb.Redirects[path]; ok { + return redirect + } + + return path +} + +func cleanupRealmPath(prefix, realm string) string { + // Trim prefix + path := strings.TrimPrefix(realm, prefix) + // redirect if any well known path + path = redirectWebPath(path) + // trim any slash + path = strings.TrimPrefix(path, "/") + // clean up path + path = filepath.Clean(path) + + return path +} diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 7f0c266bf48..5b7c4fe08da 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -12,6 +12,7 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/amino" @@ -52,7 +53,7 @@ func DefaultNodeConfig(rootdir string) *NodeConfig { balances := []gnoland.Balance{ { Address: defaultDeployer, - Amount: std.Coins{std.NewCoin("ugnot", 10e12)}, + Amount: std.Coins{std.NewCoin(ugnot.Denom, 10e12)}, }, } @@ -87,7 +88,7 @@ type Node struct { currentStateIndex int } -var DefaultFee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) +var DefaultFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { mpkgs, err := NewPackagesMap(cfg.PackagesPathList) diff --git a/contribs/gnodev/pkg/dev/node_state_test.go b/contribs/gnodev/pkg/dev/node_state_test.go index 17f96367512..efaeb979693 100644 --- a/contribs/gnodev/pkg/dev/node_state_test.go +++ b/contribs/gnodev/pkg/dev/node_state_test.go @@ -8,8 +8,8 @@ import ( emitter "github.com/gnolang/gno/contribs/gnodev/internal/mock" "github.com/gnolang/gno/contribs/gnodev/pkg/events" - "github.com/gnolang/gno/gno.land/pkg/gnoclient" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -87,10 +87,10 @@ func TestSaveCurrentState(t *testing.T) { require.NoError(t, err) // Send a new tx - msg := gnoclient.MsgCall{ - PkgPath: testCounterRealm, - FuncName: "Inc", - Args: []string{"10"}, + msg := vm.MsgCall{ + PkgPath: testCounterRealm, + Func: "Inc", + Args: []string{"10"}, } res, err := testingCallRealm(t, node, msg) @@ -169,10 +169,10 @@ func Render(_ string) string { return strconv.Itoa(value) } for i := 0; i < inc; i++ { t.Logf("call %d", i) // Craft `Inc` msg - msg := gnoclient.MsgCall{ - PkgPath: testCounterRealm, - FuncName: "Inc", - Args: []string{"1"}, + msg := vm.MsgCall{ + PkgPath: testCounterRealm, + Func: "Inc", + Args: []string{"1"}, } res, err := testingCallRealm(t, node, msg) diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 48204b4ce8d..11b0a2090d7 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -10,7 +10,9 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/gno.land/pkg/gnoclient" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -189,11 +191,11 @@ func Render(_ string) string { return str } require.Equal(t, render, "foo") // Call `UpdateStr` to update `str` value with "bar" - msg := gnoclient.MsgCall{ - PkgPath: "gno.land/r/dev/foo", - FuncName: "UpdateStr", - Args: []string{"bar"}, - Send: "", + msg := vm.MsgCall{ + PkgPath: "gno.land/r/dev/foo", + Func: "UpdateStr", + Args: []string{"bar"}, + Send: nil, } res, err := testingCallRealm(t, node, msg) require.NoError(t, err) @@ -236,7 +238,7 @@ func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error return render, err } -func testingCallRealm(t *testing.T, node *Node, msgs ...gnoclient.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { +func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { t.Helper() signer := newInMemorySigner(t, node.Config().ChainID()) @@ -246,11 +248,19 @@ func testingCallRealm(t *testing.T, node *Node, msgs ...gnoclient.MsgCall) (*cor } txcfg := gnoclient.BaseTxCfg{ - GasFee: "1000000ugnot", // Gas fee - GasWanted: 2_000_000, // Gas wanted + GasFee: ugnot.ValueString(1000000), // Gas fee + GasWanted: 2_000_000, // Gas wanted } - return cli.Call(txcfg, msgs...) + // Set Caller in the msgs + caller, err := signer.Info() + require.NoError(t, err) + vmMsgs := make([]vm.MsgCall, 0, len(msgs)) + for _, msg := range msgs { + vmMsgs = append(vmMsgs, vm.NewMsgCall(caller.GetAddress(), msg.Send, msg.PkgPath, msg.Func, msg.Args)) + } + + return cli.Call(txcfg, vmMsgs...) } func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go index 605db312429..151a89a7815 100644 --- a/contribs/gnodev/pkg/dev/packages_test.go +++ b/contribs/gnodev/pkg/dev/packages_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/gnolang/gno/contribs/gnodev/pkg/address" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/assert" @@ -26,33 +27,60 @@ func TestResolvePackagePathQuery(t *testing.T) { ExpectedPackagePath PackagePath ShouldFail bool }{ - {".", PackagePath{ + { Path: ".", - }, false}, - {"/simple/path", PackagePath{ + ExpectedPackagePath: PackagePath{ + Path: ".", + }, + }, + { Path: "/simple/path", - }, false}, - {"/ambiguo/u//s/path///", PackagePath{ - Path: "/ambiguo/u/s/path", - }, false}, - {"/path/with/creator?creator=testAccount", PackagePath{ - Path: "/path/with/creator", - Creator: testingAddress, - }, false}, - {"/path/with/deposit?deposit=100ugnot", PackagePath{ - Path: "/path/with/deposit", - Deposit: std.MustParseCoins("100ugnot"), - }, false}, - {".?creator=g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na&deposit=100ugnot", PackagePath{ - Path: ".", - Creator: testingAddress, - Deposit: std.MustParseCoins("100ugnot"), - }, false}, + ExpectedPackagePath: PackagePath{ + Path: "/simple/path", + }, + }, + { + Path: "/ambiguo/u//s/path///", + ExpectedPackagePath: PackagePath{ + Path: "/ambiguo/u/s/path", + }, + }, + { + Path: "/path/with/creator?creator=testAccount", + ExpectedPackagePath: PackagePath{ + Path: "/path/with/creator", + Creator: testingAddress, + }, + }, + { + Path: "/path/with/deposit?deposit=" + ugnot.ValueString(100), + ExpectedPackagePath: PackagePath{ + Path: "/path/with/deposit", + Deposit: std.MustParseCoins(ugnot.ValueString(100)), + }, + }, + { + Path: ".?creator=g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na&deposit=" + ugnot.ValueString(100), + ExpectedPackagePath: PackagePath{ + Path: ".", + Creator: testingAddress, + Deposit: std.MustParseCoins(ugnot.ValueString(100)), + }, + }, // errors cases - {"/invalid/account?creator=UnknownAccount", PackagePath{}, true}, - {"/invalid/address?creator=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", PackagePath{}, true}, - {"/invalid/deposit?deposit=abcd", PackagePath{}, true}, + { + Path: "/invalid/account?creator=UnknownAccount", + ShouldFail: true, + }, + { + Path: "/invalid/address?creator=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", + ShouldFail: true, + }, + { + Path: "/invalid/deposit?deposit=abcd", + ShouldFail: true, + }, } for _, tc := range cases { diff --git a/contribs/gnodev/pkg/emitter/server.go b/contribs/gnodev/pkg/emitter/server.go index e6052890095..3e32984268d 100644 --- a/contribs/gnodev/pkg/emitter/server.go +++ b/contribs/gnodev/pkg/emitter/server.go @@ -60,7 +60,7 @@ func (s *Server) Emit(evt events.Event) { go s.emit(evt) } -type eventJSON struct { +type EventJSON struct { Type events.Type `json:"type"` Data any `json:"data"` } @@ -69,7 +69,7 @@ func (s *Server) emit(evt events.Event) { s.muClients.Lock() defer s.muClients.Unlock() - jsonEvt := eventJSON{evt.Type(), evt} + jsonEvt := EventJSON{evt.Type(), evt} s.logger.Info("sending event to clients", "clients", len(s.clients), diff --git a/contribs/gnodev/pkg/emitter/server_test.go b/contribs/gnodev/pkg/emitter/server_test.go index 4725378dbda..8795d2da048 100644 --- a/contribs/gnodev/pkg/emitter/server_test.go +++ b/contribs/gnodev/pkg/emitter/server_test.go @@ -40,7 +40,7 @@ func TestServer_ServeHTTP(t *testing.T) { sendEvt := events.Custom("TEST") svr.Emit(sendEvt) // simulate reload - var recvEvt eventJSON + var recvEvt EventJSON err = c.ReadJSON(&recvEvt) require.NoError(t, err) assert.Equal(t, sendEvt.Type(), recvEvt.Type) diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod index 69768fc7e26..c56c0b7d425 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -5,7 +5,7 @@ go 1.22 toolchain go1.22.4 require ( - github.com/gnolang/faucet v0.3.1 + github.com/gnolang/faucet v0.3.2 github.com/gnolang/gno v0.1.1 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index 2056f4f89e9..1508cdae1e6 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -45,8 +45,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/faucet v0.3.1 h1:BalLeZNYk9v/+jW6d+2ox1SMWgHtDHZ+9rSD71h4Xcg= -github.com/gnolang/faucet v0.3.1/go.mod h1:/wbw9h4ooMzzyNBuM0X+ol7CiPH2OFjAFF3bYAXqA7U= +github.com/gnolang/faucet v0.3.2 h1:3QBrdmnQszRaAZbxgO5xDDm3czNa0L/RFmhnCkbxy5I= +github.com/gnolang/faucet v0.3.2/go.mod h1:/wbw9h4ooMzzyNBuM0X+ol7CiPH2OFjAFF3bYAXqA7U= github.com/gnolang/gno v0.1.1 h1:t41S0SWIUa3syI7XpRAuCneCgRc8gOJ2g8DkUedF72U= github.com/gnolang/gno v0.1.1/go.mod h1:BTaBNeaoY/W95NN6QA4RCoQ6Z7mi8M+Zb1I1wMWGg2w= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= diff --git a/docs/gno-infrastructure/validators/setting-up-a-new-chain.md b/docs/gno-infrastructure/validators/setting-up-a-new-chain.md index 5d440a86684..0411fa3b02a 100644 --- a/docs/gno-infrastructure/validators/setting-up-a-new-chain.md +++ b/docs/gno-infrastructure/validators/setting-up-a-new-chain.md @@ -93,7 +93,7 @@ Let's break down the most important default settings: :::info Resetting the chain As mentioned, the working directory for the node is located in `data-dir`. To reset the chain, you need -to delete this directory and start the node up again. If you are using the default node configuration, you can run +to delete this directory and `genesis.json`, then start the node up again. If you are using the default node configuration, you can run `make fclean` from the `gno.land` sub-folder to delete the `gnoland-data` working directory. ::: diff --git a/docs/gno-tooling/cli/gnodev.md b/docs/gno-tooling/cli/gnodev.md index 4a1880822fc..f9491fea803 100644 --- a/docs/gno-tooling/cli/gnodev.md +++ b/docs/gno-tooling/cli/gnodev.md @@ -105,7 +105,7 @@ A specific deposit amount can also be set with the following pattern: gnodev ./myrealm?deposit=42ugnot ``` -This patten can be expanded to accommodate both options: +This pattern can be expanded to accommodate both options: ``` gnodev ./myrealm?creator=&deposit= diff --git a/docs/how-to-guides/connecting-from-go.md b/docs/how-to-guides/connecting-from-go.md index 4926f700a4d..6f05a891cd2 100644 --- a/docs/how-to-guides/connecting-from-go.md +++ b/docs/how-to-guides/connecting-from-go.md @@ -223,11 +223,21 @@ message type. We will use the wrapped ugnot realm for this example, wrapping `1000000ugnot` (1 $GNOT) for demonstration purposes. ```go -msg := gnoclient.MsgCall{ - PkgPath: "gno.land/r/demo/wugnot", // wrapped ugnot realm path - FuncName: "Deposit", // function to call - Args: nil, // arguments in string format - Send: "1000000ugnot", // coins to send along with transaction +import ( + ... + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/std" +) +``` + +```go +msg := vm.MsgCall{ + Caller: addr, // address of the caller (signer) + PkgPath: "gno.land/r/demo/wugnot", // wrapped ugnot realm path + Func: "Deposit", // function to call + Args: nil, // arguments in string format + Send: std.Coins{{Denom: ugnot.Denom, Amount: int64(1000000)}}, // coins to send along with transaction } ``` diff --git a/docs/reference/network-config.md b/docs/reference/network-config.md index 0dacc8e80d7..6d4fc9ea14a 100644 --- a/docs/reference/network-config.md +++ b/docs/reference/network-config.md @@ -9,7 +9,7 @@ id: network-config | Portal Loop | https://rpc.gno.land:443 | `portal-loop` | | Test4 | https://rpc.test4.gno.land:443 | `test4` | | Test3 | https://rpc.test3.gno.land:443 | `test3` | -| Staging | http://rpc.staging.gno.land:36657 | `staging` | +| Staging | https://rpc.staging.gno.land:443 | `staging` | ### WebSocket endpoints All networks follow the same pattern for websocket connections: diff --git a/examples/gno.land/p/demo/subscription/doc.gno b/examples/gno.land/p/demo/subscription/doc.gno new file mode 100644 index 00000000000..9cc102fcc9a --- /dev/null +++ b/examples/gno.land/p/demo/subscription/doc.gno @@ -0,0 +1,66 @@ +// Package subscription provides a flexible system for managing both recurring and +// lifetime subscriptions in Gno applications. It enables developers to handle +// payment-based access control for services or products. The library supports +// both subscriptions requiring periodic payments (recurring) and one-time payments +// (lifetime). Subscriptions are tracked using an AVL tree for efficient management +// of subscription statuses. +// +// Usage: +// +// Import the required sub-packages (`recurring` and/or `lifetime`) to manage specific +// subscription types. The methods provided allow users to subscribe, check subscription +// status, and manage payments. +// +// Recurring Subscription: +// +// Recurring subscriptions require periodic payments to maintain access. +// Users pay to extend their access for a specific duration. +// +// Example: +// +// // Create a recurring subscription requiring 100 ugnot every 30 days +// recSub := recurring.NewRecurringSubscription(time.Hour * 24 * 30, 100) +// +// // Process payment for the recurring subscription +// recSub.Subscribe() +// +// // Gift a recurring subscription to another user +// recSub.GiftSubscription(recipientAddress) +// +// // Check if a user has a valid subscription +// recSub.HasValidSubscription(addr) +// +// // Get the expiration date of the subscription +// recSub.GetExpiration(caller) +// +// // Update the subscription amount to 200 ugnot +// recSub.UpdateAmount(200) +// +// // Get the current subscription amount +// recSub.GetAmount() +// +// Lifetime Subscription: +// +// Lifetime subscriptions require a one-time payment for permanent access. +// Once paid, users have indefinite access without further payments. +// +// Example: +// +// // Create a lifetime subscription costing 500 ugnot +// lifeSub := lifetime.NewLifetimeSubscription(500) +// +// // Process payment for lifetime access +// lifeSub.Subscribe() +// +// // Gift a lifetime subscription to another user +// lifeSub.GiftSubscription(recipientAddress) +// +// // Check if a user has a valid subscription +// lifeSub.HasValidSubscription(addr) +// +// // Update the lifetime subscription amount to 1000 ugnot +// lifeSub.UpdateAmount(1000) +// +// // Get the current lifetime subscription amount +// lifeSub.GetAmount() +package subscription diff --git a/examples/gno.land/p/demo/subscription/gno.mod b/examples/gno.land/p/demo/subscription/gno.mod new file mode 100644 index 00000000000..ea60a4c628a --- /dev/null +++ b/examples/gno.land/p/demo/subscription/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/subscription diff --git a/examples/gno.land/p/demo/subscription/lifetime/errors.gno b/examples/gno.land/p/demo/subscription/lifetime/errors.gno new file mode 100644 index 00000000000..faeda4cd9fe --- /dev/null +++ b/examples/gno.land/p/demo/subscription/lifetime/errors.gno @@ -0,0 +1,10 @@ +package lifetime + +import "errors" + +var ( + ErrNoSub = errors.New("lifetime subscription: no active subscription found") + ErrAmt = errors.New("lifetime subscription: payment amount does not match the required subscription amount") + ErrAlreadySub = errors.New("lifetime subscription: this address already has an active lifetime subscription") + ErrNotAuthorized = errors.New("lifetime subscription: action not authorized") +) diff --git a/examples/gno.land/p/demo/subscription/lifetime/gno.mod b/examples/gno.land/p/demo/subscription/lifetime/gno.mod new file mode 100644 index 00000000000..0084aa714c5 --- /dev/null +++ b/examples/gno.land/p/demo/subscription/lifetime/gno.mod @@ -0,0 +1,8 @@ +module gno.land/p/demo/subscription/lifetime + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest +) diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno new file mode 100644 index 00000000000..8a4c10b687b --- /dev/null +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno @@ -0,0 +1,81 @@ +package lifetime + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" +) + +// LifetimeSubscription represents a subscription that requires only a one-time payment. +// It grants permanent access to a service or product. +type LifetimeSubscription struct { + ownable.Ownable + amount int64 + subs *avl.Tree // std.Address -> bool +} + +// NewLifetimeSubscription creates and returns a new lifetime subscription. +func NewLifetimeSubscription(amount int64) *LifetimeSubscription { + return &LifetimeSubscription{ + Ownable: *ownable.New(), + amount: amount, + subs: avl.NewTree(), + } +} + +// processSubscription handles the subscription process for a given receiver. +func (ls *LifetimeSubscription) processSubscription(receiver std.Address) error { + amount := std.GetOrigSend() + + if amount.AmountOf("ugnot") != ls.amount { + return ErrAmt + } + + _, exists := ls.subs.Get(receiver.String()) + + if exists { + return ErrAlreadySub + } + + ls.subs.Set(receiver.String(), true) + + return nil +} + +// Subscribe processes the payment for a lifetime subscription. +func (ls *LifetimeSubscription) Subscribe() error { + caller := std.PrevRealm().Addr() + return ls.processSubscription(caller) +} + +// GiftSubscription allows the caller to pay for a lifetime subscription for another user. +func (ls *LifetimeSubscription) GiftSubscription(receiver std.Address) error { + return ls.processSubscription(receiver) +} + +// HasValidSubscription checks if the given address has an active lifetime subscription. +func (ls *LifetimeSubscription) HasValidSubscription(addr std.Address) error { + _, exists := ls.subs.Get(addr.String()) + + if !exists { + return ErrNoSub + } + + return nil +} + +// UpdateAmount allows the owner of the LifetimeSubscription contract to update the subscription price. +func (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error { + if err := ls.CallerIsOwner(); err != nil { + return ErrNotAuthorized + } + + ls.amount = newAmount + return nil +} + +// GetAmount returns the current subscription price. +func (ls *LifetimeSubscription) GetAmount() int64 { + return ls.amount +} diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno new file mode 100644 index 00000000000..efbae90c11c --- /dev/null +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno @@ -0,0 +1,105 @@ +package lifetime + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + charlie = testutils.TestAddress("charlie") +) + +func TestLifetimeSubscription(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + ls := NewLifetimeSubscription(1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := ls.Subscribe() + uassert.NoError(t, err, "Expected ProcessPayment to succeed") + + err = ls.HasValidSubscription(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected Alice to have access") +} + +func TestLifetimeSubscriptionGift(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + ls := NewLifetimeSubscription(1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := ls.GiftSubscription(bob) + uassert.NoError(t, err, "Expected ProcessPaymentGift to succeed for Bob") + + err = ls.HasValidSubscription(bob) + uassert.NoError(t, err, "Expected Bob to have access") + + err = ls.HasValidSubscription(charlie) + uassert.Error(t, err, "Expected Charlie to fail access check") +} + +func TestUpdateAmountAuthorization(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + ls := NewLifetimeSubscription(1000) + + err := ls.UpdateAmount(2000) + uassert.NoError(t, err, "Expected Alice to succeed in updating amount") + + std.TestSetOrigCaller(bob) + + err = ls.UpdateAmount(3000) + uassert.Error(t, err, "Expected Bob to fail when updating amount") +} + +func TestIncorrectPaymentAmount(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + ls := NewLifetimeSubscription(1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 500}}, nil) + err := ls.Subscribe() + uassert.Error(t, err, "Expected payment to fail with incorrect amount") +} + +func TestMultipleSubscriptionAttempts(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + ls := NewLifetimeSubscription(1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := ls.Subscribe() + uassert.NoError(t, err, "Expected first subscription to succeed") + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err = ls.Subscribe() + uassert.Error(t, err, "Expected second subscription to fail as Alice is already subscribed") +} + +func TestGiftSubscriptionWithIncorrectAmount(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + ls := NewLifetimeSubscription(1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 500}}, nil) + err := ls.GiftSubscription(bob) + uassert.Error(t, err, "Expected gift subscription to fail with incorrect amount") + + err = ls.HasValidSubscription(bob) + uassert.Error(t, err, "Expected Bob to not have access after incorrect gift subscription") +} + +func TestUpdateAmountEffectiveness(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + ls := NewLifetimeSubscription(1000) + + err := ls.UpdateAmount(2000) + uassert.NoError(t, err, "Expected Alice to succeed in updating amount") + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err = ls.Subscribe() + uassert.Error(t, err, "Expected subscription to fail with old amount after update") + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 2000}}, nil) + err = ls.Subscribe() + uassert.NoError(t, err, "Expected subscription to succeed with new amount") +} diff --git a/examples/gno.land/p/demo/subscription/recurring/errors.gno b/examples/gno.land/p/demo/subscription/recurring/errors.gno new file mode 100644 index 00000000000..76a55e069bf --- /dev/null +++ b/examples/gno.land/p/demo/subscription/recurring/errors.gno @@ -0,0 +1,11 @@ +package recurring + +import "errors" + +var ( + ErrNoSub = errors.New("recurring subscription: no active subscription found") + ErrSubExpired = errors.New("recurring subscription: your subscription has expired") + ErrAmt = errors.New("recurring subscription: payment amount does not match the required subscription amount") + ErrAlreadySub = errors.New("recurring subscription: this address already has an active subscription") + ErrNotAuthorized = errors.New("recurring subscription: action not authorized") +) diff --git a/examples/gno.land/p/demo/subscription/recurring/gno.mod b/examples/gno.land/p/demo/subscription/recurring/gno.mod new file mode 100644 index 00000000000..d3cf8a044f8 --- /dev/null +++ b/examples/gno.land/p/demo/subscription/recurring/gno.mod @@ -0,0 +1,8 @@ +module gno.land/p/demo/subscription/recurring + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest +) diff --git a/examples/gno.land/p/demo/subscription/recurring/recurring.gno b/examples/gno.land/p/demo/subscription/recurring/recurring.gno new file mode 100644 index 00000000000..b5277bd716e --- /dev/null +++ b/examples/gno.land/p/demo/subscription/recurring/recurring.gno @@ -0,0 +1,104 @@ +package recurring + +import ( + "std" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" +) + +// RecurringSubscription represents a subscription that requires periodic payments. +// It includes the duration of the subscription and the amount required per period. +type RecurringSubscription struct { + ownable.Ownable + duration time.Duration + amount int64 + subs *avl.Tree // std.Address -> time.Time +} + +// NewRecurringSubscription creates and returns a new recurring subscription. +func NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSubscription { + return &RecurringSubscription{ + Ownable: *ownable.New(), + duration: duration, + amount: amount, + subs: avl.NewTree(), + } +} + +// HasValidSubscription verifies if the caller has an active recurring subscription. +func (rs *RecurringSubscription) HasValidSubscription(addr std.Address) error { + expTime, exists := rs.subs.Get(addr.String()) + if !exists { + return ErrNoSub + } + + if time.Now().After(expTime.(time.Time)) { + return ErrSubExpired + } + + return nil +} + +// processSubscription processes the payment for a given receiver and renews or adds their subscription. +func (rs *RecurringSubscription) processSubscription(receiver std.Address) error { + amount := std.GetOrigSend() + + if amount.AmountOf("ugnot") != rs.amount { + return ErrAmt + } + + expTime, exists := rs.subs.Get(receiver.String()) + + // If the user is already a subscriber but his subscription has expired, authorize renewal + if exists { + expiration := expTime.(time.Time) + if time.Now().Before(expiration) { + return ErrAlreadySub + } + } + + // Renew or add subscription + newExpiration := time.Now().Add(rs.duration) + rs.subs.Set(receiver.String(), newExpiration) + + return nil +} + +// Subscribe handles the payment for the caller's subscription. +func (rs *RecurringSubscription) Subscribe() error { + caller := std.PrevRealm().Addr() + + return rs.processSubscription(caller) +} + +// GiftSubscription allows the user to pay for a subscription for another user (receiver). +func (rs *RecurringSubscription) GiftSubscription(receiver std.Address) error { + return rs.processSubscription(receiver) +} + +// GetExpiration returns the expiration date of the recurring subscription for a given caller. +func (rs *RecurringSubscription) GetExpiration(addr std.Address) (time.Time, error) { + expTime, exists := rs.subs.Get(addr.String()) + if !exists { + return time.Time{}, ErrNoSub + } + + return expTime.(time.Time), nil +} + +// UpdateAmount allows the owner of the subscription contract to change the required subscription amount. +func (rs *RecurringSubscription) UpdateAmount(newAmount int64) error { + if err := rs.CallerIsOwner(); err != nil { + return ErrNotAuthorized + } + + rs.amount = newAmount + return nil +} + +// GetAmount returns the current amount required for each subscription period. +func (rs *RecurringSubscription) GetAmount() int64 { + return rs.amount +} diff --git a/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno b/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno new file mode 100644 index 00000000000..e8bca15c0bf --- /dev/null +++ b/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno @@ -0,0 +1,134 @@ +package recurring + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + charlie = testutils.TestAddress("charlie") +) + +func TestRecurringSubscription(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.Subscribe() + uassert.NoError(t, err, "Expected ProcessPayment to succeed for Alice") + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected Alice to have access") + + expiration, err := rs.GetExpiration(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected to get expiration for Alice") +} + +func TestRecurringSubscriptionGift(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.GiftSubscription(bob) + uassert.NoError(t, err, "Expected ProcessPaymentGift to succeed for Bob") + + err = rs.HasValidSubscription(bob) + uassert.NoError(t, err, "Expected Bob to have access") + + err = rs.HasValidSubscription(charlie) + uassert.Error(t, err, "Expected Charlie to fail access check") +} + +func TestRecurringSubscriptionExpiration(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.Subscribe() + uassert.NoError(t, err, "Expected ProcessPayment to succeed for Alice") + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected Alice to have access") + + expiration := time.Now().Add(-time.Hour * 2) + rs.subs.Set(std.PrevRealm().Addr().String(), expiration) + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.Error(t, err, "Expected Alice's subscription to be expired") +} + +func TestUpdateAmountAuthorization(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + err := rs.UpdateAmount(2000) + uassert.NoError(t, err, "Expected Alice to succeed in updating amount") + + std.TestSetOrigCaller(bob) + err = rs.UpdateAmount(3000) + uassert.Error(t, err, "Expected Bob to fail when updating amount") +} + +func TestGetAmount(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + amount := rs.GetAmount() + uassert.Equal(t, amount, int64(1000), "Expected the initial amount to be 1000 ugnot") + + err := rs.UpdateAmount(2000) + uassert.NoError(t, err, "Expected Alice to succeed in updating amount") + + amount = rs.GetAmount() + uassert.Equal(t, amount, int64(2000), "Expected the updated amount to be 2000 ugnot") +} + +func TestIncorrectPaymentAmount(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 500}}, nil) + err := rs.Subscribe() + uassert.Error(t, err, "Expected payment with incorrect amount to fail") +} + +func TestMultiplePaymentsForSameUser(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.Subscribe() + uassert.NoError(t, err, "Expected first ProcessPayment to succeed for Alice") + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err = rs.Subscribe() + uassert.Error(t, err, "Expected second ProcessPayment to fail for Alice due to existing subscription") +} + +func TestRecurringSubscriptionWithMultiplePayments(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.Subscribe() + uassert.NoError(t, err, "Expected first ProcessPayment to succeed for Alice") + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected Alice to have access after first payment") + + expiration := time.Now().Add(-time.Hour * 2) + rs.subs.Set(std.PrevRealm().Addr().String(), expiration) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err = rs.Subscribe() + uassert.NoError(t, err, "Expected second ProcessPayment to succeed for Alice") + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected Alice to have access after second payment") +} diff --git a/examples/gno.land/p/demo/subscription/subscription.gno b/examples/gno.land/p/demo/subscription/subscription.gno new file mode 100644 index 00000000000..cc52a2c0e2d --- /dev/null +++ b/examples/gno.land/p/demo/subscription/subscription.gno @@ -0,0 +1,12 @@ +package subscription + +import ( + "std" +) + +// Subscription interface defines standard methods that all subscription types must implement. +type Subscription interface { + HasValidSubscription(std.Address) error + Subscribe() error + UpdateAmount(newAmount int64) error +} diff --git a/examples/gno.land/p/demo/uassert/uassert.gno b/examples/gno.land/p/demo/uassert/uassert.gno index 7b3254ea505..2776e93dca9 100644 --- a/examples/gno.land/p/demo/uassert/uassert.gno +++ b/examples/gno.land/p/demo/uassert/uassert.gno @@ -379,46 +379,85 @@ func NotEqual(t TestingT, expected, actual interface{}, msgs ...string) bool { return true } +func isNumberEmpty(n interface{}) (isNumber, isEmpty bool) { + switch n := n.(type) { + // NOTE: the cases are split individually, so that n becomes of the + // asserted type; the type of '0' was correctly inferred and converted + // to the corresponding type, int, int8, etc. + case int: + return true, n == 0 + case int8: + return true, n == 0 + case int16: + return true, n == 0 + case int32: + return true, n == 0 + case int64: + return true, n == 0 + case uint: + return true, n == 0 + case uint8: + return true, n == 0 + case uint16: + return true, n == 0 + case uint32: + return true, n == 0 + case uint64: + return true, n == 0 + case float32: + return true, n == 0 + case float64: + return true, n == 0 + } + return false, false +} func Empty(t TestingT, obj interface{}, msgs ...string) bool { t.Helper() - switch val := obj.(type) { - case string: - if val != "" { - return fail(t, msgs, "uassert.Empty: not empty string: %s", val) - } - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - if val != 0 { - return fail(t, msgs, "uassert.Empty: not empty number: %d", val) + + isNumber, isEmpty := isNumberEmpty(obj) + if isNumber { + if !isEmpty { + return fail(t, msgs, "uassert.Empty: not empty number: %d", obj) } - case std.Address: - var zeroAddr std.Address - if val != zeroAddr { - return fail(t, msgs, "uassert.Empty: not empty std.Address: %s", string(val)) + } else { + switch val := obj.(type) { + case string: + if val != "" { + return fail(t, msgs, "uassert.Empty: not empty string: %s", val) + } + case std.Address: + var zeroAddr std.Address + if val != zeroAddr { + return fail(t, msgs, "uassert.Empty: not empty std.Address: %s", string(val)) + } + default: + return fail(t, msgs, "uassert.Empty: unsupported type") } - default: - return fail(t, msgs, "uassert.Empty: unsupported type") } return true } func NotEmpty(t TestingT, obj interface{}, msgs ...string) bool { t.Helper() - switch val := obj.(type) { - case string: - if val == "" { - return fail(t, msgs, "uassert.NotEmpty: empty string: %s", val) - } - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - if val == 0 { - return fail(t, msgs, "uassert.NotEmpty: empty number: %d", val) - } - case std.Address: - var zeroAddr std.Address - if val == zeroAddr { - return fail(t, msgs, "uassert.NotEmpty: empty std.Address: %s", string(val)) + isNumber, isEmpty := isNumberEmpty(obj) + if isNumber { + if isEmpty { + return fail(t, msgs, "uassert.NotEmpty: empty number: %d", obj) + } + } else { + switch val := obj.(type) { + case string: + if val == "" { + return fail(t, msgs, "uassert.NotEmpty: empty string: %s", val) + } + case std.Address: + var zeroAddr std.Address + if val == zeroAddr { + return fail(t, msgs, "uassert.NotEmpty: empty std.Address: %s", string(val)) + } + default: + return fail(t, msgs, "uassert.NotEmpty: unsupported type") } - default: - return fail(t, msgs, "uassert.NotEmpty: unsupported type") } return true } diff --git a/examples/gno.land/p/demo/uassert/uassert_test.gno b/examples/gno.land/p/demo/uassert/uassert_test.gno index 5ead848fd15..7862eca7305 100644 --- a/examples/gno.land/p/demo/uassert/uassert_test.gno +++ b/examples/gno.land/p/demo/uassert/uassert_test.gno @@ -218,6 +218,7 @@ func TestEmpty(t *testing.T) { {"", true}, {0, true}, {int(0), true}, + {int32(0), true}, {int64(0), true}, {uint(0), true}, // XXX: continue @@ -335,6 +336,7 @@ func TestNotEmpty(t *testing.T) { {"", false}, {0, false}, {int(0), false}, + {int32(0), false}, {int64(0), false}, {uint(0), false}, {std.Address(""), false}, diff --git a/examples/gno.land/r/demo/art/millipede/millipede.gno b/examples/gno.land/r/demo/art/millipede/millipede.gno index 414941b947b..446c76e5d12 100644 --- a/examples/gno.land/r/demo/art/millipede/millipede.gno +++ b/examples/gno.land/r/demo/art/millipede/millipede.gno @@ -40,7 +40,7 @@ func Render(path string) string { output := "```\n" + Draw(size) + "```\n" if size > minSize { - output += ufmt.Sprintf("[%d](/r/demo/art/millpede:%d)< ", size-1, size-1) + output += ufmt.Sprintf("[%d](/r/demo/art/millipede:%d)< ", size-1, size-1) } if size < maxSize { output += ufmt.Sprintf(" >[%d](/r/demo/art/millipede:%d)", size+1, size+1) diff --git a/examples/gno.land/r/demo/art/millipede/millipede_test.gno b/examples/gno.land/r/demo/art/millipede/millipede_test.gno index 7fb5a5114b9..035b611d881 100644 --- a/examples/gno.land/r/demo/art/millipede/millipede_test.gno +++ b/examples/gno.land/r/demo/art/millipede/millipede_test.gno @@ -35,7 +35,7 @@ func TestRender(t *testing.T) { ╚═(███)═╝ ╚═(███)═╝ ╚═(███)═╝ -` + "```\n[19](/r/demo/art/millpede:19)< >[21](/r/demo/art/millipede:21)", +` + "```\n[19](/r/demo/art/millipede:19)< >[21](/r/demo/art/millipede:21)", }, { path: "4", @@ -45,7 +45,7 @@ func TestRender(t *testing.T) { ╚═(███)═╝ ╚═(███)═╝ ╚═(███)═╝ -` + "```\n[3](/r/demo/art/millpede:3)< >[5](/r/demo/art/millipede:5)", +` + "```\n[3](/r/demo/art/millipede:3)< >[5](/r/demo/art/millipede:5)", }, } diff --git a/examples/gno.land/r/demo/disperse/disperse.gno b/examples/gno.land/r/demo/disperse/disperse.gno new file mode 100644 index 00000000000..0dc833dda95 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/disperse.gno @@ -0,0 +1,99 @@ +package disperse + +import ( + "std" + + tokens "gno.land/r/demo/grc20factory" +) + +// Get address of Disperse realm +var realmAddr = std.CurrentRealm().Addr() + +// DisperseUgnot parses receivers and amounts and sends out ugnot +// The function will send out the coins to the addresses and return the leftover coins to the caller +// if there are any to return +func DisperseUgnot(addresses []std.Address, coins std.Coins) { + coinSent := std.GetOrigSend() + caller := std.PrevRealm().Addr() + banker := std.GetBanker(std.BankerTypeOrigSend) + + if len(addresses) != len(coins) { + panic(ErrNumAddrValMismatch) + } + + for _, coin := range coins { + if coin.Amount <= 0 { + panic(ErrNegativeCoinAmount) + } + + if banker.GetCoins(realmAddr).AmountOf(coin.Denom) < coin.Amount { + panic(ErrMismatchBetweenSentAndParams) + } + } + + // Send coins + for i, _ := range addresses { + banker.SendCoins(realmAddr, addresses[i], std.NewCoins(coins[i])) + } + + // Return possible leftover coins + for _, coin := range coinSent { + leftoverAmt := banker.GetCoins(realmAddr).AmountOf(coin.Denom) + if leftoverAmt > 0 { + send := std.Coins{std.NewCoin(coin.Denom, leftoverAmt)} + banker.SendCoins(realmAddr, caller, send) + } + } +} + +// DisperseGRC20 disperses tokens to multiple addresses +// Note that it is necessary to approve the realm to spend the tokens before calling this function +// see the corresponding filetests for examples +func DisperseGRC20(addresses []std.Address, amounts []uint64, symbols []string) { + caller := std.PrevRealm().Addr() + + if (len(addresses) != len(amounts)) || (len(amounts) != len(symbols)) { + panic(ErrArgLenAndSentLenMismatch) + } + + for i := 0; i < len(addresses); i++ { + tokens.TransferFrom(symbols[i], caller, addresses[i], amounts[i]) + } +} + +// DisperseGRC20String receives a string of addresses and a string of tokens +// and parses them to be used in DisperseGRC20 +func DisperseGRC20String(addresses string, tokens string) { + parsedAddresses, err := parseAddresses(addresses) + if err != nil { + panic(err) + } + + parsedAmounts, parsedSymbols, err := parseTokens(tokens) + if err != nil { + panic(err) + } + + DisperseGRC20(parsedAddresses, parsedAmounts, parsedSymbols) +} + +// DisperseUgnotString receives a string of addresses and a string of amounts +// and parses them to be used in DisperseUgnot +func DisperseUgnotString(addresses string, amounts string) { + parsedAddresses, err := parseAddresses(addresses) + if err != nil { + panic(err) + } + + parsedAmounts, err := parseAmounts(amounts) + if err != nil { + panic(err) + } + + coins := make(std.Coins, len(parsedAmounts)) + for i, amount := range parsedAmounts { + coins[i] = std.NewCoin("ugnot", amount) + } + + DisperseUgnot(parsedAddresses, coins) +} diff --git a/examples/gno.land/r/demo/disperse/doc.gno b/examples/gno.land/r/demo/disperse/doc.gno new file mode 100644 index 00000000000..100aa92cb3d --- /dev/null +++ b/examples/gno.land/r/demo/disperse/doc.gno @@ -0,0 +1,19 @@ +// Package disperse provides methods to disperse coins or GRC20 tokens among multiple addresses. +// +// The disperse package is an implementation of an existing service that allows users to send coins or GRC20 tokens to multiple addresses +// on the Ethereum blockchain. +// +// Usage: +// To use disperse, you can either use `DisperseUgnot` to send coins or `DisperseGRC20` to send GRC20 tokens to multiple addresses. +// +// Example: +// Dispersing 200 coins to two addresses: +// - DisperseUgnotString("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150,50") +// Dispersing 200 worth of a GRC20 token "TEST" to two addresses: +// - DisperseGRC20String("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150TEST,50TEST") +// +// Reference: +// - [the original dispere app](https://disperse.app/) +// - [the original disperse app on etherscan](https://etherscan.io/address/0xd152f549545093347a162dce210e7293f1452150#code) +// - [the gno disperse web app](https://gno-disperse.netlify.app/) +package disperse // import "gno.land/r/demo/disperse" diff --git a/examples/gno.land/r/demo/disperse/errors.gno b/examples/gno.land/r/demo/disperse/errors.gno new file mode 100644 index 00000000000..c054e658651 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/errors.gno @@ -0,0 +1,12 @@ +package disperse + +import "errors" + +var ( + ErrNotEnoughCoin = errors.New("disperse: not enough coin sent in") + ErrNumAddrValMismatch = errors.New("disperse: number of addresses and values to send doesn't match") + ErrInvalidAddress = errors.New("disperse: invalid address") + ErrNegativeCoinAmount = errors.New("disperse: coin amount cannot be negative") + ErrMismatchBetweenSentAndParams = errors.New("disperse: mismatch between coins sent and params called") + ErrArgLenAndSentLenMismatch = errors.New("disperse: mismatch between coins sent and args called") +) diff --git a/examples/gno.land/r/demo/disperse/gno.mod b/examples/gno.land/r/demo/disperse/gno.mod new file mode 100644 index 00000000000..0ba9c88810a --- /dev/null +++ b/examples/gno.land/r/demo/disperse/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/demo/disperse + +require gno.land/r/demo/grc20factory v0.0.0-latest diff --git a/examples/gno.land/r/demo/disperse/util.gno b/examples/gno.land/r/demo/disperse/util.gno new file mode 100644 index 00000000000..7101522572d --- /dev/null +++ b/examples/gno.land/r/demo/disperse/util.gno @@ -0,0 +1,67 @@ +package disperse + +import ( + "std" + "strconv" + "strings" + "unicode" +) + +func parseAddresses(addresses string) ([]std.Address, error) { + var ret []std.Address + + for _, str := range strings.Split(addresses, ",") { + addr := std.Address(str) + if !addr.IsValid() { + return nil, ErrInvalidAddress + } + + ret = append(ret, addr) + } + + return ret, nil +} + +func splitString(input string) (string, string) { + var pos int + for i, char := range input { + if !unicode.IsDigit(char) { + pos = i + break + } + } + return input[:pos], input[pos:] +} + +func parseTokens(tokens string) ([]uint64, []string, error) { + var amounts []uint64 + var symbols []string + + for _, token := range strings.Split(tokens, ",") { + amountStr, symbol := splitString(token) + amount, _ := strconv.Atoi(amountStr) + if amount < 0 { + return nil, nil, ErrNegativeCoinAmount + } + + amounts = append(amounts, uint64(amount)) + symbols = append(symbols, symbol) + } + + return amounts, symbols, nil +} + +func parseAmounts(amounts string) ([]int64, error) { + var ret []int64 + + for _, amt := range strings.Split(amounts, ",") { + amount, _ := strconv.Atoi(amt) + if amount < 0 { + return nil, ErrNegativeCoinAmount + } + + ret = append(ret, int64(amount)) + } + + return ret, nil +} diff --git a/examples/gno.land/r/demo/disperse/z_0_filetest.gno b/examples/gno.land/r/demo/disperse/z_0_filetest.gno new file mode 100644 index 00000000000..62a34cfdf26 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_0_filetest.gno @@ -0,0 +1,32 @@ +// SEND: 200ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + mainbal := banker.GetCoins(mainaddr) + println("main before:", mainbal) + + banker.SendCoins(mainaddr, disperseAddr, std.Coins{{"ugnot", 200}}) + disperse.DisperseUgnotString("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150,50") + + mainbal = banker.GetCoins(mainaddr) + println("main after:", mainbal) +} + +// Output: +// main before: 200000200ugnot +// main after: 200000000ugnot diff --git a/examples/gno.land/r/demo/disperse/z_1_filetest.gno b/examples/gno.land/r/demo/disperse/z_1_filetest.gno new file mode 100644 index 00000000000..1e042d320f6 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_1_filetest.gno @@ -0,0 +1,32 @@ +// SEND: 300ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + mainbal := banker.GetCoins(mainaddr) + println("main before:", mainbal) + + banker.SendCoins(mainaddr, disperseAddr, std.Coins{{"ugnot", 300}}) + disperse.DisperseUgnotString("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150,50") + + mainbal = banker.GetCoins(mainaddr) + println("main after:", mainbal) +} + +// Output: +// main before: 200000300ugnot +// main after: 200000100ugnot diff --git a/examples/gno.land/r/demo/disperse/z_2_filetest.gno b/examples/gno.land/r/demo/disperse/z_2_filetest.gno new file mode 100644 index 00000000000..163bb2fc1ab --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_2_filetest.gno @@ -0,0 +1,25 @@ +// SEND: 300ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + banker.SendCoins(mainaddr, disperseAddr, std.Coins{{"ugnot", 100}}) + disperse.DisperseUgnotString("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150,50") +} + +// Error: +// disperse: mismatch between coins sent and params called diff --git a/examples/gno.land/r/demo/disperse/z_3_filetest.gno b/examples/gno.land/r/demo/disperse/z_3_filetest.gno new file mode 100644 index 00000000000..eabed52fb38 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_3_filetest.gno @@ -0,0 +1,45 @@ +// SEND: 300ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" + tokens "gno.land/r/demo/grc20factory" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + beneficiary1 := std.Address("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0") + beneficiary2 := std.Address("g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + tokens.New("test", "TEST", 4, 0, 0) + tokens.Mint("TEST", mainaddr, 200) + + mainbal := tokens.BalanceOf("TEST", mainaddr) + println("main before:", mainbal) + + tokens.Approve("TEST", disperseAddr, 200) + + disperse.DisperseGRC20String("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "150TEST,50TEST") + + mainbal = tokens.BalanceOf("TEST", mainaddr) + println("main after:", mainbal) + ben1bal := tokens.BalanceOf("TEST", beneficiary1) + println("beneficiary1:", ben1bal) + ben2bal := tokens.BalanceOf("TEST", beneficiary2) + println("beneficiary2:", ben2bal) +} + +// Output: +// main before: 200 +// main after: 0 +// beneficiary1: 150 +// beneficiary2: 50 diff --git a/examples/gno.land/r/demo/disperse/z_4_filetest.gno b/examples/gno.land/r/demo/disperse/z_4_filetest.gno new file mode 100644 index 00000000000..ebf4bed4473 --- /dev/null +++ b/examples/gno.land/r/demo/disperse/z_4_filetest.gno @@ -0,0 +1,48 @@ +// SEND: 300ugnot + +package main + +import ( + "std" + + "gno.land/r/demo/disperse" + tokens "gno.land/r/demo/grc20factory" +) + +func main() { + disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") + mainaddr := std.DerivePkgAddr("main") + beneficiary1 := std.Address("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0") + beneficiary2 := std.Address("g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c") + + std.TestSetOrigPkgAddr(disperseAddr) + std.TestSetOrigCaller(mainaddr) + + banker := std.GetBanker(std.BankerTypeRealmSend) + + tokens.New("test1", "TEST1", 4, 0, 0) + tokens.Mint("TEST1", mainaddr, 200) + tokens.New("test2", "TEST2", 4, 0, 0) + tokens.Mint("TEST2", mainaddr, 200) + + mainbal := tokens.BalanceOf("TEST1", mainaddr) + tokens.BalanceOf("TEST2", mainaddr) + println("main before:", mainbal) + + tokens.Approve("TEST1", disperseAddr, 200) + tokens.Approve("TEST2", disperseAddr, 200) + + disperse.DisperseGRC20String("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c", "200TEST1,200TEST2") + + mainbal = tokens.BalanceOf("TEST1", mainaddr) + tokens.BalanceOf("TEST2", mainaddr) + println("main after:", mainbal) + ben1bal := tokens.BalanceOf("TEST1", beneficiary1) + tokens.BalanceOf("TEST2", beneficiary1) + println("beneficiary1:", ben1bal) + ben2bal := tokens.BalanceOf("TEST1", beneficiary2) + tokens.BalanceOf("TEST2", beneficiary2) + println("beneficiary2:", ben2bal) +} + +// Output: +// main before: 400 +// main after: 0 +// beneficiary1: 200 +// beneficiary2: 200 diff --git a/gno.land/cmd/gnoland/genesis_balances_add.go b/gno.land/cmd/gnoland/genesis_balances_add.go index 4c8603c1273..f9a898715c8 100644 --- a/gno.land/cmd/gnoland/genesis_balances_add.go +++ b/gno.land/cmd/gnoland/genesis_balances_add.go @@ -10,6 +10,7 @@ import ( "os" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" @@ -57,13 +58,13 @@ func (c *balancesAddCfg) RegisterFlags(fs *flag.FlagSet) { &c.balanceSheet, "balance-sheet", "", - "the path to the balance file containing addresses in the format
=ugnot", + "the path to the balance file containing addresses in the format
="+ugnot.Denom, ) fs.Var( &c.singleEntries, "single", - "the direct balance addition in the format
=ugnot", + "the direct balance addition in the format
="+ugnot.Denom, ) fs.StringVar( @@ -167,7 +168,7 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) e io.Println() for address, balance := range finalBalances { - io.Printfln("%s:%dugnot", address.String(), balance) + io.Printfln("%s:%d%s", address.String(), balance, ugnot.Denom) } return nil @@ -208,7 +209,7 @@ func getBalancesFromTransactions( } feeAmount := std.NewCoins(tx.Fee.GasFee) - if feeAmount.AmountOf("ugnot") <= 0 { + if feeAmount.AmountOf(ugnot.Denom) <= 0 { io.ErrPrintfln( "invalid gas fee amount encountered: %q", tx.Fee.GasFee.String(), @@ -223,7 +224,7 @@ func getBalancesFromTransactions( msgSend := msg.(bank.MsgSend) sendAmount := msgSend.Amount - if sendAmount.AmountOf("ugnot") <= 0 { + if sendAmount.AmountOf(ugnot.Denom) <= 0 { io.ErrPrintfln( "invalid send amount encountered: %s", msgSend.Amount.String(), @@ -248,7 +249,7 @@ func getBalancesFromTransactions( if from.IsAllLT(sendAmount) || from.IsAllLT(feeAmount) { // Account cannot cover send amount / fee // (see message above) - from = std.NewCoins(std.NewCoin("ugnot", 0)) + from = std.NewCoins(std.NewCoin(ugnot.Denom, 0)) } if from.IsAllGT(sendAmount) { diff --git a/gno.land/cmd/gnoland/genesis_balances_add_test.go b/gno.land/cmd/gnoland/genesis_balances_add_test.go index 9589bf919cc..8f2879f9c57 100644 --- a/gno.land/cmd/gnoland/genesis_balances_add_test.go +++ b/gno.land/cmd/gnoland/genesis_balances_add_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" @@ -100,16 +101,16 @@ func TestGenesis_Balances_Add(t *testing.T) { tempGenesis.Name(), } - amount := std.NewCoins(std.NewCoin("ugnot", 10)) + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) for _, dummyKey := range dummyKeys { args = append(args, "--single") args = append( args, fmt.Sprintf( - "%s=%dugnot", + "%s=%s", dummyKey.Address().String(), - amount.AmountOf("ugnot"), + ugnot.ValueString(amount.AmountOf(ugnot.Denom)), ), ) } @@ -158,7 +159,7 @@ func TestGenesis_Balances_Add(t *testing.T) { require.NoError(t, genesis.SaveAs(tempGenesis.Name())) dummyKeys := getDummyKeys(t, 10) - amount := std.NewCoins(std.NewCoin("ugnot", 10)) + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) balances := make([]string, len(dummyKeys)) @@ -167,9 +168,9 @@ func TestGenesis_Balances_Add(t *testing.T) { for index, key := range dummyKeys { balances[index] = fmt.Sprintf( - "%s=%dugnot", + "%s=%s", key.Address().String(), - amount.AmountOf("ugnot"), + ugnot.ValueString(amount.AmountOf(ugnot.Denom)), ) } @@ -237,9 +238,9 @@ func TestGenesis_Balances_Add(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = std.NewCoins(std.NewCoin("ugnot", 10)) - amountCoins = std.NewCoins(std.NewCoin("ugnot", 10)) - gasFee = std.NewCoin("ugnot", 1000000) + amount = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + gasFee = std.NewCoin(ugnot.Denom, 1000000) txs = make([]std.Tx, 0) ) @@ -316,7 +317,7 @@ func TestGenesis_Balances_Add(t *testing.T) { if index == 0 { // the first address should // have a balance of 0 - checkAmount = std.NewCoins(std.NewCoin("ugnot", 0)) + checkAmount = std.NewCoins(std.NewCoin(ugnot.Denom, 0)) } if dummyKey.Address().String() == balance.Address.String() { @@ -347,7 +348,7 @@ func TestGenesis_Balances_Add(t *testing.T) { Balances: []gnoland.Balance{ { Address: dummyKeys[0].Address(), - Amount: std.NewCoins(std.NewCoin("ugnot", 100)), + Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 100)), }, }, } @@ -364,16 +365,16 @@ func TestGenesis_Balances_Add(t *testing.T) { tempGenesis.Name(), } - amount := std.NewCoins(std.NewCoin("ugnot", 10)) + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) for _, dummyKey := range dummyKeys { args = append(args, "--single") args = append( args, fmt.Sprintf( - "%s=%dugnot", + "%s=%s", dummyKey.Address().String(), - amount.AmountOf("ugnot"), + ugnot.ValueString(amount.AmountOf(ugnot.Denom)), ), ) } @@ -421,9 +422,9 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = std.NewCoins(std.NewCoin("ugnot", 10)) - amountCoins = std.NewCoins(std.NewCoin("ugnot", 10)) - gasFee = std.NewCoin("ugnot", 1000000) + amount = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + gasFee = std.NewCoin(ugnot.Denom, 1000000) txs = make([]std.Tx, 0) ) @@ -479,7 +480,7 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amountCoins = std.NewCoins(std.NewCoin("ugnot", 10)) + amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) gasFee = std.NewCoin("gnos", 1) // invalid fee txs = make([]std.Tx, 0) ) @@ -532,7 +533,7 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) amountCoins = std.NewCoins(std.NewCoin("gnogno", 10)) // invalid send amount - gasFee = std.NewCoin("ugnot", 1) + gasFee = std.NewCoin(ugnot.Denom, 1) txs = make([]std.Tx, 0) ) diff --git a/gno.land/cmd/gnoland/genesis_balances_export_test.go b/gno.land/cmd/gnoland/genesis_balances_export_test.go index ee88af4c56b..bd1f6152246 100644 --- a/gno.land/cmd/gnoland/genesis_balances_export_test.go +++ b/gno.land/cmd/gnoland/genesis_balances_export_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/testutils" @@ -18,7 +19,7 @@ func getDummyBalances(t *testing.T, count int) []gnoland.Balance { t.Helper() dummyKeys := getDummyKeys(t, count) - amount := std.NewCoins(std.NewCoin("ugnot", 10)) + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) balances := make([]gnoland.Balance, len(dummyKeys)) diff --git a/gno.land/cmd/gnoland/genesis_balances_remove_test.go b/gno.land/cmd/gnoland/genesis_balances_remove_test.go index a8ec6ddac10..ed11836ba4d 100644 --- a/gno.land/cmd/gnoland/genesis_balances_remove_test.go +++ b/gno.land/cmd/gnoland/genesis_balances_remove_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/std" @@ -75,7 +76,7 @@ func TestGenesis_Balances_Remove(t *testing.T) { Balances: []gnoland.Balance{ { Address: dummyKey.Address(), - Amount: std.NewCoins(std.NewCoin("ugnot", 100)), + Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 100)), }, }, } diff --git a/gno.land/cmd/gnoland/genesis_txs_add_packages.go b/gno.land/cmd/gnoland/genesis_txs_add_packages.go index 93246eadff5..56d165c070b 100644 --- a/gno.land/cmd/gnoland/genesis_txs_add_packages.go +++ b/gno.land/cmd/gnoland/genesis_txs_add_packages.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -16,7 +17,7 @@ var errInvalidPackageDir = errors.New("invalid package directory") var ( genesisDeployAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 - genesisDeployFee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) ) // newTxsAddPackagesCmd creates the genesis txs add packages subcommand diff --git a/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go b/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go index 1d49422afd1..a70446cfe6c 100644 --- a/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go +++ b/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" @@ -30,12 +31,12 @@ func generateDummyTxs(t *testing.T, count int) []std.Tx { bank.MsgSend{ FromAddress: crypto.Address{byte(i)}, ToAddress: crypto.Address{byte((i + 1) % count)}, - Amount: std.NewCoins(std.NewCoin("ugnot", 1)), + Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 1)), }, }, Fee: std.Fee{ GasWanted: 1, - GasFee: std.NewCoin("ugnot", 1000000), + GasFee: std.NewCoin(ugnot.Denom, 1000000), }, Memo: fmt.Sprintf("tx %d", i), } diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go index 8aef07451d6..b7eb21837a7 100644 --- a/gno.land/pkg/gnoclient/client_test.go +++ b/gno.land/pkg/gnoclient/client_test.go @@ -6,14 +6,19 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" ) +var testGasFee = ugnot.ValueString(10000) + func TestRender(t *testing.T) { t.Parallel() testRealmPath := "gno.land/r/demo/deep/very/deep" @@ -88,25 +93,35 @@ func TestCallSingle(t *testing.T) { cfg := BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", } - msg := []MsgCall{ + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := []vm.MsgCall{ { - PkgPath: "gno.land/r/demo/deep/very/deep", - FuncName: "Render", - Args: []string{""}, - Send: "100ugnot", + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{""}, + Send: std.Coins{{Denom: ugnot.Denom, Amount: int64(100)}}, }, } res, err := client.Call(cfg, msg...) assert.NoError(t, err) require.NotNil(t, res) - assert.Equal(t, string(res.DeliverTx.Data), "it works!") + expected := "it works!" + assert.Equal(t, string(res.DeliverTx.Data), expected) + + res, err = callSigningSeparately(t, client, cfg, msg...) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, string(res.DeliverTx.Data), expected) } func TestCallMultiple(t *testing.T) { @@ -147,47 +162,60 @@ func TestCallMultiple(t *testing.T) { cfg := BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", } - msg := []MsgCall{ + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := []vm.MsgCall{ { - PkgPath: "gno.land/r/demo/deep/very/deep", - FuncName: "Render", - Args: []string{""}, - Send: "100ugnot", + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{""}, + Send: std.Coins{{Denom: ugnot.Denom, Amount: int64(100)}}, }, { - PkgPath: "gno.land/r/demo/wugnot", - FuncName: "Deposit", - Args: []string{""}, - Send: "1000ugnot", + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/wugnot", + Func: "Deposit", + Args: []string{""}, + Send: std.Coins{{Denom: ugnot.Denom, Amount: int64(1000)}}, }, { - PkgPath: "gno.land/r/demo/tamagotchi", - FuncName: "Feed", - Args: []string{""}, - Send: "", + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/tamagotchi", + Func: "Feed", + Args: []string{""}, + Send: nil, }, } res, err := client.Call(cfg, msg...) assert.NoError(t, err) assert.NotNil(t, res) + + res, err = callSigningSeparately(t, client, cfg, msg...) + assert.NoError(t, err) + assert.NotNil(t, res) } func TestCallErrors(t *testing.T) { t.Parallel() + // These tests don't actually sign + mockAddress, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + testCases := []struct { name string client Client cfg BaseTxCfg - msgs []MsgCall - expectedError error + msgs []vm.MsgCall + expectedError string }{ { name: "Invalid Signer", @@ -197,20 +225,21 @@ func TestCallErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgCall{ + msgs: []vm.MsgCall{ { - PkgPath: "random/path", - FuncName: "RandomName", - Send: "", - Args: []string{}, + Caller: mockAddress, + PkgPath: "gno.land/r/random/path", + Func: "RandomName", + Send: nil, + Args: []string{}, }, }, - expectedError: ErrMissingSigner, + expectedError: ErrMissingSigner.Error(), }, { name: "Invalid RPCClient", @@ -220,20 +249,21 @@ func TestCallErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgCall{ + msgs: []vm.MsgCall{ { - PkgPath: "random/path", - FuncName: "RandomName", - Send: "", - Args: []string{}, + Caller: mockAddress, + PkgPath: "gno.land/r/random/path", + Func: "RandomName", + Send: nil, + Args: []string{}, }, }, - expectedError: ErrMissingRPCClient, + expectedError: ErrMissingRPCClient.Error(), }, { name: "Invalid Gas Fee", @@ -248,13 +278,14 @@ func TestCallErrors(t *testing.T) { SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgCall{ + msgs: []vm.MsgCall{ { - PkgPath: "random/path", - FuncName: "RandomName", + Caller: mockAddress, + PkgPath: "gno.land/r/random/path", + Func: "RandomName", }, }, - expectedError: ErrInvalidGasFee, + expectedError: ErrInvalidGasFee.Error(), }, { name: "Negative Gas Wanted", @@ -264,20 +295,21 @@ func TestCallErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: -1, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgCall{ + msgs: []vm.MsgCall{ { - PkgPath: "random/path", - FuncName: "RandomName", - Send: "", - Args: []string{}, + Caller: mockAddress, + PkgPath: "gno.land/r/random/path", + Func: "RandomName", + Send: nil, + Args: []string{}, }, }, - expectedError: ErrInvalidGasWanted, + expectedError: ErrInvalidGasWanted.Error(), }, { name: "0 Gas Wanted", @@ -287,20 +319,21 @@ func TestCallErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 0, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgCall{ + msgs: []vm.MsgCall{ { - PkgPath: "random/path", - FuncName: "RandomName", - Send: "", - Args: []string{}, + Caller: mockAddress, + PkgPath: "gno.land/r/random/path", + Func: "RandomName", + Send: nil, + Args: []string{}, }, }, - expectedError: ErrInvalidGasWanted, + expectedError: ErrInvalidGasWanted.Error(), }, { name: "Invalid PkgPath", @@ -310,20 +343,21 @@ func TestCallErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgCall{ + msgs: []vm.MsgCall{ { - PkgPath: "", - FuncName: "RandomName", - Send: "", - Args: []string{}, + Caller: mockAddress, + PkgPath: "", + Func: "RandomName", + Send: nil, + Args: []string{}, }, }, - expectedError: ErrEmptyPkgPath, + expectedError: vm.InvalidPkgPathError{}.Error(), }, { name: "Invalid FuncName", @@ -333,20 +367,21 @@ func TestCallErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgCall{ + msgs: []vm.MsgCall{ { - PkgPath: "random/path", - FuncName: "", - Send: "", - Args: []string{}, + Caller: mockAddress, + PkgPath: "gno.land/r/random/path", + Func: "", + Send: nil, + Args: []string{}, }, }, - expectedError: ErrEmptyFuncName, + expectedError: vm.InvalidExprError{}.Error(), }, } @@ -357,7 +392,7 @@ func TestCallErrors(t *testing.T) { res, err := tc.client.Call(tc.cfg, tc.msgs...) assert.Nil(t, res) - assert.ErrorIs(t, err, tc.expectedError) + assert.ErrorContains(t, err, tc.expectedError) }) } } @@ -365,13 +400,16 @@ func TestCallErrors(t *testing.T) { func TestClient_Send_Errors(t *testing.T) { t.Parallel() + // These tests don't actually sign + mockAddress, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + toAddress, _ := crypto.AddressFromBech32("g14a0y9a64dugh3l7hneshdxr4w0rfkkww9ls35p") testCases := []struct { name string client Client cfg BaseTxCfg - msgs []MsgSend - expectedError error + msgs []bank.MsgSend + expectedError string }{ { name: "Invalid Signer", @@ -381,18 +419,19 @@ func TestClient_Send_Errors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgSend{ + msgs: []bank.MsgSend{ { - ToAddress: toAddress, - Send: "1ugnot", + FromAddress: mockAddress, + ToAddress: toAddress, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, }, }, - expectedError: ErrMissingSigner, + expectedError: ErrMissingSigner.Error(), }, { name: "Invalid RPCClient", @@ -402,18 +441,19 @@ func TestClient_Send_Errors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgSend{ + msgs: []bank.MsgSend{ { - ToAddress: toAddress, - Send: "1ugnot", + FromAddress: mockAddress, + ToAddress: toAddress, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, }, }, - expectedError: ErrMissingRPCClient, + expectedError: ErrMissingRPCClient.Error(), }, { name: "Invalid Gas Fee", @@ -428,13 +468,14 @@ func TestClient_Send_Errors(t *testing.T) { SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgSend{ + msgs: []bank.MsgSend{ { - ToAddress: toAddress, - Send: "1ugnot", + FromAddress: mockAddress, + ToAddress: toAddress, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, }, }, - expectedError: ErrInvalidGasFee, + expectedError: ErrInvalidGasFee.Error(), }, { name: "Negative Gas Wanted", @@ -444,18 +485,19 @@ func TestClient_Send_Errors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: -1, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgSend{ + msgs: []bank.MsgSend{ { - ToAddress: toAddress, - Send: "1ugnot", + FromAddress: mockAddress, + ToAddress: toAddress, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, }, }, - expectedError: ErrInvalidGasWanted, + expectedError: ErrInvalidGasWanted.Error(), }, { name: "0 Gas Wanted", @@ -465,18 +507,19 @@ func TestClient_Send_Errors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 0, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgSend{ + msgs: []bank.MsgSend{ { - ToAddress: toAddress, - Send: "1ugnot", + FromAddress: mockAddress, + ToAddress: toAddress, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, }, }, - expectedError: ErrInvalidGasWanted, + expectedError: ErrInvalidGasWanted.Error(), }, { name: "Invalid To Address", @@ -495,18 +538,19 @@ func TestClient_Send_Errors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgSend{ + msgs: []bank.MsgSend{ { - ToAddress: crypto.Address{}, - Send: "1ugnot", + FromAddress: mockAddress, + ToAddress: crypto.Address{}, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(1)}}, }, }, - expectedError: ErrInvalidToAddress, + expectedError: std.InvalidAddressError{}.Error(), }, { name: "Invalid Send Coins", @@ -525,18 +569,19 @@ func TestClient_Send_Errors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgSend{ + msgs: []bank.MsgSend{ { - ToAddress: toAddress, - Send: "-1ugnot", + FromAddress: mockAddress, + ToAddress: toAddress, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(-1)}}, }, }, - expectedError: ErrInvalidSendAmount, + expectedError: std.InvalidCoinsError{}.Error(), }, } @@ -547,7 +592,7 @@ func TestClient_Send_Errors(t *testing.T) { res, err := tc.client.Send(tc.cfg, tc.msgs...) assert.Nil(t, res) - assert.ErrorIs(t, err, tc.expectedError) + assert.ErrorContains(t, err, tc.expectedError) }) } } @@ -586,7 +631,7 @@ func TestRunSingle(t *testing.T) { cfg := BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", @@ -602,7 +647,11 @@ func main() { println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) }` - msg := MsgRun{ + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg := vm.MsgRun{ + Caller: caller.GetAddress(), Package: &std.MemPackage{ Files: []*std.MemFile{ { @@ -611,13 +660,19 @@ func main() { }, }, }, - Send: "", + Send: nil, } res, err := client.Run(cfg, msg) assert.NoError(t, err) require.NotNil(t, res) - assert.Equal(t, "hi gnoclient!\n", string(res.DeliverTx.Data)) + expected := "hi gnoclient!\n" + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = runSigningSeparately(t, client, cfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) } func TestRunMultiple(t *testing.T) { @@ -653,7 +708,7 @@ func TestRunMultiple(t *testing.T) { cfg := BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", @@ -669,7 +724,11 @@ func main() { println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) }` - msg1 := MsgRun{ + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg1 := vm.MsgRun{ + Caller: caller.GetAddress(), Package: &std.MemPackage{ Files: []*std.MemFile{ { @@ -678,10 +737,11 @@ func main() { }, }, }, - Send: "", + Send: nil, } - msg2 := MsgRun{ + msg2 := vm.MsgRun{ + Caller: caller.GetAddress(), Package: &std.MemPackage{ Files: []*std.MemFile{ { @@ -690,24 +750,33 @@ func main() { }, }, }, - Send: "", + Send: nil, } res, err := client.Run(cfg, msg1, msg2) assert.NoError(t, err) require.NotNil(t, res) - assert.Equal(t, "hi gnoclient!\nhi gnoclient!\n", string(res.DeliverTx.Data)) + expected := "hi gnoclient!\nhi gnoclient!\n" + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = runSigningSeparately(t, client, cfg, msg1, msg2) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) } func TestRunErrors(t *testing.T) { t.Parallel() + // These tests don't actually sign + mockAddress, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + testCases := []struct { name string client Client cfg BaseTxCfg - msgs []MsgRun - expectedError error + msgs []vm.MsgRun + expectedError string }{ { name: "Invalid Signer", @@ -717,13 +786,14 @@ func TestRunErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgRun{ + msgs: []vm.MsgRun{ { + Caller: mockAddress, Package: &std.MemPackage{ Name: "", Path: "", @@ -734,10 +804,10 @@ func TestRunErrors(t *testing.T) { }, }, }, - Send: "", + Send: nil, }, }, - expectedError: ErrMissingSigner, + expectedError: ErrMissingSigner.Error(), }, { name: "Invalid RPCClient", @@ -747,13 +817,13 @@ func TestRunErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgRun{}, - expectedError: ErrMissingRPCClient, + msgs: []vm.MsgRun{}, + expectedError: ErrMissingRPCClient.Error(), }, { name: "Invalid Gas Fee", @@ -768,8 +838,9 @@ func TestRunErrors(t *testing.T) { SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgRun{ + msgs: []vm.MsgRun{ { + Caller: mockAddress, Package: &std.MemPackage{ Name: "", Path: "", @@ -780,10 +851,10 @@ func TestRunErrors(t *testing.T) { }, }, }, - Send: "", + Send: nil, }, }, - expectedError: ErrInvalidGasFee, + expectedError: ErrInvalidGasFee.Error(), }, { name: "Negative Gas Wanted", @@ -793,13 +864,14 @@ func TestRunErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: -1, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgRun{ + msgs: []vm.MsgRun{ { + Caller: mockAddress, Package: &std.MemPackage{ Name: "", Path: "", @@ -810,10 +882,10 @@ func TestRunErrors(t *testing.T) { }, }, }, - Send: "", + Send: nil, }, }, - expectedError: ErrInvalidGasWanted, + expectedError: ErrInvalidGasWanted.Error(), }, { name: "0 Gas Wanted", @@ -823,13 +895,14 @@ func TestRunErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 0, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgRun{ + msgs: []vm.MsgRun{ { + Caller: mockAddress, Package: &std.MemPackage{ Name: "", Path: "", @@ -840,10 +913,10 @@ func TestRunErrors(t *testing.T) { }, }, }, - Send: "", + Send: nil, }, }, - expectedError: ErrInvalidGasWanted, + expectedError: ErrInvalidGasWanted.Error(), }, { name: "Invalid Empty Package", @@ -862,18 +935,19 @@ func TestRunErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgRun{ + msgs: []vm.MsgRun{ { - Package: nil, - Send: "", + Caller: mockAddress, + Package: &std.MemPackage{Name: "", Path: " "}, + Send: nil, }, }, - expectedError: ErrEmptyPackage, + expectedError: vm.InvalidPkgPathError{}.Error(), }, } @@ -884,7 +958,7 @@ func TestRunErrors(t *testing.T) { res, err := tc.client.Run(tc.cfg, tc.msgs...) assert.Nil(t, res) - assert.ErrorIs(t, err, tc.expectedError) + assert.ErrorContains(t, err, tc.expectedError) }) } } @@ -893,12 +967,15 @@ func TestRunErrors(t *testing.T) { func TestAddPackageErrors(t *testing.T) { t.Parallel() + // These tests don't actually sign + mockAddress, _ := crypto.AddressFromBech32("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + testCases := []struct { name string client Client cfg BaseTxCfg - msgs []MsgAddPackage - expectedError error + msgs []vm.MsgAddPackage + expectedError string }{ { name: "Invalid Signer", @@ -908,13 +985,14 @@ func TestAddPackageErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgAddPackage{ + msgs: []vm.MsgAddPackage{ { + Creator: mockAddress, Package: &std.MemPackage{ Name: "", Path: "", @@ -925,10 +1003,10 @@ func TestAddPackageErrors(t *testing.T) { }, }, }, - Deposit: "", + Deposit: nil, }, }, - expectedError: ErrMissingSigner, + expectedError: ErrMissingSigner.Error(), }, { name: "Invalid RPCClient", @@ -938,13 +1016,13 @@ func TestAddPackageErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgAddPackage{}, - expectedError: ErrMissingRPCClient, + msgs: []vm.MsgAddPackage{}, + expectedError: ErrMissingRPCClient.Error(), }, { name: "Invalid Gas Fee", @@ -959,8 +1037,9 @@ func TestAddPackageErrors(t *testing.T) { SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgAddPackage{ + msgs: []vm.MsgAddPackage{ { + Creator: mockAddress, Package: &std.MemPackage{ Name: "", Path: "", @@ -971,10 +1050,10 @@ func TestAddPackageErrors(t *testing.T) { }, }, }, - Deposit: "", + Deposit: nil, }, }, - expectedError: ErrInvalidGasFee, + expectedError: ErrInvalidGasFee.Error(), }, { name: "Negative Gas Wanted", @@ -984,13 +1063,14 @@ func TestAddPackageErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: -1, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgAddPackage{ + msgs: []vm.MsgAddPackage{ { + Creator: mockAddress, Package: &std.MemPackage{ Name: "", Path: "", @@ -1001,10 +1081,10 @@ func TestAddPackageErrors(t *testing.T) { }, }, }, - Deposit: "", + Deposit: nil, }, }, - expectedError: ErrInvalidGasWanted, + expectedError: ErrInvalidGasWanted.Error(), }, { name: "0 Gas Wanted", @@ -1014,13 +1094,14 @@ func TestAddPackageErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 0, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgAddPackage{ + msgs: []vm.MsgAddPackage{ { + Creator: mockAddress, Package: &std.MemPackage{ Name: "", Path: "", @@ -1031,10 +1112,10 @@ func TestAddPackageErrors(t *testing.T) { }, }, }, - Deposit: "", + Deposit: nil, }, }, - expectedError: ErrInvalidGasWanted, + expectedError: ErrInvalidGasWanted.Error(), }, { name: "Invalid Empty Package", @@ -1053,18 +1134,19 @@ func TestAddPackageErrors(t *testing.T) { }, cfg: BaseTxCfg{ GasWanted: 100000, - GasFee: "10000ugnot", + GasFee: testGasFee, AccountNumber: 1, SequenceNumber: 1, Memo: "Test memo", }, - msgs: []MsgAddPackage{ + msgs: []vm.MsgAddPackage{ { - Package: nil, - Deposit: "", + Creator: mockAddress, + Package: &std.MemPackage{Name: "", Path: ""}, + Deposit: nil, }, }, - expectedError: ErrEmptyPackage, + expectedError: vm.InvalidPkgPathError{}.Error(), }, } @@ -1075,7 +1157,7 @@ func TestAddPackageErrors(t *testing.T) { res, err := tc.client.AddPackage(tc.cfg, tc.msgs...) assert.Nil(t, res) - assert.ErrorIs(t, err, tc.expectedError) + assert.ErrorContains(t, err, tc.expectedError) }) } } @@ -1266,3 +1348,63 @@ func TestLatestBlockHeightErrors(t *testing.T) { }) } } + +// The same as client.Call, but test signing separately +func callSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewCallTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTxCommit(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +// The same as client.Run, but test signing separately +func runSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewRunTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTxCommit(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +// The same as client.Send, but test signing separately +func sendSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewSendTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTxCommit(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +// The same as client.AddPackage, but test signing separately +func addPackageSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewAddPackageTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTxCommit(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go index a32a6899abe..9d3dbde22ae 100644 --- a/gno.land/pkg/gnoclient/client_txs.go +++ b/gno.land/pkg/gnoclient/client_txs.go @@ -4,22 +4,16 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" ) var ( - ErrEmptyPackage = errors.New("empty package to run") - ErrEmptyPkgPath = errors.New("empty pkg path") - ErrEmptyFuncName = errors.New("empty function name") - ErrInvalidGasWanted = errors.New("invalid gas wanted") - ErrInvalidGasFee = errors.New("invalid gas fee") - ErrMissingSigner = errors.New("missing Signer") - ErrMissingRPCClient = errors.New("missing RPCClient") - ErrInvalidToAddress = errors.New("invalid send to address") - ErrInvalidSendAmount = errors.New("invalid send amount") + ErrInvalidGasWanted = errors.New("invalid gas wanted") + ErrInvalidGasFee = errors.New("invalid gas fee") + ErrMissingSigner = errors.New("missing Signer") + ErrMissingRPCClient = errors.New("missing RPCClient") ) // BaseTxCfg defines the base transaction configuration, shared by all message types @@ -31,34 +25,8 @@ type BaseTxCfg struct { Memo string // Memo } -// MsgCall - syntax sugar for vm.MsgCall -type MsgCall struct { - PkgPath string // Package path - FuncName string // Function name - Args []string // Function arguments - Send string // Send amount -} - -// MsgSend - syntax sugar for bank.MsgSend -type MsgSend struct { - ToAddress crypto.Address // Send to address - Send string // Send amount -} - -// MsgRun - syntax sugar for vm.MsgRun -type MsgRun struct { - Package *std.MemPackage // Package to run - Send string // Send amount -} - -// MsgAddPackage - syntax sugar for vm.MsgAddPackage -type MsgAddPackage struct { - Package *std.MemPackage // Package to add - Deposit string // Coin deposit -} - // Call executes one or more MsgCall calls on the blockchain -func (c *Client) Call(cfg BaseTxCfg, msgs ...MsgCall) (*ctypes.ResultBroadcastTxCommit, error) { +func (c *Client) Call(cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. if err := c.validateSigner(); err != nil { return nil, err @@ -67,38 +35,29 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...MsgCall) (*ctypes.ResultBroadcastTx return nil, err } + tx, err := NewCallTx(cfg, msgs...) + if err != nil { + return nil, err + } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) +} + +// NewCallTx makes an unsigned transaction from one or more MsgCall. +// The Caller field must be set. +func NewCallTx(cfg BaseTxCfg, msgs ...vm.MsgCall) (*std.Tx, error) { // Validate base transaction config if err := cfg.validateBaseTxConfig(); err != nil { return nil, err } - // Parse MsgCall slice vmMsgs := make([]std.Msg, 0, len(msgs)) for _, msg := range msgs { // Validate MsgCall fields - if err := msg.validateMsgCall(); err != nil { - return nil, err - } - - // Parse send coins - send, err := std.ParseCoins(msg.Send) - if err != nil { - return nil, err - } - - caller, err := c.Signer.Info() - if err != nil { + if err := msg.ValidateBasic(); err != nil { return nil, err } - // Unwrap syntax sugar to vm.MsgCall slice - vmMsgs = append(vmMsgs, std.Msg(vm.MsgCall{ - Caller: caller.GetAddress(), - PkgPath: msg.PkgPath, - Func: msg.FuncName, - Args: msg.Args, - Send: send, - })) + vmMsgs = append(vmMsgs, std.Msg(msg)) } // Parse gas fee @@ -108,18 +67,16 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...MsgCall) (*ctypes.ResultBroadcastTx } // Pack transaction - tx := std.Tx{ + return &std.Tx{ Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, - } - - return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) + }, nil } // Run executes one or more MsgRun calls on the blockchain -func (c *Client) Run(cfg BaseTxCfg, msgs ...MsgRun) (*ctypes.ResultBroadcastTxCommit, error) { +func (c *Client) Run(cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. if err := c.validateSigner(); err != nil { return nil, err @@ -128,39 +85,29 @@ func (c *Client) Run(cfg BaseTxCfg, msgs ...MsgRun) (*ctypes.ResultBroadcastTxCo return nil, err } + tx, err := NewRunTx(cfg, msgs...) + if err != nil { + return nil, err + } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) +} + +// NewRunTx makes an unsigned transaction from one or more MsgRun. +// The Caller field must be set. +func NewRunTx(cfg BaseTxCfg, msgs ...vm.MsgRun) (*std.Tx, error) { // Validate base transaction config if err := cfg.validateBaseTxConfig(); err != nil { return nil, err } - // Parse MsgRun slice vmMsgs := make([]std.Msg, 0, len(msgs)) for _, msg := range msgs { - // Validate MsgCall fields - if err := msg.validateMsgRun(); err != nil { + // Validate MsgRun fields + if err := msg.ValidateBasic(); err != nil { return nil, err } - // Parse send coins - send, err := std.ParseCoins(msg.Send) - if err != nil { - return nil, err - } - - caller, err := c.Signer.Info() - if err != nil { - return nil, err - } - - msg.Package.Name = "main" - msg.Package.Path = "" - - // Unwrap syntax sugar to vm.MsgCall slice - vmMsgs = append(vmMsgs, std.Msg(vm.MsgRun{ - Caller: caller.GetAddress(), - Package: msg.Package, - Send: send, - })) + vmMsgs = append(vmMsgs, std.Msg(msg)) } // Parse gas fee @@ -170,18 +117,16 @@ func (c *Client) Run(cfg BaseTxCfg, msgs ...MsgRun) (*ctypes.ResultBroadcastTxCo } // Pack transaction - tx := std.Tx{ + return &std.Tx{ Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, - } - - return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) + }, nil } // Send executes one or more MsgSend calls on the blockchain -func (c *Client) Send(cfg BaseTxCfg, msgs ...MsgSend) (*ctypes.ResultBroadcastTxCommit, error) { +func (c *Client) Send(cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. if err := c.validateSigner(); err != nil { return nil, err @@ -190,36 +135,29 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...MsgSend) (*ctypes.ResultBroadcastTx return nil, err } + tx, err := NewSendTx(cfg, msgs...) + if err != nil { + return nil, err + } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) +} + +// NewSendTx makes an unsigned transaction from one or more MsgSend. +// The FromAddress field must be set. +func NewSendTx(cfg BaseTxCfg, msgs ...bank.MsgSend) (*std.Tx, error) { // Validate base transaction config if err := cfg.validateBaseTxConfig(); err != nil { return nil, err } - // Parse MsgSend slice vmMsgs := make([]std.Msg, 0, len(msgs)) for _, msg := range msgs { // Validate MsgSend fields - if err := msg.validateMsgSend(); err != nil { - return nil, err - } - - // Parse send coins - send, err := std.ParseCoins(msg.Send) - if err != nil { - return nil, err - } - - caller, err := c.Signer.Info() - if err != nil { + if err := msg.ValidateBasic(); err != nil { return nil, err } - // Unwrap syntax sugar to vm.MsgSend slice - vmMsgs = append(vmMsgs, std.Msg(bank.MsgSend{ - FromAddress: caller.GetAddress(), - ToAddress: msg.ToAddress, - Amount: send, - })) + vmMsgs = append(vmMsgs, std.Msg(msg)) } // Parse gas fee @@ -229,18 +167,16 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...MsgSend) (*ctypes.ResultBroadcastTx } // Pack transaction - tx := std.Tx{ + return &std.Tx{ Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, - } - - return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) + }, nil } // AddPackage executes one or more AddPackage calls on the blockchain -func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...MsgAddPackage) (*ctypes.ResultBroadcastTxCommit, error) { +func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.ResultBroadcastTxCommit, error) { // Validate required client fields. if err := c.validateSigner(); err != nil { return nil, err @@ -249,36 +185,29 @@ func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...MsgAddPackage) (*ctypes.Resul return nil, err } + tx, err := NewAddPackageTx(cfg, msgs...) + if err != nil { + return nil, err + } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) +} + +// NewAddPackageTx makes an unsigned transaction from one or more MsgAddPackage. +// The Creator field must be set. +func NewAddPackageTx(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*std.Tx, error) { // Validate base transaction config if err := cfg.validateBaseTxConfig(); err != nil { return nil, err } - // Parse MsgRun slice vmMsgs := make([]std.Msg, 0, len(msgs)) for _, msg := range msgs { - // Validate MsgCall fields - if err := msg.validateMsgAddPackage(); err != nil { + // Validate MsgAddPackage fields + if err := msg.ValidateBasic(); err != nil { return nil, err } - // Parse deposit coins - deposit, err := std.ParseCoins(msg.Deposit) - if err != nil { - return nil, err - } - - caller, err := c.Signer.Info() - if err != nil { - return nil, err - } - - // Unwrap syntax sugar to vm.MsgCall slice - vmMsgs = append(vmMsgs, std.Msg(vm.MsgAddPackage{ - Creator: caller.GetAddress(), - Package: msg.Package, - Deposit: deposit, - })) + vmMsgs = append(vmMsgs, std.Msg(msg)) } // Parse gas fee @@ -288,18 +217,29 @@ func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...MsgAddPackage) (*ctypes.Resul } // Pack transaction - tx := std.Tx{ + return &std.Tx{ Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, - } - - return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) + }, nil } // signAndBroadcastTxCommit signs a transaction and broadcasts it, returning the result func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) { + signedTx, err := c.SignTx(tx, accountNumber, sequenceNumber) + if err != nil { + return nil, err + } + return c.BroadcastTxCommit(signedTx) +} + +// SignTx signs a transaction and returns a signed tx ready for broadcasting. +// If accountNumber or sequenceNumber is 0 then query the blockchain for the value. +func (c *Client) SignTx(tx std.Tx, accountNumber, sequenceNumber uint64) (*std.Tx, error) { + if err := c.validateSigner(); err != nil { + return nil, err + } caller, err := c.Signer.Info() if err != nil { return nil, err @@ -323,7 +263,15 @@ func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumb if err != nil { return nil, errors.Wrap(err, "sign") } + return signedTx, nil +} +// BroadcastTxCommit marshals and broadcasts the signed transaction, returning the result. +// If the result has a delivery error, then return a wrapped error. +func (c *Client) BroadcastTxCommit(signedTx *std.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + if err := c.validateRPCClient(); err != nil { + return nil, err + } bz, err := amino.Marshal(signedTx) if err != nil { return nil, errors.Wrap(err, "marshaling tx binary bytes") diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index 06360244b7f..ea068e0680b 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -5,9 +5,12 @@ import ( "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -36,19 +39,23 @@ func TestCallSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: "10000ugnot", + GasFee: ugnot.ValueString(10000), GasWanted: 8000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", } + caller, err := client.Signer.Info() + require.NoError(t, err) + // Make Msg config - msg := MsgCall{ - PkgPath: "gno.land/r/demo/deep/very/deep", - FuncName: "Render", - Args: []string{"test argument"}, - Send: "", + msg := vm.MsgCall{ + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{"test argument"}, + Send: nil, } // Execute call @@ -59,6 +66,11 @@ func TestCallSingle_Integration(t *testing.T) { got := string(res.DeliverTx.Data) assert.Equal(t, expected, got) + + res, err = callSigningSeparately(t, client, baseCfg, msg) + require.NoError(t, err) + got = string(res.DeliverTx.Data) + assert.Equal(t, expected, got) } func TestCallMultiple_Integration(t *testing.T) { @@ -80,27 +92,32 @@ func TestCallMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: "10000ugnot", + GasFee: ugnot.ValueString(10000), GasWanted: 8000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", } + caller, err := client.Signer.Info() + require.NoError(t, err) + // Make Msg configs - msg1 := MsgCall{ - PkgPath: "gno.land/r/demo/deep/very/deep", - FuncName: "Render", - Args: []string{""}, - Send: "", + msg1 := vm.MsgCall{ + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{""}, + Send: nil, } // Same call, different argument - msg2 := MsgCall{ - PkgPath: "gno.land/r/demo/deep/very/deep", - FuncName: "Render", - Args: []string{"test argument"}, - Send: "", + msg2 := vm.MsgCall{ + Caller: caller.GetAddress(), + PkgPath: "gno.land/r/demo/deep/very/deep", + Func: "Render", + Args: []string{"test argument"}, + Send: nil, } expected := "(\"it works!\" string)\n\n(\"hi test argument\" string)\n\n" @@ -111,6 +128,11 @@ func TestCallMultiple_Integration(t *testing.T) { got := string(res.DeliverTx.Data) assert.Equal(t, expected, got) + + res, err = callSigningSeparately(t, client, baseCfg, msg1, msg2) + require.NoError(t, err) + got = string(res.DeliverTx.Data) + assert.Equal(t, expected, got) } func TestSendSingle_Integration(t *testing.T) { @@ -132,19 +154,23 @@ func TestSendSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: "10000ugnot", + GasFee: ugnot.ValueString(10000), GasWanted: 8000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", } + caller, err := client.Signer.Info() + require.NoError(t, err) + // Make Send config for a new address on the blockchain toAddress, _ := crypto.AddressFromBech32("g14a0y9a64dugh3l7hneshdxr4w0rfkkww9ls35p") amount := 10 - msg := MsgSend{ - ToAddress: toAddress, - Send: std.Coin{"ugnot", int64(amount)}.String(), + msg := bank.MsgSend{ + FromAddress: caller.GetAddress(), + ToAddress: toAddress, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(amount)}}, } // Execute send @@ -156,10 +182,21 @@ func TestSendSingle_Integration(t *testing.T) { account, _, err := client.QueryAccount(toAddress) require.NoError(t, err) - expected := std.Coins{{"ugnot", int64(amount)}} + expected := std.Coins{{Denom: ugnot.Denom, Amount: int64(amount)}} got := account.GetCoins() assert.Equal(t, expected, got) + + res, err = sendSigningSeparately(t, client, baseCfg, msg) + require.NoError(t, err) + assert.Equal(t, "", string(res.DeliverTx.Data)) + + // Get the new account balance + account, _, err = client.QueryAccount(toAddress) + require.NoError(t, err) + expected2 := std.Coins{{Denom: ugnot.Denom, Amount: int64(2 * amount)}} + got = account.GetCoins() + assert.Equal(t, expected2, got) } func TestSendMultiple_Integration(t *testing.T) { @@ -181,26 +218,31 @@ func TestSendMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: "10000ugnot", + GasFee: ugnot.ValueString(10000), GasWanted: 8000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", } + caller, err := client.Signer.Info() + require.NoError(t, err) + // Make Msg configs toAddress, _ := crypto.AddressFromBech32("g14a0y9a64dugh3l7hneshdxr4w0rfkkww9ls35p") amount1 := 10 - msg1 := MsgSend{ - ToAddress: toAddress, - Send: std.Coin{"ugnot", int64(amount1)}.String(), + msg1 := bank.MsgSend{ + FromAddress: caller.GetAddress(), + ToAddress: toAddress, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(amount1)}}, } // Same send, different argument amount2 := 20 - msg2 := MsgSend{ - ToAddress: toAddress, - Send: std.Coin{"ugnot", int64(amount2)}.String(), + msg2 := bank.MsgSend{ + FromAddress: caller.GetAddress(), + ToAddress: toAddress, + Amount: std.Coins{{Denom: ugnot.Denom, Amount: int64(amount2)}}, } // Execute send @@ -212,10 +254,21 @@ func TestSendMultiple_Integration(t *testing.T) { account, _, err := client.QueryAccount(toAddress) assert.NoError(t, err) - expected := std.Coins{{"ugnot", int64(amount1 + amount2)}} + expected := std.Coins{{Denom: ugnot.Denom, Amount: int64(amount1 + amount2)}} got := account.GetCoins() assert.Equal(t, expected, got) + + res, err = sendSigningSeparately(t, client, baseCfg, msg1, msg2) + require.NoError(t, err) + assert.Equal(t, "", string(res.DeliverTx.Data)) + + // Get the new account balance + account, _, err = client.QueryAccount(toAddress) + require.NoError(t, err) + expected2 := std.Coins{{Denom: ugnot.Denom, Amount: int64(2 * (amount1 + amount2))}} + got = account.GetCoins() + assert.Equal(t, expected2, got) } // Run tests @@ -237,7 +290,7 @@ func TestRunSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: "10000ugnot", + GasFee: ugnot.ValueString(10000), GasWanted: 8000000, AccountNumber: 0, SequenceNumber: 0, @@ -257,9 +310,14 @@ func main() { println(ufmt.Sprintf("- after: %d", tests.Counter())) }` + caller, err := client.Signer.Info() + require.NoError(t, err) + // Make Msg configs - msg := MsgRun{ + msg := vm.MsgRun{ + Caller: caller.GetAddress(), Package: &std.MemPackage{ + Name: "main", Files: []*std.MemFile{ { Name: "main.gno", @@ -267,13 +325,18 @@ func main() { }, }, }, - Send: "", + Send: nil, } res, err := client.Run(baseCfg, msg) assert.NoError(t, err) require.NotNil(t, res) assert.Equal(t, string(res.DeliverTx.Data), "- before: 0\n- after: 10\n") + + res, err = runSigningSeparately(t, client, baseCfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, string(res.DeliverTx.Data), "- before: 10\n- after: 20\n") } // Run tests @@ -295,7 +358,7 @@ func TestRunMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: "10000ugnot", + GasFee: ugnot.ValueString(10000), GasWanted: 8000000, AccountNumber: 0, SequenceNumber: 0, @@ -324,9 +387,14 @@ func main() { println(ufmt.Sprintf("%s", deep.Render("gnoclient!"))) }` + caller, err := client.Signer.Info() + require.NoError(t, err) + // Make Msg configs - msg1 := MsgRun{ + msg1 := vm.MsgRun{ + Caller: caller.GetAddress(), Package: &std.MemPackage{ + Name: "main", Files: []*std.MemFile{ { Name: "main.gno", @@ -334,10 +402,12 @@ func main() { }, }, }, - Send: "", + Send: nil, } - msg2 := MsgRun{ + msg2 := vm.MsgRun{ + Caller: caller.GetAddress(), Package: &std.MemPackage{ + Name: "main", Files: []*std.MemFile{ { Name: "main.gno", @@ -345,7 +415,7 @@ func main() { }, }, }, - Send: "", + Send: nil, } expected := "- before: 0\n- after: 10\nhi gnoclient!\n" @@ -354,6 +424,12 @@ func main() { assert.NoError(t, err) require.NotNil(t, res) assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = runSigningSeparately(t, client, baseCfg, msg1, msg2) + require.NoError(t, err) + require.NotNil(t, res) + expected2 := "- before: 10\n- after: 20\nhi gnoclient!\n" + assert.Equal(t, expected2, string(res.DeliverTx.Data)) } func TestAddPackageSingle_Integration(t *testing.T) { @@ -375,7 +451,7 @@ func TestAddPackageSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: "10000ugnot", + GasFee: ugnot.ValueString(10000), GasWanted: 8000000, AccountNumber: 0, SequenceNumber: 0, @@ -390,10 +466,14 @@ func Echo(str string) string { fileName := "echo.gno" deploymentPath := "gno.land/p/demo/integration/test/echo" - deposit := "100ugnot" + deposit := std.Coins{{Denom: ugnot.Denom, Amount: int64(100)}} + + caller, err := client.Signer.Info() + require.NoError(t, err) // Make Msg config - msg := MsgAddPackage{ + msg := vm.MsgAddPackage{ + Creator: caller.GetAddress(), Package: &std.MemPackage{ Name: "echo", Path: deploymentPath, @@ -422,7 +502,19 @@ func Echo(str string) string { // Query balance to validate deposit baseAcc, _, err := client.QueryAccount(gnolang.DerivePkgAddr(deploymentPath)) require.NoError(t, err) - assert.Equal(t, baseAcc.GetCoins().String(), deposit) + assert.Equal(t, baseAcc.GetCoins(), deposit) + + // Test signing separately (using a different deployment path) + deploymentPathB := "gno.land/p/demo/integration/test/echo2" + msg.Package.Path = deploymentPathB + _, err = addPackageSigningSeparately(t, client, baseCfg, msg) + assert.NoError(t, err) + query, err = client.Query(QueryCfg{ + Path: "vm/qfile", + Data: []byte(deploymentPathB), + }) + require.NoError(t, err) + assert.Equal(t, string(query.Response.Data), fileName) } func TestAddPackageMultiple_Integration(t *testing.T) { @@ -444,14 +536,14 @@ func TestAddPackageMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: "10000ugnot", + GasFee: ugnot.ValueString(10000), GasWanted: 8000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", } - deposit := "100ugnot" + deposit := std.Coins{{Denom: ugnot.Denom, Amount: int64(100)}} deploymentPath1 := "gno.land/p/demo/integration/test/echo" body1 := `package echo @@ -467,7 +559,11 @@ func Hello(str string) string { return "Hello " + str + "!" }` - msg1 := MsgAddPackage{ + caller, err := client.Signer.Info() + require.NoError(t, err) + + msg1 := vm.MsgAddPackage{ + Creator: caller.GetAddress(), Package: &std.MemPackage{ Name: "echo", Path: deploymentPath1, @@ -478,10 +574,11 @@ func Hello(str string) string { }, }, }, - Deposit: "", + Deposit: nil, } - msg2 := MsgAddPackage{ + msg2 := vm.MsgAddPackage{ + Creator: caller.GetAddress(), Package: &std.MemPackage{ Name: "hello", Path: deploymentPath2, @@ -528,7 +625,28 @@ func Hello(str string) string { // Query balance to validate deposit baseAcc, _, err = client.QueryAccount(gnolang.DerivePkgAddr(deploymentPath2)) require.NoError(t, err) - assert.Equal(t, baseAcc.GetCoins().String(), deposit) + assert.Equal(t, baseAcc.GetCoins(), deposit) + + // Test signing separately (using a different deployment path) + deploymentPath1B := "gno.land/p/demo/integration/test/echo2" + deploymentPath2B := "gno.land/p/demo/integration/test/hello2" + msg1.Package.Path = deploymentPath1B + msg2.Package.Path = deploymentPath2B + _, err = addPackageSigningSeparately(t, client, baseCfg, msg1, msg2) + assert.NoError(t, err) + query, err = client.Query(QueryCfg{ + Path: "vm/qfile", + Data: []byte(deploymentPath1B), + }) + require.NoError(t, err) + assert.Equal(t, string(query.Response.Data), "echo.gno") + query, err = client.Query(QueryCfg{ + Path: "vm/qfile", + Data: []byte(deploymentPath2B), + }) + require.NoError(t, err) + assert.Contains(t, string(query.Response.Data), "hello.gno") + assert.Contains(t, string(query.Response.Data), "gno.mod") } // todo add more integration tests: diff --git a/gno.land/pkg/gnoclient/signer.go b/gno.land/pkg/gnoclient/signer.go index 0462865f2be..6e652080c72 100644 --- a/gno.land/pkg/gnoclient/signer.go +++ b/gno.land/pkg/gnoclient/signer.go @@ -3,6 +3,7 @@ package gnoclient import ( "fmt" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/errors" @@ -47,7 +48,7 @@ func (s SignerFromKeybase) Validate() error { signCfg := SignCfg{ UnsignedTX: std.Tx{ Msgs: []std.Msg{msg}, - Fee: std.NewFee(0, std.NewCoin("ugnot", 1000000)), + Fee: std.NewFee(0, std.NewCoin(ugnot.Denom, 1000000)), }, } if _, err = s.Sign(signCfg); err != nil { diff --git a/gno.land/pkg/gnoclient/util.go b/gno.land/pkg/gnoclient/util.go index 177e6d92906..50099eb4bd8 100644 --- a/gno.land/pkg/gnoclient/util.go +++ b/gno.land/pkg/gnoclient/util.go @@ -1,7 +1,5 @@ package gnoclient -import "github.com/gnolang/gno/tm2/pkg/std" - func (cfg BaseTxCfg) validateBaseTxConfig() error { if cfg.GasWanted <= 0 { return ErrInvalidGasWanted @@ -12,42 +10,3 @@ func (cfg BaseTxCfg) validateBaseTxConfig() error { return nil } - -func (msg MsgCall) validateMsgCall() error { - if msg.PkgPath == "" { - return ErrEmptyPkgPath - } - if msg.FuncName == "" { - return ErrEmptyFuncName - } - - return nil -} - -func (msg MsgSend) validateMsgSend() error { - if msg.ToAddress.IsZero() { - return ErrInvalidToAddress - } - _, err := std.ParseCoins(msg.Send) - if err != nil { - return ErrInvalidSendAmount - } - - return nil -} - -func (msg MsgRun) validateMsgRun() error { - if msg.Package == nil || len(msg.Package.Files) == 0 { - return ErrEmptyPackage - } - - return nil -} - -func (msg MsgAddPackage) validateMsgAddPackage() error { - if msg.Package == nil || len(msg.Package.Files) == 0 { - return ErrEmptyPackage - } - - return nil -} diff --git a/gno.land/pkg/gnoland/balance_test.go b/gno.land/pkg/gnoland/balance_test.go index 59dffcc4333..99a348e9f2f 100644 --- a/gno.land/pkg/gnoland/balance_test.go +++ b/gno.land/pkg/gnoland/balance_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/amino" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -78,7 +79,7 @@ func TestBalance_Parse(t *testing.T) { func TestBalance_AminoUnmarshalJSON(t *testing.T) { expected := Balance{ Address: crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), - Amount: std.MustParseCoins("100ugnot"), + Amount: std.MustParseCoins(ugnot.ValueString(100)), } value := fmt.Sprintf("[%q]", expected.String()) @@ -95,7 +96,7 @@ func TestBalance_AminoUnmarshalJSON(t *testing.T) { func TestBalance_AminoMarshalJSON(t *testing.T) { expected := Balance{ Address: crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), - Amount: std.MustParseCoins("100ugnot"), + Amount: std.MustParseCoins(ugnot.ValueString(100)), } expectedJSON := fmt.Sprintf("[%q]", expected.String()) @@ -112,15 +113,15 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { // Generate dummy keys dummyKeys := getDummyKeys(t, 2) - amount := std.NewCoins(std.NewCoin("ugnot", 10)) + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) entries := make([]string, len(dummyKeys)) for index, key := range dummyKeys { entries[index] = fmt.Sprintf( - "%s=%dugnot", + "%s=%s", key.Address().String(), - amount.AmountOf("ugnot"), + ugnot.ValueString(amount.AmountOf(ugnot.Denom)), ) } @@ -150,7 +151,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { t.Parallel() balances := []string{ - "dummyaddress=10ugnot", + "dummyaddress=" + ugnot.ValueString(10), } balanceMap, err := GetBalancesFromEntries(balances...) @@ -165,9 +166,10 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balances := []string{ fmt.Sprintf( - "%s=%sugnot", + "%s=%s%s", dummyKey.Address().String(), strconv.FormatUint(math.MaxUint64, 10), + ugnot.Denom, ), } @@ -185,15 +187,15 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { // Generate dummy keys dummyKeys := getDummyKeys(t, 2) - amount := std.NewCoins(std.NewCoin("ugnot", 10)) + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) balances := make([]string, len(dummyKeys)) for index, key := range dummyKeys { balances[index] = fmt.Sprintf( - "%s=%dugnot", + "%s=%s", key.Address().String(), - amount.AmountOf("ugnot"), + ugnot.ValueString(amount.AmountOf(ugnot.Denom)), ) } @@ -215,9 +217,10 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { balances := []string{ fmt.Sprintf( - "%s=%sugnot", + "%s=%s%s", dummyKey.Address().String(), strconv.FormatUint(math.MaxUint64, 10), + ugnot.Denom, ), } diff --git a/gno.land/pkg/gnoland/ugnot/denom.go b/gno.land/pkg/gnoland/ugnot/denom.go new file mode 100644 index 00000000000..734c8532398 --- /dev/null +++ b/gno.land/pkg/gnoland/ugnot/denom.go @@ -0,0 +1,11 @@ +package ugnot + +import "strconv" + +// Denom is the denomination for ugnot, gno.land's native token. +const Denom = "ugnot" + +// ValueString converts `value` to a string, appends "ugnot", and returns it. +func ValueString(value int64) string { + return strconv.FormatInt(value, 10) + Denom +} diff --git a/gno.land/pkg/gnoweb/alias.go b/gno.land/pkg/gnoweb/alias.go new file mode 100644 index 00000000000..d7297ed9d5d --- /dev/null +++ b/gno.land/pkg/gnoweb/alias.go @@ -0,0 +1,26 @@ +package gnoweb + +// realm aliases +var Aliases = map[string]string{ + "/": "/r/gnoland/home", + "/about": "/r/gnoland/pages:p/about", + "/gnolang": "/r/gnoland/pages:p/gnolang", + "/ecosystem": "/r/gnoland/pages:p/ecosystem", + "/partners": "/r/gnoland/pages:p/partners", + "/testnets": "/r/gnoland/pages:p/testnets", + "/start": "/r/gnoland/pages:p/start", + "/license": "/r/gnoland/pages:p/license", + "/game-of-realms": "/r/gnoland/pages:p/gor", // XXX: replace with gor realm + "/events": "/r/gnoland/events", +} + +// http redirects +var Redirects = map[string]string{ + "/r/demo/boards:gnolang/6": "/r/demo/boards:gnolang/3", // XXX: temporary + "/blog": "/r/gnoland/blog", + "/gor": "/game-of-realms", + "/grants": "/partners", + "/language": "/gnolang", + "/getting-started": "/start", + "/gophercon24": "https://docs.gno.land", +} diff --git a/gno.land/pkg/gnoweb/gnoweb.go b/gno.land/pkg/gnoweb/gnoweb.go index 1cd0d664193..5377ae6a420 100644 --- a/gno.land/pkg/gnoweb/gnoweb.go +++ b/gno.land/pkg/gnoweb/gnoweb.go @@ -80,34 +80,11 @@ func MakeApp(logger *slog.Logger, cfg Config) gotuna.App { Static: static.EmbeddedStatic, } - // realm aliases - aliases := map[string]string{ - "/": "/r/gnoland/home", - "/about": "/r/gnoland/pages:p/about", - "/gnolang": "/r/gnoland/pages:p/gnolang", - "/ecosystem": "/r/gnoland/pages:p/ecosystem", - "/partners": "/r/gnoland/pages:p/partners", - "/testnets": "/r/gnoland/pages:p/testnets", - "/start": "/r/gnoland/pages:p/start", - "/license": "/r/gnoland/pages:p/license", - "/game-of-realms": "/r/gnoland/pages:p/gor", // XXX: replace with gor realm - "/events": "/r/gnoland/events", - } - - for from, to := range aliases { + for from, to := range Aliases { app.Router.Handle(from, handlerRealmAlias(logger, app, &cfg, to)) } - // http redirects - redirects := map[string]string{ - "/r/demo/boards:gnolang/6": "/r/demo/boards:gnolang/3", // XXX: temporary - "/blog": "/r/gnoland/blog", - "/gor": "/game-of-realms", - "/grants": "/partners", - "/language": "/gnolang", - "/getting-started": "/start", - "/gophercon24": "https://docs.gno.land", - } - for from, to := range redirects { + + for from, to := range Redirects { app.Router.Handle(from, handlerRedirect(logger, app, &cfg, to)) } // realm routes diff --git a/gno.land/pkg/gnoweb/static/js/renderer.js b/gno.land/pkg/gnoweb/static/js/renderer.js index 4937b5a5691..0aa6400633d 100644 --- a/gno.land/pkg/gnoweb/static/js/renderer.js +++ b/gno.land/pkg/gnoweb/static/js/renderer.js @@ -7,27 +7,42 @@ function renderUsernames(raw) { return raw.replace(/( |\n)@([_a-z0-9]{5,16})/, "$1[@$2](/r/demo/users:$2)"); } -function parseContent(source) { - const { markedHighlight } = globalThis.markedHighlight; - const { Marked } = globalThis.marked; - const markedInstance = new Marked( - markedHighlight({ - langPrefix: 'language-', - highlight(code, lang, info) { - if (lang === "json") { - try { - code = JSON.stringify(JSON.parse(code), null, 2); - } catch {} - } - const language = hljs.getLanguage(lang) ? lang : 'plaintext'; - return hljs.highlight(code, { language }).value; - } - }) - ); - markedInstance.setOptions({ gfm: true }); - const doc = new DOMParser().parseFromString(source, "text/html"); - const contents = doc.documentElement.textContent; - return markedInstance.parse(contents); +function parseContent(source, isCode) { + if (isCode) { + const highlightedCode = hljs.highlightAuto(source).value; + const codeElement = document.createElement("code"); + codeElement.classList.add("hljs"); + codeElement.innerHTML = highlightedCode; + + const preElement = document.createElement("pre"); + preElement.appendChild(codeElement); + + return preElement; + } else { + const { markedHighlight } = globalThis.markedHighlight; + const { Marked } = globalThis.marked; + const markedInstance = new Marked( + markedHighlight({ + langPrefix: "language-", + highlight(code, lang, info) { + if (lang === "json") { + try { + code = JSON.stringify(JSON.parse(code), null, 2); + } catch { + console.error('Error: The provided JSON code is invalid.'); + } + } + const language = hljs.getLanguage(lang) ? lang : "plaintext"; + return hljs.highlight(code, { language }).value; + }, + }) + ); + markedInstance.setOptions({ gfm: true }); + const doc = new DOMParser().parseFromString(source, "text/html"); + const contents = doc.documentElement.textContent; + + return markedInstance.parse(contents); + } } /* diff --git a/gno.land/pkg/gnoweb/views/funcs.html b/gno.land/pkg/gnoweb/views/funcs.html index a02f83144f8..37c63458515 100644 --- a/gno.land/pkg/gnoweb/views/funcs.html +++ b/gno.land/pkg/gnoweb/views/funcs.html @@ -160,18 +160,20 @@ diff --git a/gno.land/pkg/gnoweb/views/package_file.html b/gno.land/pkg/gnoweb/views/package_file.html index 42e1d0a28fc..43e7820b29f 100644 --- a/gno.land/pkg/gnoweb/views/package_file.html +++ b/gno.land/pkg/gnoweb/views/package_file.html @@ -11,21 +11,13 @@
{{ .Data.DirPath }}/{{ .Data.FileName }}
-
-
{{ .Data.FileContents }}
+ {{ .Data.FileContents }}
{{ template "footer" }} {{ template "js" .}} - - {{- end -}} diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 0462b0c7639..d525591f51e 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -13,6 +13,7 @@ import ( "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/keyscli" "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" @@ -172,7 +173,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // get packages pkgs := ts.Value(envKeyPkgsLoader).(*pkgsLoader) // grab logger creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 - defaultFee := std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) pkgsTxs, err := pkgs.LoadPackages(creator, defaultFee, nil) if err != nil { ts.Fatalf("unable to load packages txs: %s", err) @@ -562,7 +563,7 @@ func createAccount(env envSetter, kb keys.Keybase, accountName string) (gnoland. return gnoland.Balance{ Address: address, - Amount: std.Coins{std.NewCoin("ugnot", 10e6)}, + Amount: std.Coins{std.NewCoin(ugnot.Denom, 10e6)}, }, nil } @@ -586,7 +587,7 @@ func createAccountFrom(env envSetter, kb keys.Keybase, accountName, mnemonic str return gnoland.Balance{ Address: address, - Amount: std.Coins{std.NewCoin("ugnot", 10e6)}, + Amount: std.Coins{std.NewCoin(ugnot.Denom, 10e6)}, }, nil } diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 993386f6b04..f3baf55b0dd 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -102,7 +103,7 @@ func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey Balances: []gnoland.Balance{ { Address: crypto.MustAddressFromString(DefaultAccount_Address), - Amount: std.MustParseCoins("10000000000000ugnot"), + Amount: std.MustParseCoins(ugnot.ValueString(10000000000000)), }, }, Txs: []std.Tx{}, @@ -114,7 +115,7 @@ func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey func LoadDefaultPackages(t TestingTS, creator bft.Address, gnoroot string) []std.Tx { examplesDir := filepath.Join(gnoroot, "examples") - defaultFee := std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) txs, err := gnoland.LoadPackagesFromDir(examplesDir, creator, defaultFee) require.NoError(t, err) diff --git a/gno.land/pkg/keyscli/root.go b/gno.land/pkg/keyscli/root.go index 0aae4ff8af9..ba1b3482cc2 100644 --- a/gno.land/pkg/keyscli/root.go +++ b/gno.land/pkg/keyscli/root.go @@ -17,7 +17,7 @@ func NewRootCmd(io commands.IO, base client.BaseOptions) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ ShortUsage: " [flags] [...]", - LongHelp: "Manages private keys for the node", + LongHelp: "gno.land keychain & client", Options: []ff.Option{ ff.WithConfigFileFlag("config"), ff.WithConfigFileParser(fftoml.Parser), diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index 66655994bd4..de647c8735a 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -3,6 +3,7 @@ package vm import ( "testing" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk" @@ -140,7 +141,7 @@ func setupAddPkg(success bool) (sdk.Context, sdk.Tx, vmHandler) { addr := crypto.AddressFromPreimage([]byte("test1")) acc := env.acck.NewAccountWithAddress(ctx, addr) env.acck.SetAccount(ctx, acc) - env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(ugnot.ValueString(10000000))) // success message var files []*std.MemFile if success { @@ -172,7 +173,7 @@ func Echo() UnknowType { // create messages and a transaction msg := NewMsgAddPackage(addr, pkgPath, files) msgs := []std.Msg{msg} - fee := std.NewFee(500000, std.MustParseCoin("1ugnot")) + fee := std.NewFee(500000, std.MustParseCoin(ugnot.ValueString(1))) tx := std.NewTx(msgs, fee, []std.Signature{}, "") return ctx, tx, vmHandler diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index a86ca5e4a97..75b55c3174a 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -10,10 +10,13 @@ import ( "github.com/stretchr/testify/assert" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" ) +var coinsString = ugnot.ValueString(10000000) + func TestVMKeeperAddPackage(t *testing.T) { env := setupTestEnv() ctx := env.ctx @@ -23,8 +26,8 @@ func TestVMKeeperAddPackage(t *testing.T) { addr := crypto.AddressFromPreimage([]byte("addr1")) acc := env.acck.NewAccountWithAddress(ctx, addr) env.acck.SetAccount(ctx, acc) - env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) - assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot"))) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) // Create test package. files := []*std.MemFile{ @@ -68,8 +71,8 @@ func TestVMKeeperOrigSend1(t *testing.T) { addr := crypto.AddressFromPreimage([]byte("addr1")) acc := env.acck.NewAccountWithAddress(ctx, addr) env.acck.SetAccount(ctx, acc) - env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) - assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot"))) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) // Create test package. files := []*std.MemFile{ @@ -96,7 +99,7 @@ func Echo(msg string) string { assert.NoError(t, err) // Run Echo function. - coins := std.MustParseCoins("10000000ugnot") + coins := std.MustParseCoins(coinsString) msg2 := NewMsgCall(addr, coins, pkgPath, "Echo", []string{"hello world"}) res, err := env.vmk.Call(ctx, msg2) assert.NoError(t, err) @@ -113,8 +116,8 @@ func TestVMKeeperOrigSend2(t *testing.T) { addr := crypto.AddressFromPreimage([]byte("addr1")) acc := env.acck.NewAccountWithAddress(ctx, addr) env.acck.SetAccount(ctx, acc) - env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) - assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot"))) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) // Create test package. files := []*std.MemFile{ @@ -149,7 +152,7 @@ func GetAdmin() string { assert.NoError(t, err) // Run Echo function. - coins := std.MustParseCoins("11000000ugnot") + coins := std.MustParseCoins(ugnot.ValueString(11000000)) msg2 := NewMsgCall(addr, coins, pkgPath, "Echo", []string{"hello world"}) res, err := env.vmk.Call(ctx, msg2) assert.Error(t, err) @@ -167,8 +170,8 @@ func TestVMKeeperOrigSend3(t *testing.T) { addr := crypto.AddressFromPreimage([]byte("addr1")) acc := env.acck.NewAccountWithAddress(ctx, addr) env.acck.SetAccount(ctx, acc) - env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) - assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot"))) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) // Create test package. files := []*std.MemFile{ @@ -195,7 +198,7 @@ func Echo(msg string) string { assert.NoError(t, err) // Run Echo function. - coins := std.MustParseCoins("9000000ugnot") + coins := std.MustParseCoins(ugnot.ValueString(9000000)) msg2 := NewMsgCall(addr, coins, pkgPath, "Echo", []string{"hello world"}) // XXX change this into an error and make sure error message is descriptive. _, err = env.vmk.Call(ctx, msg2) @@ -211,8 +214,8 @@ func TestVMKeeperRealmSend1(t *testing.T) { addr := crypto.AddressFromPreimage([]byte("addr1")) acc := env.acck.NewAccountWithAddress(ctx, addr) env.acck.SetAccount(ctx, acc) - env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) - assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot"))) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) // Create test package. files := []*std.MemFile{ @@ -239,7 +242,7 @@ func Echo(msg string) string { assert.NoError(t, err) // Run Echo function. - coins := std.MustParseCoins("10000000ugnot") + coins := std.MustParseCoins(coinsString) msg2 := NewMsgCall(addr, coins, pkgPath, "Echo", []string{"hello world"}) res, err := env.vmk.Call(ctx, msg2) assert.NoError(t, err) @@ -255,8 +258,8 @@ func TestVMKeeperRealmSend2(t *testing.T) { addr := crypto.AddressFromPreimage([]byte("addr1")) acc := env.acck.NewAccountWithAddress(ctx, addr) env.acck.SetAccount(ctx, acc) - env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) - assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot"))) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) // Create test package. files := []*std.MemFile{ @@ -283,7 +286,7 @@ func Echo(msg string) string { assert.NoError(t, err) // Run Echo function. - coins := std.MustParseCoins("9000000ugnot") + coins := std.MustParseCoins(ugnot.ValueString(9000000)) msg2 := NewMsgCall(addr, coins, pkgPath, "Echo", []string{"hello world"}) // XXX change this into an error and make sure error message is descriptive. _, err = env.vmk.Call(ctx, msg2) @@ -299,8 +302,8 @@ func TestVMKeeperOrigCallerInit(t *testing.T) { addr := crypto.AddressFromPreimage([]byte("addr1")) acc := env.acck.NewAccountWithAddress(ctx, addr) env.acck.SetAccount(ctx, acc) - env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) - assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot"))) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) // Create test package. files := []*std.MemFile{ @@ -422,8 +425,8 @@ func TestNumberOfArgsError(t *testing.T) { addr := crypto.AddressFromPreimage([]byte("addr1")) acc := env.acck.NewAccountWithAddress(ctx, addr) env.acck.SetAccount(ctx, acc) - env.bank.SetCoins(ctx, addr, std.MustParseCoins("10000000ugnot")) - assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins("10000000ugnot"))) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) // Create test package. files := []*std.MemFile{ @@ -442,7 +445,7 @@ func Echo(msg string) string { assert.NoError(t, err) // Call Echo function with wrong number of arguments - coins := std.MustParseCoins("1ugnot") + coins := std.MustParseCoins(ugnot.ValueString(1)) msg2 := NewMsgCall(addr, coins, pkgPath, "Echo", []string{"hello world", "extra arg"}) assert.PanicsWithValue( t, diff --git a/gnovm/pkg/gnolang/op_binary.go b/gnovm/pkg/gnolang/op_binary.go index 0d4581377c2..db3c1e5695c 100644 --- a/gnovm/pkg/gnolang/op_binary.go +++ b/gnovm/pkg/gnolang/op_binary.go @@ -79,7 +79,6 @@ func (m *Machine) doOpEql() { if debug { debugAssertEqualityTypes(lv.T, rv.T) } - // set result in lv. res := isEql(m.Store, lv, rv) lv.T = UntypedBoolType @@ -344,6 +343,9 @@ func isEql(store Store, lv, rv *TypedValue) bool { } else if rvu { return false } + if err := checkSame(lv.T, rv.T, ""); err != nil { + return false + } if lnt, ok := lv.T.(*NativeType); ok { if rnt, ok := rv.T.(*NativeType); ok { if lnt.Type != rnt.Type { diff --git a/gnovm/pkg/gnolang/op_expressions.go b/gnovm/pkg/gnolang/op_expressions.go index 36130ccbf4d..8ff0b5bd538 100644 --- a/gnovm/pkg/gnolang/op_expressions.go +++ b/gnovm/pkg/gnolang/op_expressions.go @@ -194,8 +194,13 @@ func (m *Machine) doOpRef() { nv.Value = rv2 } } + // when obtaining a pointer of the databyte type, use the ElemType of databyte + elt := xv.TV.T + if elt == DataByteType { + elt = xv.TV.V.(DataByteValue).ElemType + } m.PushValue(TypedValue{ - T: m.Alloc.NewType(&PointerType{Elt: xv.TV.T}), + T: m.Alloc.NewType(&PointerType{Elt: elt}), V: xv, }) } diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 5a710723b86..ba60ead28f6 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -1503,6 +1503,13 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { checkOrConvertIntegerKind(store, last, n.High) checkOrConvertIntegerKind(store, last, n.Max) + // if n.X is untyped, convert to corresponding type + t := evalStaticTypeOf(store, last, n.X) + if isUntyped(t) { + dt := defaultTypeOf(t) + checkOrConvertType(store, last, &n.X, dt, false) + } + // TRANS_LEAVE ----------------------- case *TypeAssertExpr: if n.Type == nil { @@ -2163,13 +2170,42 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { numNames := len(n.NameExprs) sts := make([]Type, numNames) // static types tvs := make([]TypedValue, numNames) + if numNames > 1 && len(n.Values) == 1 { - // special case if `var a, b, c T? = f()` form. - cx := n.Values[0].(*CallExpr) - tt := evalStaticTypeOfRaw(store, last, cx).(*tupleType) - if rLen := len(tt.Elts); rLen != numNames { - panic(fmt.Sprintf("assignment mismatch: %d variable(s) but %s returns %d value(s)", numNames, cx.Func.String(), rLen)) + // Special cases if one of the following: + // - `var a, b, c T = f()` + // - `var a, b = n.(T)` + // - `var a, b = n[i], where n is a map` + + var tuple *tupleType + valueExpr := n.Values[0] + valueType := evalStaticTypeOfRaw(store, last, valueExpr) + + switch expr := valueExpr.(type) { + case *CallExpr: + tuple = valueType.(*tupleType) + case *TypeAssertExpr, *IndexExpr: + tuple = &tupleType{Elts: []Type{valueType, BoolType}} + if ex, ok := expr.(*TypeAssertExpr); ok { + ex.HasOK = true + break + } + expr.(*IndexExpr).HasOK = true + default: + panic(fmt.Sprintf("unexpected ValueDecl value expression type %T", expr)) + } + + if rLen := len(tuple.Elts); rLen != numNames { + panic( + fmt.Sprintf( + "assignment mismatch: %d variable(s) but %s returns %d value(s)", + numNames, + valueExpr.String(), + rLen, + ), + ) } + if n.Type != nil { // only a single type can be specified. nt := evalStaticType(store, last, n.Type) @@ -2181,7 +2217,7 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { } else { // set types as return types. for i := 0; i < numNames; i++ { - et := tt.Elts[i] + et := tuple.Elts[i] sts[i] = et tvs[i] = anyValue(et) } @@ -2794,8 +2830,10 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative // push t into bx.Left checkOrConvertType(store, last, &bx.Left, t, autoNative) return - // case EQL, LSS, GTR, NEQ, LEQ, GEQ: - // default: + case EQL, LSS, GTR, NEQ, LEQ, GEQ: + // do nothing + default: + // do nothing } } } @@ -2906,6 +2944,92 @@ func convertConst(store Store, last BlockNode, cx *ConstExpr, t Type) { } } +func assertTypeDeclNoCycle(store Store, last BlockNode, td *TypeDecl, stack *[]Name) { + assertTypeDeclNoCycle2(store, last, td.Type, stack, false, td.IsAlias) +} + +func assertTypeDeclNoCycle2(store Store, last BlockNode, x Expr, stack *[]Name, indirect bool, isAlias bool) { + if x == nil { + panic("unexpected nil expression when checking for type declaration cycles") + } + + var lastX Expr + defer func() { + if _, ok := lastX.(*NameExpr); ok { + // pop stack + *stack = (*stack)[:len(*stack)-1] + } + }() + + switch cx := x.(type) { + case *NameExpr: + var msg string + + // Function to build the error message + buildMessage := func() string { + for j := 0; j < len(*stack); j++ { + msg += fmt.Sprintf("%s -> ", (*stack)[j]) + } + return msg + string(cx.Name) // Append the current name last + } + + // Check for existence of cx.Name in stack + findCycle := func() { + for _, n := range *stack { + if n == cx.Name { + msg = buildMessage() + panic(fmt.Sprintf("invalid recursive type: %s", msg)) + } + } + } + + if indirect && !isAlias { + *stack = (*stack)[:0] + } else { + findCycle() + *stack = append(*stack, cx.Name) + lastX = cx + } + + return + case *SelectorExpr: + assertTypeDeclNoCycle2(store, last, cx.X, stack, indirect, isAlias) + case *StarExpr: + assertTypeDeclNoCycle2(store, last, cx.X, stack, true, isAlias) + case *FieldTypeExpr: + assertTypeDeclNoCycle2(store, last, cx.Type, stack, indirect, isAlias) + case *ArrayTypeExpr: + if cx.Len != nil { + assertTypeDeclNoCycle2(store, last, cx.Len, stack, indirect, isAlias) + } + assertTypeDeclNoCycle2(store, last, cx.Elt, stack, indirect, isAlias) + case *SliceTypeExpr: + assertTypeDeclNoCycle2(store, last, cx.Elt, stack, true, isAlias) + case *InterfaceTypeExpr: + for i := range cx.Methods { + assertTypeDeclNoCycle2(store, last, &cx.Methods[i], stack, indirect, isAlias) + } + case *ChanTypeExpr: + assertTypeDeclNoCycle2(store, last, cx.Value, stack, true, isAlias) + case *FuncTypeExpr: + for i := range cx.Params { + assertTypeDeclNoCycle2(store, last, &cx.Params[i], stack, true, isAlias) + } + for i := range cx.Results { + assertTypeDeclNoCycle2(store, last, &cx.Results[i], stack, true, isAlias) + } + case *MapTypeExpr: + assertTypeDeclNoCycle2(store, last, cx.Key, stack, true, isAlias) + assertTypeDeclNoCycle2(store, last, cx.Value, stack, true, isAlias) + case *StructTypeExpr: + for i := range cx.Fields { + assertTypeDeclNoCycle2(store, last, &cx.Fields[i], stack, indirect, isAlias) + } + default: + } + return +} + // Returns any names not yet defined nor predefined in expr. These happen // upon transcribe:enter from the top, so value paths cannot be used. If no // names are un and x is TypeExpr, evalStaticType(store,last, x) must not @@ -3197,11 +3321,11 @@ func predefineNow(store Store, last BlockNode, d Decl) (Decl, bool) { } } }() - m := make(map[Name]struct{}) - return predefineNow2(store, last, d, m) + stack := &[]Name{} + return predefineNow2(store, last, d, stack) } -func predefineNow2(store Store, last BlockNode, d Decl, m map[Name]struct{}) (Decl, bool) { +func predefineNow2(store Store, last BlockNode, d Decl, stack *[]Name) (Decl, bool) { pkg := packageOf(last) // pre-register d.GetName() to detect circular definition. for _, dn := range d.GetDeclNames() { @@ -3209,15 +3333,24 @@ func predefineNow2(store Store, last BlockNode, d Decl, m map[Name]struct{}) (De panic(fmt.Sprintf( "builtin identifiers cannot be shadowed: %s", dn)) } - m[dn] = struct{}{} + *stack = append(*stack, dn) } + + // check type decl cycle + if td, ok := d.(*TypeDecl); ok { + // recursively check + assertTypeDeclNoCycle(store, last, td, stack) + } + // recursively predefine dependencies. for { un := tryPredefine(store, last, d) if un != "" { // check circularity. - if _, ok := m[un]; ok { - panic(fmt.Sprintf("constant definition loop with %s", un)) + for _, n := range *stack { + if n == un { + panic(fmt.Sprintf("constant definition loop with %s", un)) + } } // look up dependency declaration from fileset. file, decl := pkg.FileSet.GetDeclFor(un) @@ -3226,7 +3359,7 @@ func predefineNow2(store Store, last BlockNode, d Decl, m map[Name]struct{}) (De panic("all types from files in file-set should have already been predefined") } // predefine dependency (recursive). - *decl, _ = predefineNow2(store, file, *decl, m) + *decl, _ = predefineNow2(store, file, *decl, stack) } else { break } diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index 3fea714b142..f6bd789f1bf 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" @@ -53,7 +54,7 @@ func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { pkgAddr := gno.DerivePkgAddr(pkgPath) // the addr of the pkgPath called. caller := gno.DerivePkgAddr("user1.gno") - pkgCoins := std.MustParseCoins("200000000ugnot").Add(send) // >= send. + pkgCoins := std.MustParseCoins(ugnot.ValueString(200000000)).Add(send) // >= send. banker := newTestBanker(pkgAddr.Bech32(), pkgCoins) ctx := stdlibs.ExecContext{ ChainID: "dev", diff --git a/gnovm/tests/files/circular_constant.gno b/gnovm/tests/files/circular_constant.gno new file mode 100644 index 00000000000..ff25da7428d --- /dev/null +++ b/gnovm/tests/files/circular_constant.gno @@ -0,0 +1,10 @@ +package main + +const A = B +const B = A + 1 + +func main() { +} + +// Error: +// main/files/circular_constant.gno:3:7: constant definition loop with A diff --git a/gnovm/tests/files/recursive1.gno b/gnovm/tests/files/recursive1.gno new file mode 100644 index 00000000000..8279e247d84 --- /dev/null +++ b/gnovm/tests/files/recursive1.gno @@ -0,0 +1,13 @@ +package main + +type S struct { + T S +} + +func main() { + var a, b S + println(a == b) +} + +// Error: +// main/files/recursive1.gno:1:1: invalid recursive type: S -> S diff --git a/gnovm/tests/files/recursive1a.gno b/gnovm/tests/files/recursive1a.gno new file mode 100644 index 00000000000..87681e1fcdd --- /dev/null +++ b/gnovm/tests/files/recursive1a.gno @@ -0,0 +1,15 @@ +package main + +type S1 *S + +type S struct { + T S1 +} + +func main() { + var a, b S + println(a == b) +} + +// Output: +// true \ No newline at end of file diff --git a/gnovm/tests/files/recursive1b.gno b/gnovm/tests/files/recursive1b.gno new file mode 100644 index 00000000000..2893baf8fca --- /dev/null +++ b/gnovm/tests/files/recursive1b.gno @@ -0,0 +1,16 @@ +package main + +type S struct { + T *S + B Integer +} + +type Integer int + +func main() { + var a, b S + println(a == b) +} + +// Output: +// true diff --git a/gnovm/tests/files/recursive1c.gno b/gnovm/tests/files/recursive1c.gno new file mode 100644 index 00000000000..7797f375027 --- /dev/null +++ b/gnovm/tests/files/recursive1c.gno @@ -0,0 +1,17 @@ +package main + +import "fmt" + +type S struct { + A [2][2]S +} + +func main() { + var a, b S + + fmt.Println(a) + fmt.Println(b) +} + +// Error: +// main/files/recursive1c.gno:1:1: invalid recursive type: S -> S diff --git a/gnovm/tests/files/recursive1d.gno b/gnovm/tests/files/recursive1d.gno new file mode 100644 index 00000000000..22bf172b5ac --- /dev/null +++ b/gnovm/tests/files/recursive1d.gno @@ -0,0 +1,17 @@ +package main + +import "fmt" + +type S struct { + A [2]S +} + +func main() { + var a, b S + + fmt.Println(a) + fmt.Println(b) +} + +// Error: +// main/files/recursive1d.gno:1:1: invalid recursive type: S -> S diff --git a/gnovm/tests/files/recursive1e.gno b/gnovm/tests/files/recursive1e.gno new file mode 100644 index 00000000000..6d1636ba9f3 --- /dev/null +++ b/gnovm/tests/files/recursive1e.gno @@ -0,0 +1,13 @@ +package main + +type S struct { + A [2][]S +} + +func main() { + var a, b S + println(a) +} + +// Output: +// (struct{(array[(nil []main.S),(nil []main.S)] [2][]main.S)} main.S) diff --git a/gnovm/tests/files/recursive1f.gno b/gnovm/tests/files/recursive1f.gno new file mode 100644 index 00000000000..81fe2a5699c --- /dev/null +++ b/gnovm/tests/files/recursive1f.gno @@ -0,0 +1,13 @@ +package main + +func main() { + type S struct { + T S + } + + var a, b S + println(a == b) +} + +// Error: +// main/files/recursive1f.gno:3:1: invalid recursive type: S -> S diff --git a/gnovm/tests/files/recursive2.gno b/gnovm/tests/files/recursive2.gno new file mode 100644 index 00000000000..4ed86f03d58 --- /dev/null +++ b/gnovm/tests/files/recursive2.gno @@ -0,0 +1,21 @@ +package main + +type A struct { + X B +} + +type B struct { + X C +} + +type C struct { + X A +} + +func main() { + var p, q A + println(p == q) +} + +// Error: +// main/files/recursive2.gno:1:1: invalid recursive type: A -> B -> C -> A diff --git a/gnovm/tests/files/recursive2a.gno b/gnovm/tests/files/recursive2a.gno new file mode 100644 index 00000000000..9c7dd3e179f --- /dev/null +++ b/gnovm/tests/files/recursive2a.gno @@ -0,0 +1,21 @@ +package main + +type A struct { + X B +} + +type B struct { + X int +} + +type C struct { + X A +} + +func main() { + var p, q A + println(p == q) +} + +// Output: +// true diff --git a/gnovm/tests/files/recursive2b.gno b/gnovm/tests/files/recursive2b.gno new file mode 100644 index 00000000000..92d633cdda1 --- /dev/null +++ b/gnovm/tests/files/recursive2b.gno @@ -0,0 +1,21 @@ +package main + +type A struct { + X B +} + +type B struct { + X C +} + +type C struct { + X *A +} + +func main() { + var p, q A + println(p == q) +} + +// Output: +// true diff --git a/gnovm/tests/files/recursive2c.gno b/gnovm/tests/files/recursive2c.gno new file mode 100644 index 00000000000..3b5c27ed8ea --- /dev/null +++ b/gnovm/tests/files/recursive2c.gno @@ -0,0 +1,21 @@ +package main + +func main() { + type A struct { + X B + } + + type B struct { + X C + } + + type C struct { + X A + } + + var p, q A + println(p == q) +} + +// Error: +// main/files/recursive2c.gno:3:1: name B not defined in fileset with files [files/recursive2c.gno] diff --git a/gnovm/tests/files/recursive2d.gno b/gnovm/tests/files/recursive2d.gno new file mode 100644 index 00000000000..b2439ba6259 --- /dev/null +++ b/gnovm/tests/files/recursive2d.gno @@ -0,0 +1,21 @@ +package main + +type A struct { + X *B +} + +type B struct { + X int +} + +type C struct { + X A +} + +func main() { + var p, q A + println(p == q) +} + +// Output: +// true diff --git a/gnovm/tests/files/recursive3.gno b/gnovm/tests/files/recursive3.gno new file mode 100644 index 00000000000..552c086c91b --- /dev/null +++ b/gnovm/tests/files/recursive3.gno @@ -0,0 +1,13 @@ +package main + +type S struct { + T *S +} + +func main() { + var a, b S + println(a == b) +} + +// Output: +// true diff --git a/gnovm/tests/files/recursive4.gno b/gnovm/tests/files/recursive4.gno new file mode 100644 index 00000000000..29392cb35ab --- /dev/null +++ b/gnovm/tests/files/recursive4.gno @@ -0,0 +1,15 @@ +package main + +import "time" + +type Duration struct { + t time.Duration +} + +func main() { + var a, b Duration + println(a == b) +} + +// Output: +// true diff --git a/gnovm/tests/files/recursive4a.gno b/gnovm/tests/files/recursive4a.gno new file mode 100644 index 00000000000..8b4d13b4785 --- /dev/null +++ b/gnovm/tests/files/recursive4a.gno @@ -0,0 +1,9 @@ +package main + +type time time.Duration + +func main() { +} + +// Error: +// main/files/recursive4a.gno:1:1: invalid recursive type: time -> time diff --git a/gnovm/tests/files/recursive5.gno b/gnovm/tests/files/recursive5.gno new file mode 100644 index 00000000000..1c2fbd89fb8 --- /dev/null +++ b/gnovm/tests/files/recursive5.gno @@ -0,0 +1,13 @@ +package main + +type S struct { + S +} + +func main() { + var a, b S + println(a == b) +} + +// Error: +// main/files/recursive5.gno:1:1: invalid recursive type: S -> S diff --git a/gnovm/tests/files/recursive6.gno b/gnovm/tests/files/recursive6.gno new file mode 100644 index 00000000000..73858b2ea1b --- /dev/null +++ b/gnovm/tests/files/recursive6.gno @@ -0,0 +1,23 @@ +package main + +type SelfReferencing interface { + Self() SelfReferencing +} + +type Implementation struct { + // Some implementation details... +} + +func (impl Implementation) Self() SelfReferencing { + return &impl +} + +func main() { + var obj Implementation + var intf SelfReferencing = obj + _ = intf.Self() + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/recursive6a.gno b/gnovm/tests/files/recursive6a.gno new file mode 100644 index 00000000000..8123fc626a5 --- /dev/null +++ b/gnovm/tests/files/recursive6a.gno @@ -0,0 +1,12 @@ +package main + +type SelfReferencing interface { + SelfReferencing +} + +func main() { + println("ok") +} + +// Error: +// main/files/recursive6a.gno:1:1: invalid recursive type: SelfReferencing -> SelfReferencing diff --git a/gnovm/tests/files/recursive7.gno b/gnovm/tests/files/recursive7.gno new file mode 100644 index 00000000000..9bd8a56995d --- /dev/null +++ b/gnovm/tests/files/recursive7.gno @@ -0,0 +1,10 @@ +package main + +type S []S + +func main() { + println("ok") +} + +// Output: +// ok diff --git a/gnovm/tests/files/recursive7a.gno b/gnovm/tests/files/recursive7a.gno new file mode 100644 index 00000000000..b3c57516f13 --- /dev/null +++ b/gnovm/tests/files/recursive7a.gno @@ -0,0 +1,8 @@ +package main + +type S [2]S + +func main() {} + +// Error: +// main/files/recursive7a.gno:1:1: invalid recursive type: S -> S diff --git a/gnovm/tests/files/recursive8.gno b/gnovm/tests/files/recursive8.gno new file mode 100644 index 00000000000..1f9325ae35c --- /dev/null +++ b/gnovm/tests/files/recursive8.gno @@ -0,0 +1,8 @@ +package main + +type Int Int + +func main() {} + +// Error: +// main/files/recursive8.gno:1:1: invalid recursive type: Int -> Int diff --git a/gnovm/tests/files/recursive9.gno b/gnovm/tests/files/recursive9.gno new file mode 100644 index 00000000000..8181be55d33 --- /dev/null +++ b/gnovm/tests/files/recursive9.gno @@ -0,0 +1,8 @@ +package main + +type Int = Int + +func main() {} + +// Error: +// main/files/recursive9.gno:1:1: invalid recursive type: Int -> Int diff --git a/gnovm/tests/files/recursive9a.gno b/gnovm/tests/files/recursive9a.gno new file mode 100644 index 00000000000..b96efa090e4 --- /dev/null +++ b/gnovm/tests/files/recursive9a.gno @@ -0,0 +1,8 @@ +package main + +type Int = *Int + +func main() {} + +// Error: +// main/files/recursive9a.gno:1:1: invalid recursive type: Int -> Int \ No newline at end of file diff --git a/gnovm/tests/files/recursive9b.gno b/gnovm/tests/files/recursive9b.gno new file mode 100644 index 00000000000..e033349d597 --- /dev/null +++ b/gnovm/tests/files/recursive9b.gno @@ -0,0 +1,8 @@ +package main + +type Int = func() Int + +func main() {} + +// Error: +// main/files/recursive9b.gno:1:1: invalid recursive type: Int -> Int \ No newline at end of file diff --git a/gnovm/tests/files/recursive9c.gno b/gnovm/tests/files/recursive9c.gno new file mode 100644 index 00000000000..ad865978920 --- /dev/null +++ b/gnovm/tests/files/recursive9c.gno @@ -0,0 +1,8 @@ +package main + +type Int = []Int + +func main() {} + +// Error: +// main/files/recursive9c.gno:1:1: invalid recursive type: Int -> Int diff --git a/gnovm/tests/files/recursive9d.gno b/gnovm/tests/files/recursive9d.gno new file mode 100644 index 00000000000..ae7310ede0f --- /dev/null +++ b/gnovm/tests/files/recursive9d.gno @@ -0,0 +1,10 @@ +package main + +type S = struct { + *S +} + +func main() {} + +// Error: +// main/files/recursive9d.gno:1:1: invalid recursive type: S -> S diff --git a/gnovm/tests/files/types/cmp_databyte.gno b/gnovm/tests/files/types/cmp_databyte.gno new file mode 100644 index 00000000000..0583ed9a259 --- /dev/null +++ b/gnovm/tests/files/types/cmp_databyte.gno @@ -0,0 +1,12 @@ +package main + +import "bytes" + +func main() { + cmp := bytes.Compare([]byte("hello"), []byte("hey")) + println(cmp) + +} + +// Output: +// -1 diff --git a/gnovm/tests/files/types/cmp_iface_0_stdlibs.gno b/gnovm/tests/files/types/cmp_iface_0_stdlibs.gno new file mode 100644 index 00000000000..fb4ac682243 --- /dev/null +++ b/gnovm/tests/files/types/cmp_iface_0_stdlibs.gno @@ -0,0 +1,27 @@ +package main + +import ( + "errors" + "strconv" +) + +type Error int64 + +func (e Error) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +var errCmp = errors.New("XXXX") + +// special case: +// one is interface +func main() { + if Error(0) == errCmp { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Output: +// something else diff --git a/gnovm/tests/files/types/cmp_iface_1.gno b/gnovm/tests/files/types/cmp_iface_1.gno new file mode 100644 index 00000000000..551b4acf0f1 --- /dev/null +++ b/gnovm/tests/files/types/cmp_iface_1.gno @@ -0,0 +1,29 @@ +package main + +import ( + "errors" + "strconv" +) + +type Error int64 + +func (e Error) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +// typed +var errCmp error = errors.New("XXXX") + +// special case: +// one is interface +func main() { + const e Error = Error(0) // typed const + if e == errCmp { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Output: +// something else diff --git a/gnovm/tests/files/types/cmp_iface_2.gno b/gnovm/tests/files/types/cmp_iface_2.gno new file mode 100644 index 00000000000..5ad121f515b --- /dev/null +++ b/gnovm/tests/files/types/cmp_iface_2.gno @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "strconv" +) + +type E interface { + Error() string +} +type Error int64 + +func (e Error) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +// special case: +// one is interface +func main() { + var e0 E + e0 = Error(0) + fmt.Printf("%T \n", e0) + if e0 == Error(0) { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Output: +// int64 +// what the firetruck? diff --git a/gnovm/tests/files/types/cmp_iface_3_stdlibs.gno b/gnovm/tests/files/types/cmp_iface_3_stdlibs.gno new file mode 100644 index 00000000000..9c4cb0e5ea0 --- /dev/null +++ b/gnovm/tests/files/types/cmp_iface_3_stdlibs.gno @@ -0,0 +1,27 @@ +package main + +import ( + "errors" + "strconv" +) + +type Error int64 + +func (e Error) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +var errCmp = errors.New("XXXX") + +// special case: +// one is interface +func main() { + if Error(1) == errCmp { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Output: +// something else diff --git a/gnovm/tests/files/types/cmp_iface_4.gno b/gnovm/tests/files/types/cmp_iface_4.gno new file mode 100644 index 00000000000..a4ae0463291 --- /dev/null +++ b/gnovm/tests/files/types/cmp_iface_4.gno @@ -0,0 +1,24 @@ +package main + +import ( + "strconv" +) + +type Error int64 + +func (e Error) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +// both not const, and both interface +func main() { + var l interface{} + if l == Error(0) { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Output: +// something else diff --git a/gnovm/tests/files/types/cmp_iface_5_stdlibs.gno b/gnovm/tests/files/types/cmp_iface_5_stdlibs.gno new file mode 100644 index 00000000000..e706c74808e --- /dev/null +++ b/gnovm/tests/files/types/cmp_iface_5_stdlibs.gno @@ -0,0 +1,27 @@ +package main + +import ( + "errors" + "strconv" +) + +type Error int64 + +func (e Error) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +var errCmp = errors.New("XXXX") + +// special case: +// one is interface +func main() { + if errCmp == int64(1) { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Error: +// main/files/types/cmp_iface_5_stdlibs.gno:19:5: int64 does not implement .uverse.error (missing method Error) diff --git a/gnovm/tests/files/types/cmp_iface_6.gno b/gnovm/tests/files/types/cmp_iface_6.gno new file mode 100644 index 00000000000..6abc84992ea --- /dev/null +++ b/gnovm/tests/files/types/cmp_iface_6.gno @@ -0,0 +1,31 @@ +package main + +import ( + "strconv" +) + +type E interface { + Error() string +} + +type Error1 int64 + +func (e Error1) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +type Error2 int64 + +func (e Error2) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +// both not const, and both interface +func main() { + var e1 E = Error1(0) + var e2 E = Error2(0) + println(e1 == e2) +} + +// Output: +// false diff --git a/gnovm/tests/files/types/cmp_iface_7.gno b/gnovm/tests/files/types/cmp_iface_7.gno new file mode 100644 index 00000000000..a0ba3e8a0d3 --- /dev/null +++ b/gnovm/tests/files/types/cmp_iface_7.gno @@ -0,0 +1,24 @@ +package main + +import "fmt" + +func check(v1, v2 interface{}) bool { + return v1 == v2 +} + +func main() { + type t1 int + type t2 int + v1 := t1(1) + v2 := t2(1) + v3 := t2(3) + + fmt.Println("v1, v2", v1, v2, check(v1, v2)) + fmt.Println("v1, v3", v1, v3, check(v1, v3)) + fmt.Println("v2, v3", v2, v3, check(v2, v3)) +} + +// Output: +// v1, v2 1 1 false +// v1, v3 1 3 false +// v2, v3 1 3 false diff --git a/gnovm/tests/files/types/cmp_primitive_0.gno b/gnovm/tests/files/types/cmp_primitive_0.gno new file mode 100644 index 00000000000..2c968b5158f --- /dev/null +++ b/gnovm/tests/files/types/cmp_primitive_0.gno @@ -0,0 +1,24 @@ +package main + +import ( + "strconv" +) + +type Error int8 + +func (e Error) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +// left is untyped const, right is typed const +// left is assignable to right +func main() { + if 1 == Error(1) { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Output: +// what the firetruck? diff --git a/gnovm/tests/files/types/cmp_primitive_1.gno b/gnovm/tests/files/types/cmp_primitive_1.gno new file mode 100644 index 00000000000..2f2e1c94ef1 --- /dev/null +++ b/gnovm/tests/files/types/cmp_primitive_1.gno @@ -0,0 +1,22 @@ +package main + +type Error string + +func (e Error) Error() string { + return "error: " + string(e) +} + +// left is untyped const, right is typed const +// left is not assignable to right +// a) it's (untyped) bigint +// b) base type of right is string +func main() { + if 1 == Error(1) { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Error: +// main/files/types/cmp_primitive_1.gno:14:5: cannot use untyped Bigint as StringKind diff --git a/gnovm/tests/files/types/cmp_primitive_2.gno b/gnovm/tests/files/types/cmp_primitive_2.gno new file mode 100644 index 00000000000..34c8a24cba2 --- /dev/null +++ b/gnovm/tests/files/types/cmp_primitive_2.gno @@ -0,0 +1,15 @@ +package main + +var a int8 + +func main() { + a = 1 + if 1 == a { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Output: +// what the firetruck? diff --git a/gnovm/tests/files/types/cmp_primitive_3.gno b/gnovm/tests/files/types/cmp_primitive_3.gno new file mode 100644 index 00000000000..c1692c8019c --- /dev/null +++ b/gnovm/tests/files/types/cmp_primitive_3.gno @@ -0,0 +1,23 @@ +package main + +import ( + "strconv" +) + +type Error int8 + +func (e Error) Error() string { + return "error: " + strconv.Itoa(int(e)) +} + +// left is typed const, right is untyped const +func main() { + if Error(1) == 1 { + println("what the firetruck?") + } else { + println("something else") + } +} + +// Output: +// what the firetruck? diff --git a/gnovm/tests/files/types/cmp_slice_0.gno b/gnovm/tests/files/types/cmp_slice_0.gno new file mode 100644 index 00000000000..1db537a4d8c --- /dev/null +++ b/gnovm/tests/files/types/cmp_slice_0.gno @@ -0,0 +1,20 @@ +package main + +type S struct { + expected string +} + +// special case when RHS is result of slice operation, its type is determined in runtime +func main() { + s := S{ + expected: `hello`[:], // this is not converted + } + + a := "hello" + + println(a == s.expected) + +} + +// Output: +// true diff --git a/gnovm/tests/files/types/cmp_slice_1.gno b/gnovm/tests/files/types/cmp_slice_1.gno new file mode 100644 index 00000000000..76f2db8d7d8 --- /dev/null +++ b/gnovm/tests/files/types/cmp_slice_1.gno @@ -0,0 +1,10 @@ +package main + +func main() { + expected := `hello`[:] + a := "hello" + println(a == expected) +} + +// Output: +// true diff --git a/gnovm/tests/files/types/cmp_slice_2.gno b/gnovm/tests/files/types/cmp_slice_2.gno new file mode 100644 index 00000000000..018d53fa81a --- /dev/null +++ b/gnovm/tests/files/types/cmp_slice_2.gno @@ -0,0 +1,14 @@ +package main + +type S struct { + expected string +} + +func main() { + println("hello" == S{ + expected: `hello`[:], + }.expected) +} + +// Output: +// true diff --git a/gnovm/tests/files/types/cmp_slice_3.gno b/gnovm/tests/files/types/cmp_slice_3.gno new file mode 100644 index 00000000000..2795d618e91 --- /dev/null +++ b/gnovm/tests/files/types/cmp_slice_3.gno @@ -0,0 +1,16 @@ +package main + +type S struct { + expected string +} + +func main() { + var s = S{ + expected: `hello`[:], + } + a := "hello" + println(a == s.expected) +} + +// Output: +// true diff --git a/gnovm/tests/files/types/cmp_slice_4.gno b/gnovm/tests/files/types/cmp_slice_4.gno new file mode 100644 index 00000000000..2bdd3191eff --- /dev/null +++ b/gnovm/tests/files/types/cmp_slice_4.gno @@ -0,0 +1,10 @@ +package main + +func main() { + expected := `hello`[:] + a := 1 + println(a == expected) // both typed +} + +// Error: +// main/files/types/cmp_slice_4.gno:6:10: cannot use int as string diff --git a/gnovm/tests/files/types/cmp_typeswitch.gno b/gnovm/tests/files/types/cmp_typeswitch.gno new file mode 100644 index 00000000000..721dfc9579a --- /dev/null +++ b/gnovm/tests/files/types/cmp_typeswitch.gno @@ -0,0 +1,18 @@ +package main + +func main() { + var l interface{} + l = int64(0) + + switch val := l.(type) { + case int64, int: + if val == 0 { + println("l is zero") + } else { + println("NOT zero") + } + } +} + +// Output: +// NOT zero diff --git a/gnovm/tests/files/types/cmp_typeswitch_a.gno b/gnovm/tests/files/types/cmp_typeswitch_a.gno new file mode 100644 index 00000000000..f70fcb3d3d6 --- /dev/null +++ b/gnovm/tests/files/types/cmp_typeswitch_a.gno @@ -0,0 +1,18 @@ +package main + +func main() { + var l interface{} + l = int(0) + + switch val := l.(type) { + case int64, int: + if val == 0 { + println("l is zero") + } else { + println("NOT zero") + } + } +} + +// Output: +// l is zero diff --git a/gnovm/tests/files/types/iface_eql.gno b/gnovm/tests/files/types/iface_eql.gno new file mode 100644 index 00000000000..97daa27c184 --- /dev/null +++ b/gnovm/tests/files/types/iface_eql.gno @@ -0,0 +1,9 @@ +package main + +func main() { + var l interface{} = 1 + println(int8(1) == l) +} + +// Output: +// false diff --git a/gnovm/tests/files/var20.gno b/gnovm/tests/files/var20.gno index e2455cbaed8..6e15fcca6c5 100644 --- a/gnovm/tests/files/var20.gno +++ b/gnovm/tests/files/var20.gno @@ -9,4 +9,4 @@ func main() { } // Error: -// main/files/var20.gno:8:6: assignment mismatch: 3 variable(s) but r returns 1 value(s) +// main/files/var20.gno:8:6: assignment mismatch: 3 variable(s) but r() returns 1 value(s) diff --git a/gnovm/tests/files/vardecl.gno b/gnovm/tests/files/vardecl.gno new file mode 100644 index 00000000000..34390f26a6a --- /dev/null +++ b/gnovm/tests/files/vardecl.gno @@ -0,0 +1,23 @@ +package main + +func main() { + var i interface{} = 1 + var a, ok = i.(int) + println(a, ok) + + var b, c = doSomething() + println(b, c) + + m := map[string]int{"a": 1, "b": 2} + var d, okk = m["d"] + println(d, okk) +} + +func doSomething() (int, string) { + return 4, "hi" +} + +// Output: +// 1 true +// 4 hi +// 0 false diff --git a/misc/deployments/staging.gno.land/docker-compose.yml b/misc/deployments/staging.gno.land/docker-compose.yml index 3479067372d..7d264a34dbd 100644 --- a/misc/deployments/staging.gno.land/docker-compose.yml +++ b/misc/deployments/staging.gno.land/docker-compose.yml @@ -1,130 +1,126 @@ -version: "2" - +name: "staging-gno-land" services: + traefik: + image: "traefik:v2.11" + restart: unless-stopped + command: + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.rpc.address=:26657" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + - "--entrypoints.web.http.redirections.entrypoint.permanent=true" + - "--entryPoints.web.forwardedHeaders.insecure" + - "--entrypoints.traefik.address=:8080" + - "--entrypoints.websecure.address=:443" + + - "--certificatesresolvers.le.acme.tlschallenge=true" + - "--certificatesresolvers.le.acme.email=dev@gno.land" + - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json" + ports: + - "80:80" + - "443:443" + - "26657:26657" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + - ./letsencrypt:/letsencrypt + gnoland: - container_name: gnoland - build: ../../.. + image: ghcr.io/gnolang/gno/gnoland:master + restart: unless-stopped + entrypoint: /entrypoint.sh + working_dir: /gnoroot environment: - - VIRTUAL_HOST=rpc.staging.gno.land - - VIRTUAL_PORT=26657 - - LETSENCRYPT_HOST=rpc.staging.gno.land - - LOG_LEVEL=4 - working_dir: /opt/gno/src/gno.land - command: - - gnoland - - start - - --skip-failing-genesis-txs - - --chainid=staging - - --genesis-remote=staging.gno.land:26657 + CHAIN_ID: staging + MONIKER: gno-staging volumes: - - "./data/gnoland:/opt/gno/src/gno.land/gnoland-data" - ports: - - 26656:26656 - - 26657:26657 - restart: on-failure - logging: - driver: "json-file" - options: - max-file: "10" - max-size: "100m" + - ./gnoland.entrypoint.sh:/entrypoint.sh + #ports: + # - 26656:26656 + labels: + com.centurylinklabs.watchtower.enable: "true" + traefik.enable: "true" + traefik.http.routers.gnoland.entrypoints: "web,websecure" + traefik.http.routers.gnoland.rule: "Host(`rpc.staging.gno.land`)" + traefik.http.routers.gnoland.service: gnoland-rpc + traefik.http.routers.gnoland.tls: "true" + traefik.http.routers.gnoland.tls.certresolver: "le" + traefik.http.routers.gnoland-rpc.entrypoints: "rpc" + traefik.http.routers.gnoland-rpc.rule: "PathPrefix(`/`)" + traefik.http.routers.gnoland-rpc.service: gnoland-rpc + traefik.http.services.gnoland-rpc.loadbalancer.server.port: 26657 gnoweb: - container_name: gnoweb - build: ../../.. - command: + image: ghcr.io/gnolang/gno/gnoweb:master + restart: unless-stopped + env_file: ".env" + entrypoint: - gnoweb - - --bind=0.0.0.0:80 - - --remote=gnoland:26657 - - --captcha-site=$RECAPTCHA_SITE_KEY - - --faucet-url=https://faucet-staging.gno.land/ - - --help-chainid=staging - - --help-remote=staging.gno.land:26657 + - --bind=0.0.0.0:8888 + - --remote=http://traefik:26657 + - --faucet-url=https://faucet-api.staging.gno.land + - --captcha-site=$CAPTCHA_SITE_KEY - --with-analytics - volumes: - - "./overlay:/overlay:ro" - links: - - gnoland - environment: - - VIRTUAL_HOST=staging.gno.land - - LETSENCRYPT_HOST=staging.gno.land - # from .env - - RECAPTCHA_SITE_KEY - restart: on-failure - logging: - driver: "json-file" - options: - max-file: "10" - max-size: "100m" + - --help-chainid=staging + - --help-remote=https://rpc.staging.gno.land:443 + labels: + com.centurylinklabs.watchtower.enable: "true" + traefik.enable: "true" + traefik.http.routers.gnoweb.entrypoints: "web,websecure" + traefik.http.routers.gnoweb.rule: "Host(`staging.gno.land`)" + traefik.http.routers.gnoweb.tls: "true" + traefik.http.routers.gnoweb.tls.certresolver: "le" gnofaucet: - container_name: gnofaucet - build: ../../.. - command: sh -xc " - date && - mkdir -p /.gno && - expect -c \"set timeout -1; spawn gnokey add --home /.gno/ --recover faucet; expect \\\"Enter a passphrase\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect \\\"Repeat the passphrase\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect \\\"Enter your bip39 mnemonic\\\"; send \\\"$FAUCET_WORDS\\r\\\"; expect eof\" && - while true; do - expect -c \"set timeout -1; spawn gnofaucet serve --send 50000000ugnot --captcha-secret \\\"$RECAPTCHA_SECRET_KEY\\\" --remote gnoland:26657 --chain-id staging --home /.gno/ faucet; expect \\\"Enter password\\\"; send \\\"$GNOKEY_PASS\\r\\\"; expect eof\"; - sleep 5; - done - " - links: - - gnoland - environment: - - VIRTUAL_HOST=faucet-staging.gno.land - - VIRTUAL_PORT=5050 - - LETSENCRYPT_HOST=faucet-staging.gno.land - # from .env - - RECAPTCHA_SECRET_KEY - - FAUCET_WORDS - - GNOKEY_PASS - ports: - - 5050 - restart: on-failure - logging: - driver: "json-file" - options: - max-file: "10" - max-size: "100m" + image: ghcr.io/gnolang/gno/gnofaucet-slim + restart: unless-stopped + command: + - "serve" + - "--listen-address=0.0.0.0:5050" + - "--chain-id=staging" + - "--is-behind-proxy=true" + - "--mnemonic=${FAUCET_MNEMONIC}" + - "--num-accounts=1" + - "--remote=http://traefik:26657" + - "--captcha-secret=${CAPTCHA_SECRET_KEY}" + env_file: ".env" + # environment: + # from .env + # - RECAPTCHA_SECRET_KEY + labels: + com.centurylinklabs.watchtower.enable: "true" + traefik.enable: "true" + traefik.http.routers.gnofaucet-api.entrypoints: "websecure" + traefik.http.routers.gnofaucet-api.rule: "Host(`faucet-api.staging.gno.land`) || Host(`faucet-api.staging.gnoteam.com`)" + traefik.http.routers.gnofaucet-api.tls: "true" + traefik.http.routers.gnofaucet-api.tls.certresolver: "le" + traefik.http.middlewares.gnofaucet-ratelimit.ratelimit.average: "6" + traefik.http.middlewares.gnofaucet-ratelimit.ratelimit.period: "1m" - nginx-proxy: - image: nginxproxy/nginx-proxy - container_name: nginx-proxy - ports: - - "80:80" - - "443:443" + watchtower: + image: containrrr/watchtower + restart: unless-stopped + command: --interval 30 --http-api-metrics --label-enable volumes: - - conf:/etc/nginx/conf.d - - vhost:/etc/nginx/vhost.d - - html:/usr/share/nginx/html - - certs:/etc/nginx/certs:ro - - /var/run/docker.sock:/tmp/docker.sock:ro - logging: - driver: "json-file" - options: - max-file: "10" - max-size: "100m" - - acme-companion: - image: nginxproxy/acme-companion - container_name: nginx-proxy-acme + - /var/run/docker.sock:/var/run/docker.sock environment: - - DEFAULT_EMAIL=noreply@gno.land - volumes_from: - - nginx-proxy - volumes: - - certs:/etc/nginx/certs:rw - - acme:/etc/acme.sh - - /var/run/docker.sock:/var/run/docker.sock:ro - logging: - driver: "json-file" - options: - max-file: "10" - max-size: "100m" + WATCHTOWER_HTTP_API_TOKEN: "mytoken" -volumes: - conf: - vhost: - html: - certs: - acme: + restarter: + image: docker:cli + restart: unless-stopped + entrypoint: [ "/bin/sh", "-c" ] + working_dir: "/app" + volumes: + - ".:/app" + - "/var/run/docker.sock:/var/run/docker.sock" + command: + - | + while true; do + if [ "$$(date +'%H:%M')" = '22:00' ]; then + docker compose restart gnoland + fi + sleep 60 + done diff --git a/misc/deployments/staging.gno.land/gnoland.entrypoint.sh b/misc/deployments/staging.gno.land/gnoland.entrypoint.sh new file mode 100755 index 00000000000..90957e92da8 --- /dev/null +++ b/misc/deployments/staging.gno.land/gnoland.entrypoint.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env sh + +set -ex + +MONIKER=${MONIKER:-"gnode"} +P2P_LADDR=${P2P_LADDR:-"tcp://0.0.0.0:26656"} +RPC_LADDR=${RPC_LADDR:-"tcp://0.0.0.0:26657"} + +CHAIN_ID=${CHAIN_ID:-"staging"} + +rm -rfv ./gnoland-data genesis.json + +gnoland config init +gnoland secrets init + +gnoland config set moniker "${MONIKER}" +gnoland config set rpc.laddr "${RPC_LADDR}" +gnoland config set p2p.laddr "${P2P_LADDR}" + +exec gnoland start \ + --skip-failing-genesis-txs \ + --chainid="${CHAIN_ID}" \ + --lazy diff --git a/misc/docker-integration/integration_test.go b/misc/docker-integration/integration_test.go index 1142709cc16..973cb386e9b 100644 --- a/misc/docker-integration/integration_test.go +++ b/misc/docker-integration/integration_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/std" @@ -55,7 +56,10 @@ func runSuite(t *testing.T, tempdir string) { var acc gnoland.GnoAccount dockerExec_gnokeyQuery(t, "auth/accounts/"+test1Addr, &acc) require.Equal(t, test1Addr, acc.Address.String(), "test1 account not found") - minCoins := std.MustParseCoins("9990000000000ugnot") // This value is chosen arbitrarily and may not be optimal. Feel free to update it to a more suitable amount + + // This value is chosen arbitrarily and may not be optimal. + // Feel free to update it to a more suitable amount. + minCoins := std.MustParseCoins(ugnot.ValueString(9990000000000)) require.True(t, acc.Coins.IsAllGTE(minCoins), "test1 account coins expected at least %s, got %s", minCoins, acc.Coins)