From 9262c1a8f9495e8195a964a9a4a9963b19c61d33 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 15 Dec 2023 13:57:29 +0100 Subject: [PATCH] feat: Implement `./contribs/gnodev` command (#1386) --- .github/workflows/misc.yml | 2 +- contribs/Makefile | 14 +- contribs/gnodev/Makefile | 9 + contribs/gnodev/README.md | 24 + contribs/gnodev/go.mod | 60 +++ contribs/gnodev/go.sum | 299 ++++++++++ contribs/gnodev/main.go | 378 +++++++++++++ contribs/gnodev/pkg/dev/node.go | 406 ++++++++++++++ contribs/gnodev/pkg/rawterm/keypress.go | 55 ++ contribs/gnodev/pkg/rawterm/rawterm.go | 180 +++++++ gno.land/cmd/gnoweb/main.go | 505 +---------------- gno.land/pkg/gnoland/genesis.go | 3 + gno.land/pkg/gnoland/node_inmemory.go | 32 +- gno.land/pkg/gnoweb/gnoweb.go | 509 ++++++++++++++++++ .../gnoweb/gnoweb_test.go} | 33 +- .../{cmd => pkg}/gnoweb/static/css/app.css | 0 .../gnoweb/static/css/normalize.css | 0 .../{cmd => pkg}/gnoweb/static/font/README.md | 0 .../gnoweb/static/font/roboto/LICENSE.txt | 0 .../static/font/roboto/RobotoMono-Bold.woff | Bin .../font/roboto/RobotoMono-BoldItalic.woff | Bin .../static/font/roboto/RobotoMono-Italic.woff | Bin .../static/font/roboto/RobotoMono-Light.woff | Bin .../font/roboto/RobotoMono-LightItalic.woff | Bin .../static/font/roboto/RobotoMono-Medium.woff | Bin .../font/roboto/RobotoMono-MediumItalic.woff | Bin .../font/roboto/RobotoMono-Regular.woff | Bin .../static/font/roboto/RobotoMono-Thin.woff | Bin .../font/roboto/RobotoMono-ThinItalic.woff | Bin .../gnoweb/static/img/favicon.ico | Bin .../gnoweb/static/img/github-mark-32px.png | Bin .../gnoweb/static/img/github-mark-64px.png | Bin .../gnoweb/static/img/ico-discord.svg | 0 .../gnoweb/static/img/ico-email.svg | 0 .../gnoweb/static/img/ico-telegram.svg | 0 .../gnoweb/static/img/ico-twitter.svg | 0 .../gnoweb/static/img/ico-youtube.svg | 0 .../gnoweb/static/img/list-alt.png | Bin .../{cmd => pkg}/gnoweb/static/img/list.png | Bin .../gnoweb/static/img/logo-square.png | Bin .../gnoweb/static/img/logo-square.svg | 0 .../gnoweb/static/img/logo-v1.png | Bin .../{cmd => pkg}/gnoweb/static/img/logo.png | Bin .../{cmd => pkg}/gnoweb/static/img/logo.svg | 0 .../{cmd => pkg}/gnoweb/static/invites.txt | 0 .../gnoweb/static/js/highlight.min.js | 0 .../gnoweb/static/js/marked.min.js | 0 .../gnoweb/static/js/purify.min.js | 0 .../gnoweb/static/js/realm_help.js | 0 .../{cmd => pkg}/gnoweb/static/js/renderer.js | 0 .../{cmd => pkg}/gnoweb/static/js/umbrella.js | 0 .../gnoweb/static/js/umbrella.min.js | 0 gno.land/{cmd => pkg}/gnoweb/static/static.go | 0 gno.land/{cmd => pkg}/gnoweb/views/404.html | 0 .../{cmd => pkg}/gnoweb/views/faucet.html | 0 gno.land/{cmd => pkg}/gnoweb/views/funcs.html | 2 +- .../{cmd => pkg}/gnoweb/views/generic.html | 0 .../gnoweb/views/package_dir.html | 0 .../gnoweb/views/package_file.html | 0 .../{cmd => pkg}/gnoweb/views/realm_help.html | 0 .../gnoweb/views/realm_render.html | 0 .../{cmd => pkg}/gnoweb/views/redirect.html | 0 gno.land/pkg/integration/testing_node.go | 33 +- 63 files changed, 2016 insertions(+), 528 deletions(-) create mode 100644 contribs/gnodev/Makefile create mode 100644 contribs/gnodev/README.md create mode 100644 contribs/gnodev/go.mod create mode 100644 contribs/gnodev/go.sum create mode 100644 contribs/gnodev/main.go create mode 100644 contribs/gnodev/pkg/dev/node.go create mode 100644 contribs/gnodev/pkg/rawterm/keypress.go create mode 100644 contribs/gnodev/pkg/rawterm/rawterm.go create mode 100644 gno.land/pkg/gnoweb/gnoweb.go rename gno.land/{cmd/gnoweb/main_test.go => pkg/gnoweb/gnoweb_test.go} (88%) rename gno.land/{cmd => pkg}/gnoweb/static/css/app.css (100%) rename gno.land/{cmd => pkg}/gnoweb/static/css/normalize.css (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/README.md (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/LICENSE.txt (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-Bold.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-Italic.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-Light.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-Medium.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-Regular.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-Thin.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/favicon.ico (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/github-mark-32px.png (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/github-mark-64px.png (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/ico-discord.svg (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/ico-email.svg (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/ico-telegram.svg (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/ico-twitter.svg (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/ico-youtube.svg (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/list-alt.png (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/list.png (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/logo-square.png (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/logo-square.svg (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/logo-v1.png (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/logo.png (100%) rename gno.land/{cmd => pkg}/gnoweb/static/img/logo.svg (100%) rename gno.land/{cmd => pkg}/gnoweb/static/invites.txt (100%) rename gno.land/{cmd => pkg}/gnoweb/static/js/highlight.min.js (100%) rename gno.land/{cmd => pkg}/gnoweb/static/js/marked.min.js (100%) rename gno.land/{cmd => pkg}/gnoweb/static/js/purify.min.js (100%) rename gno.land/{cmd => pkg}/gnoweb/static/js/realm_help.js (100%) rename gno.land/{cmd => pkg}/gnoweb/static/js/renderer.js (100%) rename gno.land/{cmd => pkg}/gnoweb/static/js/umbrella.js (100%) rename gno.land/{cmd => pkg}/gnoweb/static/js/umbrella.min.js (100%) rename gno.land/{cmd => pkg}/gnoweb/static/static.go (100%) rename gno.land/{cmd => pkg}/gnoweb/views/404.html (100%) rename gno.land/{cmd => pkg}/gnoweb/views/faucet.html (100%) rename gno.land/{cmd => pkg}/gnoweb/views/funcs.html (99%) rename gno.land/{cmd => pkg}/gnoweb/views/generic.html (100%) rename gno.land/{cmd => pkg}/gnoweb/views/package_dir.html (100%) rename gno.land/{cmd => pkg}/gnoweb/views/package_file.html (100%) rename gno.land/{cmd => pkg}/gnoweb/views/realm_help.html (100%) rename gno.land/{cmd => pkg}/gnoweb/views/realm_render.html (100%) rename gno.land/{cmd => pkg}/gnoweb/views/redirect.html (100%) diff --git a/.github/workflows/misc.yml b/.github/workflows/misc.yml index 64be9b4e829..bd8b53e7a25 100644 --- a/.github/workflows/misc.yml +++ b/.github/workflows/misc.yml @@ -71,7 +71,7 @@ jobs: - name: Check go.mods run: | - set -e + set -xe # Find all go.mod files gomods=$(find . -type f -name go.mod) diff --git a/contribs/Makefile b/contribs/Makefile index b816987b733..a7414771ffa 100644 --- a/contribs/Makefile +++ b/contribs/Makefile @@ -4,9 +4,10 @@ help: @cat Makefile | grep '^[a-z][^:]*:' | cut -d: -f1 | sort | sed 's/^/ /' .PHONY: install -install: install.gnomd +install: install.gnomd install.gnodev -install.gnomd:; cd gnomd && go install . +install.gnomd:; cd gnomd && go install . +install.gnodev:; $(MAKE) -C ./gnodev install .PHONY: clean clean: @@ -21,6 +22,15 @@ GOFMT_FLAGS ?= -w fmt: $(rundep) mvdan.cc/gofumpt $(GOFMT_FLAGS) . +.PHONY: tidy +tidy: + @for gomod in `find . -name go.mod`; do ( \ + dir=`dirname $$gomod`; \ + set -xe; \ + cd $$dir; \ + go mod tidy -v; \ + ); done + ######################################## # Test suite GOTEST_FLAGS ?= -v -p 1 -timeout=30m diff --git a/contribs/gnodev/Makefile b/contribs/gnodev/Makefile new file mode 100644 index 00000000000..23fb22a372d --- /dev/null +++ b/contribs/gnodev/Makefile @@ -0,0 +1,9 @@ +GNOROOT_DIR ?= $(abspath $(lastword $(MAKEFILE_LIST))/../../../) + +GOBUILD_FLAGS := -ldflags "-X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)" + +install: + go install $(GOBUILD_FLAGS) . + +build: + go build $(GOBUILD_FLAGS) -o build/gnodev ./cmd/gno diff --git a/contribs/gnodev/README.md b/contribs/gnodev/README.md new file mode 100644 index 00000000000..b3148c5dea3 --- /dev/null +++ b/contribs/gnodev/README.md @@ -0,0 +1,24 @@ +## `gnodev`: Your Gno Companion Tool + +`gnodev` is designed to be a robust and user-friendly tool in your realm package development journey, streamlining your workflow and enhancing productivity. + +### Synopsis +**gnodev** [**-minimal**] [**-no-watch**] [**PKGS_PATH ...**] + +### Features +- **In-Memory Node**: Automatically loads the **example** folder and any user-specified paths. +- **Web Interface Server**: Starts a gno.land web server on `:8888`. +- **Hot Reload**: Monitors the example packages folder and additional directories for file changes, reloading the package and restarting the node as needed. +- **State Maintenance**: Ensures the current state is maintained by reapplying all previous blocks. + +### Commands +- **H**: Display help information. +- **R**: Reload the node. +- **Ctrl+R**: Reset the current node state. +- **Ctrl+C**: Exit the command. + +### Example Folder Loading +The **example** package folder 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`. diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod new file mode 100644 index 00000000000..32f70de42cc --- /dev/null +++ b/contribs/gnodev/go.mod @@ -0,0 +1,60 @@ +module github.com/gnolang/gno/contribs/gnodev + +go 1.20 + +replace github.com/gnolang/gno => ../.. + +require ( + github.com/fsnotify/fsnotify v1.7.0 + github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + golang.org/x/term v0.15.0 +) + +require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btcd/btcutil v1.1.3 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/dgraph-io/badger/v3 v3.2103.4 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/gnolang/goleveldb v0.0.9 // indirect + github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/flatbuffers v1.12.1 // 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 + github.com/gorilla/websocket v1.5.1 // indirect + github.com/gotuna/gotuna v0.6.0 // indirect + github.com/jaekwon/testify v1.6.1 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/klauspost/compress v1.12.3 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/linxGnu/grocksdb v1.8.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 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/rs/cors v1.10.1 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect + go.etcd.io/bbolt v1.3.8 // indirect + go.opencensus.io v0.22.5 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/tools v0.13.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum new file mode 100644 index 00000000000..bdc46387a5e --- /dev/null +++ b/contribs/gnodev/go.sum @@ -0,0 +1,299 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +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.0 h1:V2/ZgjfDFIygAX3ZapeigkVBoVUtOJKSwrhZdlpSvaA= +github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= +github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgraph-io/badger/v3 v3.2103.4 h1:WE1B07YNTTJTtG9xjBcSW2wn0RJLyiV99h959RKZqM4= +github.com/dgraph-io/badger/v3 v3.2103.4/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +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/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gnolang/goleveldb v0.0.9 h1:Q7rGko9oXMKtQA+Apeeed5a3sjba/mcDhzJGoTVLCKE= +github.com/gnolang/goleveldb v0.0.9/go.mod h1:Dz6p9bmpy/FBESTgduiThZt5mToVDipcHGzj/zUOo8E= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= +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= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +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/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jaekwon/testify v1.6.1 h1:4AtAJcR9GzXN5W4DdY7ie74iCPiJV1JJUJL90t2ZUyw= +github.com/jaekwon/testify v1.6.1/go.mod h1:Oun0RXIHI7osufabQ60i4Lqkj0GXLbqI1I7kgzBNm1U= +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= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +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/linxGnu/grocksdb v1.8.5 h1:Okfk5B1h0ikCYdDM7Tc5yJUS8LTwAmMBq5IPWTmOLPs= +github.com/linxGnu/grocksdb v1.8.5/go.mod h1:xZCIb5Muw+nhbDK4Y5UJuOrin5MceOuiXkVUR7vp4WY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +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= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= +github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/contribs/gnodev/main.go b/contribs/gnodev/main.go new file mode 100644 index 00000000000..894d8b36738 --- /dev/null +++ b/contribs/gnodev/main.go @@ -0,0 +1,378 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" + "github.com/gnolang/gno/gno.land/pkg/gnoweb" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/tm2/pkg/commands" + tmlog "github.com/gnolang/gno/tm2/pkg/log" + osm "github.com/gnolang/gno/tm2/pkg/os" +) + +const ( + NodeLogName = "Node" + WebLogName = "GnoWeb" + KeyPressLogName = "KeyPress" + HotReloadLogName = "HotReload" +) + +type devCfg struct { + webListenerAddr string + minimal bool + verbose bool + noWatch bool +} + +var defaultDevOptions = &devCfg{ + webListenerAddr: "127.0.0.1:8888", +} + +func main() { + cfg := &devCfg{} + + stdio := commands.NewDefaultIO() + cmd := commands.NewCommand( + commands.Metadata{ + Name: "gnodev", + ShortUsage: "gnodev [flags] [path ...]", + ShortHelp: "Runs an in-memory node and gno.land web server for development purposes.", + LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface +primarily for realm package development. It automatically loads the example folder and any +additional specified paths.`, + }, + cfg, + func(_ context.Context, args []string) error { + return execDev(cfg, args, stdio) + }) + + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "%+v\n", err) + os.Exit(1) + } +} +func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.webListenerAddr, + "web-listener", + defaultDevOptions.webListenerAddr, + "web server listening address", + ) + + fs.BoolVar( + &c.minimal, + "minimal", + defaultDevOptions.verbose, + "do not load example folder packages", + ) + + fs.BoolVar( + &c.verbose, + "verbose", + defaultDevOptions.verbose, + "verbose output when deving", + ) + + fs.BoolVar( + &c.noWatch, + "no-watch", + defaultDevOptions.noWatch, + "do not watch for files change", + ) + +} + +func execDev(cfg *devCfg, args []string, io commands.IO) error { + ctx, cancel := context.WithCancelCause(context.Background()) + defer cancel(nil) + + // guess root dir + gnoroot := gnoenv.RootDir() + + // Check and Parse packages + pkgpaths, err := parseArgsPackages(args) + if err != nil { + return fmt.Errorf("unable to parse package paths: %w", err) + } + + if !cfg.minimal { + examplesDir := filepath.Join(gnoroot, "examples") + pkgpaths = append(pkgpaths, examplesDir) + } + + // Setup Raw Terminal + rt, restore, err := setupRawTerm(io) + if err != nil { + return fmt.Errorf("unable to init raw term: %w", err) + } + defer restore() + + // Setup trap signal + osm.TrapSignal(func() { + restore() + cancel(nil) + }) + + // Setup Dev Node + // XXX: find a good way to export or display node logs + devNode, err := setupDevNode(ctx, rt, pkgpaths, gnoroot) + if err != nil { + return err + } + defer devNode.Close() + + rt.Taskf(NodeLogName, "Listener: %s\n", devNode.GetRemoteAddress()) + rt.Taskf(NodeLogName, "Default Address: %s\n", gnodev.DefaultCreator.String()) + rt.Taskf(NodeLogName, "Chain ID: %s\n", devNode.Config().ChainID()) + + // Setup packages watcher + pathChangeCh := make(chan []string, 1) + go func() { + defer close(pathChangeCh) + + cancel(runPkgsWatcher(ctx, cfg, devNode.ListPkgs(), pathChangeCh)) + }() + + // Setup GnoWeb listener + l, err := net.Listen("tcp", cfg.webListenerAddr) + if err != nil { + return fmt.Errorf("unable to listen to %q: %w", cfg.webListenerAddr, err) + } + defer l.Close() + + // Run GnoWeb server + go func() { + cancel(serveGnoWebServer(l, devNode, rt)) + }() + + rt.Taskf(WebLogName, "Listener: http://%s\n", l.Addr()) + + // GnoDev should be ready, run event loop + rt.Taskf("[Ready]", "for commands and help, press `h`") + + // Run the main event loop + return runEventLoop(ctx, cfg, rt, devNode, pathChangeCh) +} + +// XXX: Automatize this the same way command does +func printHelper(rt *rawterm.RawTerm) { + rt.Taskf("Helper", ` +Gno Dev Helper: + h, H Help - display this message + r, R Reload - Reload all packages to take change into account. + Ctrl+R Reset - Reset application state. + Ctrl+C Exit - Exit the application +`) +} + +func runEventLoop(ctx context.Context, + cfg *devCfg, + rt *rawterm.RawTerm, + dnode *dev.Node, + pathsCh <-chan []string, +) error { + nodeOut := rt.NamespacedWriter(NodeLogName) + keyOut := rt.NamespacedWriter(KeyPressLogName) + + keyPressCh := listenForKeyPress(keyOut, rt) + for { + var err error + + select { + case <-ctx.Done(): + return context.Cause(ctx) + case paths, ok := <-pathsCh: + if !ok { + return nil + } + + if cfg.verbose { + for _, path := range paths { + rt.Taskf(HotReloadLogName, "path %q has been modified", path) + } + } + + fmt.Fprintln(nodeOut, "Loading package updates...") + if err = dnode.UpdatePackages(paths...); err != nil { + checkForError(rt, err) + continue + } + + fmt.Fprintln(nodeOut, "Reloading...") + err = dnode.Reload(ctx) + checkForError(rt, err) + case key, ok := <-keyPressCh: + if !ok { + return nil + } + + if cfg.verbose { + fmt.Fprintf(keyOut, "<%s>\n", key.String()) + } + + switch key.Upper() { + case rawterm.KeyH: + printHelper(rt) + case rawterm.KeyR: + fmt.Fprintln(nodeOut, "Reloading all packages...") + checkForError(nodeOut, dnode.ReloadAll(ctx)) + case rawterm.KeyCtrlR: + fmt.Fprintln(nodeOut, "Reseting state...") + checkForError(nodeOut, dnode.Reset(ctx)) + case rawterm.KeyCtrlC: + return nil + default: + } + + // Listen for the next keypress + keyPressCh = listenForKeyPress(keyOut, rt) + } + } +} + +func runPkgsWatcher(ctx context.Context, cfg *devCfg, pkgs []gnomod.Pkg, changedPathsCh chan<- []string) error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return fmt.Errorf("unable to watch files: %w", err) + } + + if cfg.noWatch { + // noop watcher, wait until context has been cancel + <-ctx.Done() + return ctx.Err() + } + + for _, pkg := range pkgs { + if err := watcher.Add(pkg.Dir); err != nil { + return fmt.Errorf("unable to watch %q: %w", pkg.Dir, err) + } + } + + const timeout = time.Millisecond * 500 + + var debounceTimer <-chan time.Time + pathList := []string{} + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case watchErr := <-watcher.Errors: + return fmt.Errorf("watch error: %w", watchErr) + case <-debounceTimer: + changedPathsCh <- pathList + // Reset pathList and debounceTimer + pathList = []string{} + debounceTimer = nil + case evt := <-watcher.Events: + if evt.Op != fsnotify.Write { + continue + } + + pathList = append(pathList, evt.Name) + debounceTimer = time.After(timeout) + } + } +} + +func setupRawTerm(io commands.IO) (rt *rawterm.RawTerm, restore func() error, err error) { + rt = rawterm.NewRawTerm() + + restore, err = rt.Init() + if err != nil { + return nil, nil, err + } + + // correctly format output for terminal + io.SetOut(commands.WriteNopCloser(rt)) + + return rt, restore, nil +} + +// setupDevNode initializes and returns a new DevNode. +func setupDevNode(ctx context.Context, rt *rawterm.RawTerm, pkgspath []string, gnoroot string) (*gnodev.Node, error) { + nodeOut := rt.NamespacedWriter("Node") + + logger := tmlog.NewTMLogger(nodeOut) + logger.SetLevel(tmlog.LevelError) + return gnodev.NewDevNode(ctx, logger, pkgspath) +} + +// setupGnowebServer initializes and starts the Gnoweb server. +func serveGnoWebServer(l net.Listener, dnode *gnodev.Node, rt *rawterm.RawTerm) error { + var server http.Server + + webConfig := gnoweb.NewDefaultConfig() + webConfig.RemoteAddr = dnode.GetRemoteAddress() + + loggerweb := tmlog.NewTMLogger(rt.NamespacedWriter("GnoWeb")) + loggerweb.SetLevel(tmlog.LevelDebug) + + app := gnoweb.MakeApp(loggerweb, webConfig) + + server.ReadHeaderTimeout = 60 * time.Second + server.Handler = app.Router + + if err := server.Serve(l); err != nil { + return fmt.Errorf("unable to serve GnoWeb: %w", err) + } + + return nil +} + +func parseArgsPackages(args []string) (paths []string, err error) { + paths = make([]string, len(args)) + for i, arg := range args { + abspath, err := filepath.Abs(arg) + if err != nil { + return nil, fmt.Errorf("invalid path %q: %w", arg, err) + } + + ppath, err := gnomod.FindRootDir(abspath) + if err != nil { + return nil, fmt.Errorf("unable to find root dir of %q: %w", abspath, err) + } + + paths[i] = ppath + } + + return paths, nil +} + +func listenForKeyPress(w io.Writer, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { + cc := make(chan rawterm.KeyPress, 1) + go func() { + defer close(cc) + key, err := rt.ReadKeyPress() + if err != nil { + fmt.Fprintf(w, "unable to read keypress: %s\n", err.Error()) + return + } + + cc <- key + }() + + return cc +} + +func checkForError(w io.Writer, err error) { + if err != nil { + fmt.Fprintf(w, "[ERROR] - %s\n", err.Error()) + return + } + + fmt.Fprintln(w, "[DONE]") +} diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go new file mode 100644 index 00000000000..6624edc13c1 --- /dev/null +++ b/contribs/gnodev/pkg/dev/node.go @@ -0,0 +1,406 @@ +package dev + +import ( + "context" + "fmt" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/integration" + vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/bft/node" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/std" + //backup "github.com/gnolang/tx-archive/backup/client" + //restore "github.com/gnolang/tx-archive/restore/client" +) + +const gnoDevChainID = "tendermint_test" // XXX: this is hardcoded and cannot be change bellow + +// DevNode is a backup.Client +// var _ backup.Client = (*Node)(nil) + +// DevNode is a restore.Client +// var _ restore.Client = (*Node)(nil) + +// Node is not thread safe +type Node struct { + *node.Node + + logger log.Logger + pkgs PkgsMap // path -> pkg +} + +var ( + DefaultFee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + DefaultCreator = crypto.MustAddressFromString(integration.DefaultAccount_Address) + DefaultBalance = []gnoland.Balance{ + { + Address: DefaultCreator, + Amount: std.MustParseCoins("10000000000000ugnot"), + }, + } +) + +func NewDevNode(ctx context.Context, logger log.Logger, pkgslist []string) (*Node, error) { + mpkgs, err := newPkgsMap(pkgslist) + if err != nil { + return nil, fmt.Errorf("unable map pkgs list: %w", err) + } + + txs, err := mpkgs.Load(DefaultCreator, DefaultFee, nil) + if err != nil { + return nil, fmt.Errorf("unable to load genesis packages: %w", err) + } + + // generate genesis state + genesis := gnoland.GnoGenesisState{ + Balances: DefaultBalance, + Txs: txs, + } + + node, err := newNode(logger, genesis) + if err != nil { + return nil, fmt.Errorf("unable to create the node: %w", err) + } + + if err := node.Start(); err != nil { + return nil, fmt.Errorf("unable to start node: %w", err) + } + + // Wait for readiness + select { + case <-gnoland.GetNodeReadiness(node): // ok + case <-ctx.Done(): + return nil, ctx.Err() + } + + return &Node{ + Node: node, + pkgs: mpkgs, + logger: logger, + }, nil +} + +func (d *Node) getLatestBlockNumber() uint64 { + return uint64(d.Node.BlockStore().Height()) +} + +func (d *Node) Close() error { + return d.Node.Stop() +} + +func (d *Node) ListPkgs() []gnomod.Pkg { + return d.pkgs.toList() +} + +func (d *Node) GetNodeReadiness() <-chan struct{} { + return gnoland.GetNodeReadiness(d.Node) +} + +func (d *Node) GetRemoteAddress() string { + return d.Node.Config().RPC.ListenAddress +} + +func (d *Node) UpdatePackages(paths ...string) error { + for _, path := range paths { + // list all packages from target path + pkgslist, err := gnomod.ListPkgs(path) + if err != nil { + return fmt.Errorf("failed to list gno packages for %q: %w", path, err) + } + + for _, pkg := range pkgslist { + d.pkgs[pkg.Dir] = pkg + } + } + + return nil +} + +func (d *Node) Reset(ctx context.Context) error { + if d.Node.IsRunning() { + if err := d.Node.Stop(); err != nil { + return fmt.Errorf("unable to stop the node: %w", err) + } + } + + // generate genesis + txs, err := d.pkgs.Load(DefaultCreator, DefaultFee, nil) + if err != nil { + return fmt.Errorf("unable to load pkgs: %w", err) + } + + genesis := gnoland.GnoGenesisState{ + Balances: DefaultBalance, + Txs: txs, + } + + return d.reset(ctx, genesis) +} + +func (d *Node) ReloadAll(ctx context.Context) error { + pkgs := d.ListPkgs() + paths := make([]string, len(pkgs)) + for i, pkg := range pkgs { + paths[i] = pkg.Dir + } + + if err := d.UpdatePackages(paths...); err != nil { + return fmt.Errorf("unable to reload packages: %w", err) + } + + return d.Reload(ctx) +} + +func (d *Node) Reload(ctx context.Context) error { + + // save current state + state, err := d.saveState(ctx) + if err != nil { + return fmt.Errorf("unable to save state: %s", err.Error()) + } + + // stop the node if not already stopped + if d.Node.IsRunning() { + if err := d.Node.Stop(); err != nil { + return fmt.Errorf("unable to stop the node: %w", err) + } + } + + // generate genesis + txs, err := d.pkgs.Load(DefaultCreator, DefaultFee, nil) + if err != nil { + return fmt.Errorf("unable to load pkgs: %w", err) + } + + genesis := gnoland.GnoGenesisState{ + Balances: DefaultBalance, + Txs: txs, + } + + // try to reset the node + if err := d.reset(ctx, genesis); err != nil { + return fmt.Errorf("unable to reset the node: %w", err) + } + + for _, tx := range state { + // skip empty transaction + if len(tx.Msgs) == 0 { + continue + } + + if err := d.SendTransaction(&tx); err != nil { + return fmt.Errorf("unable to send transaction: %w", err) + } + } + + return nil +} + +func (d *Node) reset(ctx context.Context, genesis gnoland.GnoGenesisState) error { + var err error + + recoverError := func() { + if r := recover(); r != nil { + panicErr, ok := r.(error) + if !ok { + panic(r) + } + + err = panicErr + } + } + + createNode := func() { + defer recoverError() + + node, nodeErr := newNode(d.logger, genesis) + if nodeErr != nil { + err = fmt.Errorf("unable to create node: %w", nodeErr) + return + } + + if startErr := node.Start(); startErr != nil { + err = fmt.Errorf("unable to start the node: %w", startErr) + return + } + + d.Node = node + } + + // create the node + createNode() + if err != nil { + return err + } + + // wait for readiness + select { + case <-d.GetNodeReadiness(): // ok + case <-ctx.Done(): + return ctx.Err() + } + + return err +} + +// GetBlockTransactions returns the transactions contained +// within the specified block, if any +func (d *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) { + b := d.Node.BlockStore().LoadBlock(int64(blockNum)) + txs := make([]std.Tx, len(b.Txs)) + for i, encodedTx := range b.Txs { + var tx std.Tx + if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil { + return nil, fmt.Errorf("unable to unmarshal amino tx, %w", unmarshalErr) + } + + txs[i] = tx + } + + return txs, nil +} + +// GetBlockTransactions returns the transactions contained +// within the specified block, if any +// GetLatestBlockNumber returns the latest block height from the chain +func (d *Node) GetLatestBlockNumber() (uint64, error) { + return d.getLatestBlockNumber(), nil +} + +// SendTransaction executes a broadcast sync send +// of the specified transaction to the chain +func (d *Node) SendTransaction(tx *std.Tx) error { + resCh := make(chan abci.Response, 1) + + aminoTx, err := amino.Marshal(tx) + if err != nil { + return fmt.Errorf("unable to marshal transaction to amino binary, %w", err) + } + + err = d.Node.Mempool().CheckTx(aminoTx, func(res abci.Response) { + resCh <- res + }) + if err != nil { + return fmt.Errorf("unable to check tx: %w", err) + } + + res := <-resCh + r := res.(abci.ResponseCheckTx) + if r.Error != nil { + return fmt.Errorf("unable to broadcast tx: %w\nLog: %s", r.Error, r.Log) + } + + return nil +} + +func (n *Node) saveState(ctx context.Context) ([]std.Tx, error) { + lastBlock := n.getLatestBlockNumber() + + state := make([]std.Tx, 0, int(lastBlock)) + var blocnum uint64 = 1 + for ; blocnum <= lastBlock; blocnum++ { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + txs, txErr := n.GetBlockTransactions(blocnum) + if txErr != nil { + return nil, fmt.Errorf("unable to fetch block transactions, %w", txErr) + } + + // Skip empty blocks + state = append(state, txs...) + } + + // override current state + return state, nil +} + +type PkgsMap map[string]gnomod.Pkg + +func newPkgsMap(paths []string) (PkgsMap, error) { + pkgs := make(map[string]gnomod.Pkg) + for _, path := range paths { + // list all packages from target path + pkgslist, err := gnomod.ListPkgs(path) + if err != nil { + return nil, fmt.Errorf("listing gno packages: %w", err) + } + + for _, pkg := range pkgslist { + if pkg.Dir == "" { + continue + } + + if _, ok := pkgs[pkg.Dir]; ok { + continue // skip + } + pkgs[pkg.Dir] = pkg + } + } + + // Filter out draft packages. + return pkgs, nil +} + +func (pm PkgsMap) toList() gnomod.PkgList { + list := make([]gnomod.Pkg, 0, len(pm)) + for _, pkg := range pm { + list = append(list, pkg) + } + return list +} + +func (pm PkgsMap) Load(creator bft.Address, fee std.Fee, deposit std.Coins) ([]std.Tx, error) { + pkgs := pm.toList() + + sorted, err := pkgs.Sort() + if err != nil { + return nil, fmt.Errorf("unable to sort pkgs: %w", err) + } + + nonDraft := sorted.GetNonDraftPkgs() + txs := []std.Tx{} + for _, pkg := range nonDraft { + // Open files in directory as MemPackage. + memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + if err := memPkg.Validate(); err != nil { + return nil, fmt.Errorf("invalid package: %w", err) + } + + // Create transaction + tx := std.Tx{ + Fee: fee, + Msgs: []std.Msg{ + vmm.MsgAddPackage{ + Creator: creator, + Package: memPkg, + Deposit: deposit, + }, + }, + } + + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + txs = append(txs, tx) + } + + return txs, nil +} + +func newNode(logger log.Logger, genesis gnoland.GnoGenesisState) (*node.Node, error) { + rootdir := gnoenv.RootDir() + + nodeConfig := gnoland.NewDefaultInMemoryNodeConfig(rootdir) + nodeConfig.Genesis.AppState = genesis + return gnoland.NewInMemoryNode(logger, nodeConfig) +} diff --git a/contribs/gnodev/pkg/rawterm/keypress.go b/contribs/gnodev/pkg/rawterm/keypress.go new file mode 100644 index 00000000000..20503476a9b --- /dev/null +++ b/contribs/gnodev/pkg/rawterm/keypress.go @@ -0,0 +1,55 @@ +package rawterm + +import ( + "fmt" + "unicode" +) + +type KeyPress byte + +// key representation +const ( + KeyNone KeyPress = 0 // None + KeyCtrlC KeyPress = '\x03' // Ctrl+C + KeyCtrlD KeyPress = '\x04' // Ctrl+D + KeyCtrlE KeyPress = '\x05' // Ctrl+E + KeyCtrlL KeyPress = '\x0c' // Ctrl+L + KeyCtrlO KeyPress = '\x0f' // Ctrl+O + KeyCtrlR KeyPress = '\x12' // Ctrl+R + KeyCtrlT KeyPress = '\x14' // Ctrl+T + + KeyH KeyPress = 'H' + KeyR KeyPress = 'R' +) + +func (k KeyPress) Upper() KeyPress { + return KeyPress(unicode.ToUpper(rune(k))) +} + +func (k KeyPress) String() string { + switch k { + case KeyNone: + return "Null" + case KeyCtrlC: + return "Ctrl+C" + case KeyCtrlD: + return "Ctrl+D" + case KeyCtrlE: + return "Ctrl+E" + case KeyCtrlL: + return "Ctrl+L" + case KeyCtrlO: + return "Ctrl+O" + case KeyCtrlR: + return "Ctrl+R" + case KeyCtrlT: + return "Ctrl+T" + default: + // For printable ASCII characters + if k > 0x20 && k < 0x7e { + return fmt.Sprintf("%c", k) + } + + return fmt.Sprintf("Unknown (0x%02x)", byte(k)) + } +} diff --git a/contribs/gnodev/pkg/rawterm/rawterm.go b/contribs/gnodev/pkg/rawterm/rawterm.go new file mode 100644 index 00000000000..8f29e88fc92 --- /dev/null +++ b/contribs/gnodev/pkg/rawterm/rawterm.go @@ -0,0 +1,180 @@ +package rawterm + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "sync" + + "golang.org/x/term" +) + +var ( + CRLF = []byte{'\r', '\n'} +) + +// rawTerminal wraps an io.Writer, converting \n to \r\n +type RawTerm struct { + syncWriter sync.Mutex + + fsin *os.File + reader io.Reader + taskWriter TaskWriter +} + +func NewRawTerm() *RawTerm { + return &RawTerm{ + fsin: os.Stdin, + reader: os.Stdin, + taskWriter: &rawTaskWriter{os.Stdout}, + } +} + +func (rt *RawTerm) Init() (restore func() error, err error) { + fd := int(rt.fsin.Fd()) + oldstate, err := term.MakeRaw(fd) + if err != nil { + return nil, fmt.Errorf("unable to init raw term: %w", err) + } + + rt.reader = rt.fsin + rt.taskWriter = &columnTaskWriter{os.Stdout} + return func() error { + return term.Restore(fd, oldstate) + }, nil +} + +func (rt *RawTerm) Taskf(task string, format string, args ...interface{}) (n int, err error) { + format = strings.TrimSpace(format) + if len(args) > 0 { + str := fmt.Sprintf(format, args...) + return rt.taskWriter.WriteTask(task, []byte(str+"\n")) + } + + return rt.taskWriter.WriteTask(task, []byte(format+"\n")) +} + +func (rt *RawTerm) Write(buf []byte) (n int, err error) { + rt.syncWriter.Lock() + defer rt.syncWriter.Unlock() + + return rt.taskWriter.Write(buf) +} + +func (rt *RawTerm) WriteTask(name string, buf []byte) (n int, err error) { + rt.syncWriter.Lock() + defer rt.syncWriter.Unlock() + + return rt.taskWriter.WriteTask(name, buf) +} + +func (rt *RawTerm) NamespacedWriter(namepsace string) io.Writer { + return &namespaceWriter{namepsace, rt} +} + +func (rt *RawTerm) read(buf []byte) (n int, err error) { + return rt.fsin.Read(buf) +} + +func (rt *RawTerm) ReadKeyPress() (KeyPress, error) { + buf := make([]byte, 1) + if _, err := rt.read(buf); err != nil { + return KeyNone, err + } + + return KeyPress(buf[0]), nil +} + +type namespaceWriter struct { + namespace string + writer TaskWriter +} + +func (r *namespaceWriter) Write(buf []byte) (n int, err error) { + return r.writer.WriteTask(r.namespace, buf) +} + +type TaskWriter interface { + io.Writer + WriteTask(task string, buf []byte) (n int, err error) +} + +type columnTaskWriter struct { + writer io.Writer +} + +func (r *columnTaskWriter) Write(buf []byte) (n int, err error) { + return r.WriteTask("", buf) +} + +func (r *columnTaskWriter) WriteTask(left string, buf []byte) (n int, err error) { + var nline int + for nline = 0; len(buf) > 0; nline++ { + i := bytes.IndexByte(buf, '\n') + todo := len(buf) + if i >= 0 { + todo = i + } + + var nn int + switch { + case nline == 0, left == "": // first line or left side is empty + nn, err = r.writeColumnLine(left, buf[:todo]) + case i < 0 || i+1 == len(buf): // last line + nn, err = r.writeColumnLine(" └─", buf[:todo]) + default: // middle lines + nn, err = r.writeColumnLine(" │", buf[:todo]) + } + + n += nn + if err != nil { + return n, err + } + buf = buf[todo:] + + if i >= 0 { // always jump a line on the last line + if _, err = r.writer.Write(CRLF); err != nil { + return n, err + } + n++ + buf = buf[1:] + } + } + + return +} + +func (r *columnTaskWriter) writeColumnLine(left string, line []byte) (n int, err error) { + // Write left column + if n, err = fmt.Fprintf(r.writer, "%-15s | ", left); err != nil { + return n, err + } + + // Write left line + var nn int + nn, err = r.writer.Write(line) + n += nn + + return +} + +type rawTaskWriter struct { + writer io.Writer +} + +func (r *rawTaskWriter) Write(buf []byte) (n int, err error) { + return r.writer.Write(buf) +} + +func (r *rawTaskWriter) WriteTask(task string, buf []byte) (n int, err error) { + if task != "" { + n, err = r.writer.Write([]byte(task + ": ")) + } + + var nn int + nn, err = r.writer.Write(buf) + n += nn + return +} diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index b080e0b403d..f9e8c973cb4 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -1,505 +1,54 @@ -// main.go - package main import ( - "encoding/json" - "errors" "flag" - "fmt" - "io" "net/http" "os" - "path/filepath" - "runtime" - "strings" "time" - "github.com/gnolang/gno/tm2/pkg/amino" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gorilla/mux" - "github.com/gotuna/gotuna" - - "github.com/gnolang/gno/gno.land/cmd/gnoweb/static" // for static files - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types + // for static files + "github.com/gnolang/gno/gno.land/pkg/gnoweb" + "github.com/gnolang/gno/tm2/pkg/log" + // for error types // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status) ) -const ( - qFileStr = "vm/qfile" -) +func parseConfigFlags(fs *flag.FlagSet, args []string) (gnoweb.Config, error) { + cfg := gnoweb.NewDefaultConfig() -var startedAt time.Time + fs.StringVar(&cfg.RemoteAddr, "remote", cfg.RemoteAddr, "remote gnoland node address") + fs.StringVar(&cfg.CaptchaSite, "captcha-site", cfg.CaptchaSite, "recaptcha site key (if empty, captcha are disabled)") + fs.StringVar(&cfg.FaucetURL, "faucet-url", cfg.FaucetURL, "faucet server URL") + fs.StringVar(&cfg.ViewsDir, "views-dir", cfg.ViewsDir, "views directory location") // XXX: replace with goembed + fs.StringVar(&cfg.HelpChainID, "help-chainid", cfg.HelpChainID, "help page's chainid") + fs.StringVar(&cfg.HelpRemote, "help-remote", cfg.HelpRemote, "help page's remote addr") + fs.BoolVar(&cfg.WithAnalytics, "with-analytics", cfg.WithAnalytics, "enable privacy-first analytics") -var flags struct { - BindAddr string - RemoteAddr string - CaptchaSite string - FaucetURL string - ViewsDir string - HelpChainID string - HelpRemote string - WithAnalytics bool + return cfg, fs.Parse(args) } -func init() { - flag.StringVar(&flags.RemoteAddr, "remote", "127.0.0.1:26657", "remote gnoland node address") - flag.StringVar(&flags.BindAddr, "bind", "127.0.0.1:8888", "server listening address") - flag.StringVar(&flags.CaptchaSite, "captcha-site", "", "recaptcha site key (if empty, captcha are disabled)") - flag.StringVar(&flags.FaucetURL, "faucet-url", "http://localhost:5050", "faucet server URL") - flag.StringVar(&flags.ViewsDir, "views-dir", "./cmd/gnoweb/views", "views directory location") // XXX: replace with goembed - flag.StringVar(&flags.HelpChainID, "help-chainid", "dev", "help page's chainid") - flag.StringVar(&flags.HelpRemote, "help-remote", "127.0.0.1:26657", "help page's remote addr") - flag.BoolVar(&flags.WithAnalytics, "with-analytics", false, "enable privacy-first analytics") - startedAt = time.Now() -} +func main() { + fs := flag.NewFlagSet("gnoweb", flag.PanicOnError) -func makeApp() gotuna.App { - app := gotuna.App{ - ViewFiles: os.DirFS(flags.ViewsDir), - Router: gotuna.NewMuxRouter(), - Static: static.EmbeddedStatic, - // StaticPrefix: "static/", - } + var bindAddress string + fs.StringVar(&bindAddress, "bind", "127.0.0.1:8888", "server listening address") - // 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", - "/game-of-realms": "/r/gnoland/pages:p/gor", // XXX: replace with gor realm - "/events": "/r/gnoland/pages:p/events", // XXX: replace with events realm - } - for from, to := range aliases { - app.Router.Handle(from, handlerRealmAlias(app, 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", - } - for from, to := range redirects { - app.Router.Handle(from, handlerRedirect(app, to)) + cfg, err := parseConfigFlags(fs, os.Args) + if err != nil { + panic("unable to parse flags: " + err.Error()) } - // realm routes - // NOTE: see rePathPart. - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}/{filename:(?:.*\\.(?:gno|md|txt)$)?}", handlerRealmFile(app)) - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}", handlerRealmMain(app)) - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}:{querystr:.*}", handlerRealmRender(app)) - app.Router.Handle("/p/{filepath:.*}", handlerPackageFile(app)) - - // other - app.Router.Handle("/faucet", handlerFaucet(app)) - app.Router.Handle("/static/{path:.+}", handlerStaticFile(app)) - app.Router.Handle("/favicon.ico", handlerFavicon(app)) - // api - app.Router.Handle("/status.json", handlerStatusJSON(app)) + logger := log.NewTMLogger(os.Stdout) + logger.SetLevel(log.LevelDebug) - app.Router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := r.RequestURI - handleNotFound(app, path, w, r) - }) - return app -} - -func main() { - flag.Parse() - fmt.Printf("Running on http://%s\n", flags.BindAddr) + logger.Debug("", "Running", "http://"+cfg.BindAddr) server := &http.Server{ - Addr: flags.BindAddr, + Addr: cfg.BindAddr, ReadHeaderTimeout: 60 * time.Second, - Handler: makeApp().Router, + Handler: gnoweb.MakeApp(logger, cfg).Router, } if err := server.ListenAndServe(); err != nil { - fmt.Fprintf(os.Stderr, "HTTP server stopped with error: %+v\n", err) - } -} - -// handlerRealmAlias is used to render official pages from realms. -// url is intended to be shorter. -// UX is intended to be more minimalistic. -// A link to the realm realm is added. -func handlerRealmAlias(app gotuna.App, rlmpath string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rlmfullpath := "gno.land" + rlmpath - querystr := "" // XXX: "?gnoweb-alias=1" - parts := strings.Split(rlmpath, ":") - switch len(parts) { - case 1: // continue - case 2: // r/realm:querystr - rlmfullpath = "gno.land" + parts[0] - querystr = parts[1] + querystr - default: - panic("should not happen") - } - rlmname := strings.TrimPrefix(rlmfullpath, "gno.land/r/") - qpath := "vm/qrender" - data := []byte(fmt.Sprintf("%s\n%s", rlmfullpath, querystr)) - res, err := makeRequest(qpath, data) - if err != nil { - writeError(w, fmt.Errorf("gnoweb failed to query gnoland: %w", err)) - return - } - - queryParts := strings.Split(querystr, "/") - pathLinks := []pathLink{} - for i, part := range queryParts { - pathLinks = append(pathLinks, pathLink{ - URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"), - Text: part, - }) - } - - tmpl := app.NewTemplatingEngine() - // XXX: extract title from realm's output - // XXX: extract description from realm's output - tmpl.Set("RealmName", rlmname) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("Query", querystr) - tmpl.Set("PathLinks", pathLinks) - tmpl.Set("Contents", string(res.Data)) - tmpl.Set("Flags", flags) - tmpl.Set("IsAlias", true) - tmpl.Render(w, r, "realm_render.html", "funcs.html") - }) -} - -func handlerFaucet(app gotuna.App) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - app.NewTemplatingEngine(). - Set("Flags", flags). - Render(w, r, "faucet.html", "funcs.html") - }) -} - -func handlerStatusJSON(app gotuna.App) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ret struct { - Gnoland struct { - Connected bool `json:"connected"` - Error *string `json:"error,omitempty"` - Height *int64 `json:"height,omitempty"` - // processed txs - // active connections - - Version *string `json:"version,omitempty"` - // Uptime *float64 `json:"uptime-seconds,omitempty"` - // Goarch *string `json:"goarch,omitempty"` - // Goos *string `json:"goos,omitempty"` - // GoVersion *string `json:"go-version,omitempty"` - // NumCPU *int `json:"num_cpu,omitempty"` - } `json:"gnoland"` - Website struct { - // Version string `json:"version"` - Uptime float64 `json:"uptime-seconds"` - Goarch string `json:"goarch"` - Goos string `json:"goos"` - GoVersion string `json:"go-version"` - NumCPU int `json:"num_cpu"` - } `json:"website"` - } - ret.Website.Uptime = time.Since(startedAt).Seconds() - ret.Website.Goarch = runtime.GOARCH - ret.Website.Goos = runtime.GOOS - ret.Website.NumCPU = runtime.NumCPU() - ret.Website.GoVersion = runtime.Version() - - ret.Gnoland.Connected = true - res, err := makeRequest(".app/version", []byte{}) - if err != nil { - ret.Gnoland.Connected = false - errmsg := err.Error() - ret.Gnoland.Error = &errmsg - } else { - version := string(res.Value) - ret.Gnoland.Version = &version - ret.Gnoland.Height = &res.Height - } - - out, _ := json.MarshalIndent(ret, "", " ") - w.Header().Set("Content-Type", "application/json") - w.Write(out) - }) -} - -func handlerRedirect(app gotuna.App, to string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, to, http.StatusFound) - tmpl := app.NewTemplatingEngine() - tmpl.Set("To", to) - tmpl.Set("Flags", flags) - tmpl.Render(w, r, "redirect.html", "funcs.html") - }) -} - -func handlerRealmMain(app gotuna.App) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - rlmname := vars["rlmname"] - rlmpath := "gno.land/r/" + rlmname - query := r.URL.Query() - if query.Has("help") { - // Render function helper. - funcName := query.Get("__func") - qpath := "vm/qfuncs" - data := []byte(rlmpath) - res, err := makeRequest(qpath, data) - if err != nil { - writeError(w, err) - return - } - var fsigs vm.FunctionSignatures - amino.MustUnmarshalJSON(res.Data, &fsigs) - // Fill fsigs with query parameters. - for i := range fsigs { - fsig := &(fsigs[i]) - for j := range fsig.Params { - param := &(fsig.Params[j]) - value := query.Get(param.Name) - param.Value = value - } - } - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("FuncName", funcName) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("DirPath", pathOf(rlmpath)) - tmpl.Set("FunctionSignatures", fsigs) - tmpl.Set("Flags", flags) - tmpl.Render(w, r, "realm_help.html", "funcs.html") - } else { - // Ensure realm exists. TODO optimize. - qpath := qFileStr - data := []byte(rlmpath) - _, err := makeRequest(qpath, data) - if err != nil { - writeError(w, errors.New("error querying realm package")) - return - } - // Render blank query path, /r/REALM:. - handleRealmRender(app, w, r) - } - }) -} - -type pathLink struct { - URL string - Text string -} - -func handlerRealmRender(app gotuna.App) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRealmRender(app, w, r) - }) -} - -func handleRealmRender(app gotuna.App, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - rlmname := vars["rlmname"] - rlmpath := "gno.land/r/" + rlmname - querystr := vars["querystr"] - if r.URL.Path == "/r/"+rlmname+":" { - // Redirect to /r/REALM if querypath is empty. - http.Redirect(w, r, "/r/"+rlmname, http.StatusFound) - return - } - qpath := "vm/qrender" - data := []byte(fmt.Sprintf("%s\n%s", rlmpath, querystr)) - res, err := makeRequest(qpath, data) - if err != nil { - // XXX hack - if strings.Contains(err.Error(), "Render not declared") { - res = &abci.ResponseQuery{} - res.Data = []byte("realm package has no Render() function") - } else { - writeError(w, err) - return - } - } - // linkify querystr. - queryParts := strings.Split(querystr, "/") - pathLinks := []pathLink{} - for i, part := range queryParts { - pathLinks = append(pathLinks, pathLink{ - URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"), - Text: part, - }) - } - // Render template. - tmpl := app.NewTemplatingEngine() - // XXX: extract title from realm's output - // XXX: extract description from realm's output - tmpl.Set("RealmName", rlmname) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("Query", querystr) - tmpl.Set("PathLinks", pathLinks) - tmpl.Set("Contents", string(res.Data)) - tmpl.Set("Flags", flags) - tmpl.Render(w, r, "realm_render.html", "funcs.html") -} - -func handlerRealmFile(app gotuna.App) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - diruri := "gno.land/r/" + vars["rlmname"] - filename := vars["filename"] - renderPackageFile(app, w, r, diruri, filename) - }) -} - -func handlerPackageFile(app gotuna.App) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - pkgpath := "gno.land/p/" + vars["filepath"] - diruri, filename := std.SplitFilepath(pkgpath) - if filename == "" && diruri == pkgpath { - // redirect to diruri + "/" - http.Redirect(w, r, "/p/"+vars["filepath"]+"/", http.StatusFound) - return - } - renderPackageFile(app, w, r, diruri, filename) - }) -} - -func renderPackageFile(app gotuna.App, w http.ResponseWriter, r *http.Request, diruri string, filename string) { - if filename == "" { - // Request is for a folder. - qpath := qFileStr - data := []byte(diruri) - res, err := makeRequest(qpath, data) - if err != nil { - writeError(w, err) - return - } - files := strings.Split(string(res.Data), "\n") - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("Files", files) - tmpl.Set("Flags", flags) - tmpl.Render(w, r, "package_dir.html", "funcs.html") - } else { - // Request is for a file. - filepath := diruri + "/" + filename - qpath := qFileStr - data := []byte(filepath) - res, err := makeRequest(qpath, data) - if err != nil { - writeError(w, err) - return - } - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("FileName", filename) - tmpl.Set("FileContents", string(res.Data)) - tmpl.Set("Flags", flags) - tmpl.Render(w, r, "package_file.html", "funcs.html") - } -} - -func makeRequest(qpath string, data []byte) (res *abci.ResponseQuery, err error) { - opts2 := client.ABCIQueryOptions{ - // Height: height, XXX - // Prove: false, XXX - } - remote := flags.RemoteAddr - cli := client.NewHTTP(remote, "/websocket") - qres, err := cli.ABCIQueryWithOptions( - qpath, data, opts2) - if err != nil { - return nil, err - } - if qres.Response.Error != nil { - fmt.Printf("Log: %s\n", - qres.Response.Log) - return nil, qres.Response.Error - } - return &qres.Response, nil -} - -func handlerStaticFile(app gotuna.App) http.Handler { - fs := http.FS(app.Static) - fileapp := http.StripPrefix("/static", http.FileServer(fs)) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - fpath := filepath.Clean(vars["path"]) - f, err := fs.Open(fpath) - if os.IsNotExist(err) { - handleNotFound(app, fpath, w, r) - return - } - stat, err := f.Stat() - if err != nil || stat.IsDir() { - handleNotFound(app, fpath, w, r) - return - } - - // TODO: ModTime doesn't work for embed? - // w.Header().Set("ETag", fmt.Sprintf("%x", stat.ModTime().UnixNano())) - // w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s", "31536000")) - fileapp.ServeHTTP(w, r) - }) -} - -func handlerFavicon(app gotuna.App) http.Handler { - fs := http.FS(app.Static) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fpath := "img/favicon.ico" - f, err := fs.Open(fpath) - if os.IsNotExist(err) { - handleNotFound(app, fpath, w, r) - return - } - w.Header().Set("Content-Type", "image/x-icon") - w.Header().Set("Cache-Control", "public, max-age=604800") // 7d - io.Copy(w, f) - }) -} - -func handleNotFound(app gotuna.App, path string, w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - app.NewTemplatingEngine(). - Set("title", "Not found"). - Set("path", path). - Set("Flags", flags). - Render(w, r, "404.html", "funcs.html") -} - -func writeError(w http.ResponseWriter, err error) { - // XXX: writeError should return an error page template. - w.WriteHeader(500) - - fmt.Println("main", err.Error()) - - if details := errors.Unwrap(err); details != nil { - fmt.Println("details", details.Error()) - } - - w.Write([]byte(err.Error())) -} - -func pathOf(diruri string) string { - parts := strings.Split(diruri, "/") - if parts[0] == "gno.land" { - return "/" + strings.Join(parts[1:], "/") - } else { - panic(fmt.Sprintf("invalid dir-URI %q", diruri)) + logger.Error("HTTP server stopped", " error:", err) } } diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index e809103469d..8f2fa0debf6 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -105,6 +105,9 @@ func LoadPackagesFromDir(dir string, creator bft.Address, fee std.Fee, deposit s for _, pkg := range nonDraftPkgs { // Open files in directory as MemPackage. memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + if err := memPkg.Validate(); err != nil { + return nil, fmt.Errorf("invalid package: %w", err) + } // Create transaction tx := std.Tx{ diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 0edef304281..bb6dae0085f 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -2,6 +2,7 @@ package gnoland import ( "fmt" + "sync" "time" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -12,6 +13,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/p2p" "github.com/gnolang/gno/tm2/pkg/std" @@ -51,7 +53,9 @@ func NewDefaultGenesisConfig(pk crypto.PubKey, chainid string) *bft.GenesisDoc { } func NewDefaultTMConfig(rootdir string) *tmcfg.Config { - return tmcfg.DefaultConfig().SetRootDir(rootdir) + // We use `TestConfig` here otherwise ChainID will be empty, and + // there is no other way to update it than using a config file + return tmcfg.TestConfig().SetRootDir(rootdir) } // NewInMemoryNodeConfig creates a default configuration for an in-memory node. @@ -145,3 +149,29 @@ func NewInMemoryNode(logger log.Logger, cfg *InMemoryNodeConfig) (*node.Node, er logger, ) } + +// GetNodeReadiness waits until the node is ready, signaling via the EventNewBlock event. +// XXX: This should be replace by https://github.com/gnolang/gno/pull/1216 +func GetNodeReadiness(n *node.Node) <-chan struct{} { + const listenerID = "first_block_listener" + + var once sync.Once + + nb := make(chan struct{}) + ready := func() { + close(nb) + n.EventSwitch().RemoveListener(listenerID) + } + + n.EventSwitch().AddListener(listenerID, func(ev events.Event) { + if _, ok := ev.(bft.EventNewBlock); ok { + once.Do(ready) + } + }) + + if n.BlockStore().Height() > 0 { + once.Do(ready) + } + + return nb +} diff --git a/gno.land/pkg/gnoweb/gnoweb.go b/gno.land/pkg/gnoweb/gnoweb.go new file mode 100644 index 00000000000..b09ce6e3487 --- /dev/null +++ b/gno.land/pkg/gnoweb/gnoweb.go @@ -0,0 +1,509 @@ +package gnoweb + +import ( + "embed" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gorilla/mux" + "github.com/gotuna/gotuna" + + // for static files + "github.com/gnolang/gno/gno.land/pkg/gnoweb/static" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types + // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status) +) + +const ( + qFileStr = "vm/qfile" +) + +//go:embed views/* +var defaultViewsFiles embed.FS + +type Config struct { + RemoteAddr string + CaptchaSite string + FaucetURL string + ViewsDir string + HelpChainID string + HelpRemote string + WithAnalytics bool + + BindAddr string +} + +func NewDefaultConfig() Config { + return Config{ + RemoteAddr: "127.0.0.1:26657", + CaptchaSite: "", + FaucetURL: "http://localhost:5050", + ViewsDir: "", + HelpChainID: "dev", + HelpRemote: "127.0.0.1:26657", + WithAnalytics: false, + } +} + +func MakeApp(logger log.Logger, cfg Config) gotuna.App { + var viewFiles fs.FS + + // Get specific views directory if specified + if cfg.ViewsDir != "" { + viewFiles = os.DirFS(cfg.ViewsDir) + } else { + // Get embed views + var err error + viewFiles, err = fs.Sub(defaultViewsFiles, "views") + if err != nil { + panic("unable to get views directory from embed fs: " + err.Error()) + } + } + + app := gotuna.App{ + ViewFiles: viewFiles, + Router: gotuna.NewMuxRouter(), + 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", + "/game-of-realms": "/r/gnoland/pages:p/gor", // XXX: replace with gor realm + "/events": "/r/gnoland/pages:p/events", // XXX: replace with events realm + } + 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", + } + for from, to := range redirects { + app.Router.Handle(from, handlerRedirect(logger, app, &cfg, to)) + } + // realm routes + // NOTE: see rePathPart. + app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}/{filename:(?:.*\\.(?:gno|md|txt)$)?}", handlerRealmFile(logger, app, &cfg)) + app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}", handlerRealmMain(logger, app, &cfg)) + app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}:{querystr:.*}", handlerRealmRender(logger, app, &cfg)) + app.Router.Handle("/p/{filepath:.*}", handlerPackageFile(logger, app, &cfg)) + + // other + app.Router.Handle("/faucet", handlerFaucet(logger, app, &cfg)) + app.Router.Handle("/static/{path:.+}", handlerStaticFile(logger, app, &cfg)) + app.Router.Handle("/favicon.ico", handlerFavicon(logger, app, &cfg)) + + // api + app.Router.Handle("/status.json", handlerStatusJSON(logger, app, &cfg)) + + app.Router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.RequestURI + handleNotFound(app, &cfg, path, w, r) + }) + return app +} + +// handlerRealmAlias is used to render official pages from realms. +// url is intended to be shorter. +// UX is intended to be more minimalistic. +// A link to the realm realm is added. +func handlerRealmAlias(logger log.Logger, app gotuna.App, cfg *Config, rlmpath string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rlmfullpath := "gno.land" + rlmpath + querystr := "" // XXX: "?gnoweb-alias=1" + parts := strings.Split(rlmpath, ":") + switch len(parts) { + case 1: // continue + case 2: // r/realm:querystr + rlmfullpath = "gno.land" + parts[0] + querystr = parts[1] + querystr + default: + panic("should not happen") + } + rlmname := strings.TrimPrefix(rlmfullpath, "gno.land/r/") + qpath := "vm/qrender" + data := []byte(fmt.Sprintf("%s\n%s", rlmfullpath, querystr)) + res, err := makeRequest(logger, cfg, qpath, data) + if err != nil { + writeError(logger, w, fmt.Errorf("gnoweb failed to query gnoland: %w", err)) + return + } + + queryParts := strings.Split(querystr, "/") + pathLinks := []pathLink{} + for i, part := range queryParts { + pathLinks = append(pathLinks, pathLink{ + URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"), + Text: part, + }) + } + + tmpl := app.NewTemplatingEngine() + // XXX: extract title from realm's output + // XXX: extract description from realm's output + tmpl.Set("RealmName", rlmname) + tmpl.Set("RealmPath", rlmpath) + tmpl.Set("Query", querystr) + tmpl.Set("PathLinks", pathLinks) + tmpl.Set("Contents", string(res.Data)) + tmpl.Set("Config", cfg) + tmpl.Set("IsAlias", true) + tmpl.Render(w, r, "realm_render.html", "funcs.html") + }) +} + +func handlerFaucet(logger log.Logger, app gotuna.App, cfg *Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + app.NewTemplatingEngine(). + Set("Config", cfg). + Render(w, r, "faucet.html", "funcs.html") + }) +} + +func handlerStatusJSON(logger log.Logger, app gotuna.App, cfg *Config) http.Handler { + startedAt := time.Now() + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var ret struct { + Gnoland struct { + Connected bool `json:"connected"` + Error *string `json:"error,omitempty"` + Height *int64 `json:"height,omitempty"` + // processed txs + // active connections + + Version *string `json:"version,omitempty"` + // Uptime *float64 `json:"uptime-seconds,omitempty"` + // Goarch *string `json:"goarch,omitempty"` + // Goos *string `json:"goos,omitempty"` + // GoVersion *string `json:"go-version,omitempty"` + // NumCPU *int `json:"num_cpu,omitempty"` + } `json:"gnoland"` + Website struct { + // Version string `json:"version"` + Uptime float64 `json:"uptime-seconds"` + Goarch string `json:"goarch"` + Goos string `json:"goos"` + GoVersion string `json:"go-version"` + NumCPU int `json:"num_cpu"` + } `json:"website"` + } + ret.Website.Uptime = time.Since(startedAt).Seconds() + ret.Website.Goarch = runtime.GOARCH + ret.Website.Goos = runtime.GOOS + ret.Website.NumCPU = runtime.NumCPU() + ret.Website.GoVersion = runtime.Version() + + ret.Gnoland.Connected = true + res, err := makeRequest(logger, cfg, ".app/version", []byte{}) + if err != nil { + ret.Gnoland.Connected = false + errmsg := err.Error() + ret.Gnoland.Error = &errmsg + } else { + version := string(res.Value) + ret.Gnoland.Version = &version + ret.Gnoland.Height = &res.Height + } + + out, _ := json.MarshalIndent(ret, "", " ") + w.Header().Set("Content-Type", "application/json") + w.Write(out) + }) +} + +func handlerRedirect(logger log.Logger, app gotuna.App, cfg *Config, to string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, to, http.StatusFound) + tmpl := app.NewTemplatingEngine() + tmpl.Set("To", to) + tmpl.Set("Config", cfg) + tmpl.Render(w, r, "redirect.html", "funcs.html") + }) +} + +func handlerRealmMain(logger log.Logger, app gotuna.App, cfg *Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + rlmname := vars["rlmname"] + rlmpath := "gno.land/r/" + rlmname + query := r.URL.Query() + + logger.Info("handling", "name", rlmname, "path", rlmpath) + if query.Has("help") { + // Render function helper. + funcName := query.Get("__func") + qpath := "vm/qfuncs" + data := []byte(rlmpath) + res, err := makeRequest(logger, cfg, qpath, data) + if err != nil { + writeError(logger, w, fmt.Errorf("request failed: %w", err)) + return + } + var fsigs vm.FunctionSignatures + amino.MustUnmarshalJSON(res.Data, &fsigs) + // Fill fsigs with query parameters. + for i := range fsigs { + fsig := &(fsigs[i]) + for j := range fsig.Params { + param := &(fsig.Params[j]) + value := query.Get(param.Name) + param.Value = value + } + } + // Render template. + tmpl := app.NewTemplatingEngine() + tmpl.Set("FuncName", funcName) + tmpl.Set("RealmPath", rlmpath) + tmpl.Set("DirPath", pathOf(rlmpath)) + tmpl.Set("FunctionSignatures", fsigs) + tmpl.Set("Config", cfg) + tmpl.Render(w, r, "realm_help.html", "funcs.html") + } else { + // Ensure realm exists. TODO optimize. + qpath := qFileStr + data := []byte(rlmpath) + _, err := makeRequest(logger, cfg, qpath, data) + if err != nil { + writeError(logger, w, errors.New("error querying realm package")) + return + } + // Render blank query path, /r/REALM:. + handleRealmRender(logger, app, cfg, w, r) + } + }) +} + +type pathLink struct { + URL string + Text string +} + +func handlerRealmRender(logger log.Logger, app gotuna.App, cfg *Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handleRealmRender(logger, app, cfg, w, r) + }) +} + +func handleRealmRender(logger log.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + rlmname := vars["rlmname"] + rlmpath := "gno.land/r/" + rlmname + querystr := vars["querystr"] + if r.URL.Path == "/r/"+rlmname+":" { + // Redirect to /r/REALM if querypath is empty. + http.Redirect(w, r, "/r/"+rlmname, http.StatusFound) + return + } + qpath := "vm/qrender" + data := []byte(fmt.Sprintf("%s\n%s", rlmpath, querystr)) + res, err := makeRequest(logger, cfg, qpath, data) + if err != nil { + // XXX hack + if strings.Contains(err.Error(), "Render not declared") { + res = &abci.ResponseQuery{} + res.Data = []byte("realm package has no Render() function") + } else { + writeError(logger, w, err) + return + } + } + // linkify querystr. + queryParts := strings.Split(querystr, "/") + pathLinks := []pathLink{} + for i, part := range queryParts { + pathLinks = append(pathLinks, pathLink{ + URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"), + Text: part, + }) + } + // Render template. + tmpl := app.NewTemplatingEngine() + // XXX: extract title from realm's output + // XXX: extract description from realm's output + tmpl.Set("RealmName", rlmname) + tmpl.Set("RealmPath", rlmpath) + tmpl.Set("Query", querystr) + tmpl.Set("PathLinks", pathLinks) + tmpl.Set("Contents", string(res.Data)) + tmpl.Set("Config", cfg) + tmpl.Render(w, r, "realm_render.html", "funcs.html") +} + +func handlerRealmFile(logger log.Logger, app gotuna.App, cfg *Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + diruri := "gno.land/r/" + vars["rlmname"] + filename := vars["filename"] + renderPackageFile(logger, app, cfg, w, r, diruri, filename) + }) +} + +func handlerPackageFile(logger log.Logger, app gotuna.App, cfg *Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + pkgpath := "gno.land/p/" + vars["filepath"] + diruri, filename := std.SplitFilepath(pkgpath) + if filename == "" && diruri == pkgpath { + // redirect to diruri + "/" + http.Redirect(w, r, "/p/"+vars["filepath"]+"/", http.StatusFound) + return + } + renderPackageFile(logger, app, cfg, w, r, diruri, filename) + }) +} + +func renderPackageFile(logger log.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request, diruri string, filename string) { + if filename == "" { + // Request is for a folder. + qpath := qFileStr + data := []byte(diruri) + res, err := makeRequest(logger, cfg, qpath, data) + if err != nil { + writeError(logger, w, err) + return + } + files := strings.Split(string(res.Data), "\n") + // Render template. + tmpl := app.NewTemplatingEngine() + tmpl.Set("DirURI", diruri) + tmpl.Set("DirPath", pathOf(diruri)) + tmpl.Set("Files", files) + tmpl.Set("Config", cfg) + tmpl.Render(w, r, "package_dir.html", "funcs.html") + } else { + // Request is for a file. + filepath := diruri + "/" + filename + qpath := qFileStr + data := []byte(filepath) + res, err := makeRequest(logger, cfg, qpath, data) + if err != nil { + writeError(logger, w, err) + return + } + // Render template. + tmpl := app.NewTemplatingEngine() + tmpl.Set("DirURI", diruri) + tmpl.Set("DirPath", pathOf(diruri)) + tmpl.Set("FileName", filename) + tmpl.Set("FileContents", string(res.Data)) + tmpl.Set("Config", cfg) + tmpl.Render(w, r, "package_file.html", "funcs.html") + } +} + +func makeRequest(log log.Logger, cfg *Config, qpath string, data []byte) (res *abci.ResponseQuery, err error) { + opts2 := client.ABCIQueryOptions{ + // Height: height, XXX + // Prove: false, XXX + } + remote := cfg.RemoteAddr + cli := client.NewHTTP(remote, "/websocket") + qres, err := cli.ABCIQueryWithOptions( + qpath, data, opts2) + if err != nil { + log.Error("request error", "path", qpath, "error", err) + return nil, fmt.Errorf("unable to query path %q: %w", qpath, err) + } + if qres.Response.Error != nil { + log.Error("response error", "path", qpath, "log", qres.Response.Log) + return nil, qres.Response.Error + } + return &qres.Response, nil +} + +func handlerStaticFile(logger log.Logger, app gotuna.App, cfg *Config) http.Handler { + fs := http.FS(app.Static) + fileapp := http.StripPrefix("/static", http.FileServer(fs)) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + fpath := filepath.Clean(vars["path"]) + f, err := fs.Open(fpath) + if os.IsNotExist(err) { + handleNotFound(app, cfg, fpath, w, r) + return + } + stat, err := f.Stat() + if err != nil || stat.IsDir() { + handleNotFound(app, cfg, fpath, w, r) + return + } + + // TODO: ModTime doesn't work for embed? + // w.Header().Set("ETag", fmt.Sprintf("%x", stat.ModTime().UnixNano())) + // w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s", "31536000")) + fileapp.ServeHTTP(w, r) + }) +} + +func handlerFavicon(logger log.Logger, app gotuna.App, cfg *Config) http.Handler { + fs := http.FS(app.Static) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fpath := "img/favicon.ico" + f, err := fs.Open(fpath) + if os.IsNotExist(err) { + handleNotFound(app, cfg, fpath, w, r) + return + } + w.Header().Set("Content-Type", "image/x-icon") + w.Header().Set("Cache-Control", "public, max-age=604800") // 7d + io.Copy(w, f) + }) +} + +func handleNotFound(app gotuna.App, cfg *Config, path string, w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + app.NewTemplatingEngine(). + Set("title", "Not found"). + Set("path", path). + Set("Config", cfg). + Render(w, r, "404.html", "funcs.html") +} + +func writeError(logger log.Logger, w http.ResponseWriter, err error) { + if details := errors.Unwrap(err); details != nil { + logger.Error("handler", "error", err, "details", details) + } else { + logger.Error("handler", "error:", err) + } + + // XXX: writeError should return an error page template. + w.WriteHeader(500) + w.Write([]byte(err.Error())) +} + +func pathOf(diruri string) string { + parts := strings.Split(diruri, "/") + if parts[0] == "gno.land" { + return "/" + strings.Join(parts[1:], "/") + } else { + panic(fmt.Sprintf("invalid dir-URI %q", diruri)) + } +} diff --git a/gno.land/cmd/gnoweb/main_test.go b/gno.land/pkg/gnoweb/gnoweb_test.go similarity index 88% rename from gno.land/cmd/gnoweb/main_test.go rename to gno.land/pkg/gnoweb/gnoweb_test.go index 2bec0a9ac37..61f7244141e 100644 --- a/gno.land/cmd/gnoweb/main_test.go +++ b/gno.land/pkg/gnoweb/gnoweb_test.go @@ -1,4 +1,4 @@ -package main +package gnoweb import ( "fmt" @@ -48,14 +48,14 @@ func TestRoutes(t *testing.T) { node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) defer node.Stop() + cfg := NewDefaultConfig() + + logger := log.TestingLogger() + // set the `remoteAddr` of the client to the listening address of the // node, which is randomly assigned. - flags.RemoteAddr = remoteAddr - flags.HelpChainID = "dev" - flags.CaptchaSite = "" - flags.ViewsDir = "../../cmd/gnoweb/views" - flags.WithAnalytics = false - app := makeApp() + cfg.RemoteAddr = remoteAddr + app := MakeApp(logger, cfg) for _, r := range routes { t.Run(fmt.Sprintf("test route %s", r.route), func(t *testing.T) { @@ -63,6 +63,7 @@ func TestRoutes(t *testing.T) { response := httptest.NewRecorder() app.Router.ServeHTTP(response, request) assert.Equal(t, r.status, response.Code) + assert.Contains(t, response.Body.String(), r.substring) // println(response.Body.String()) }) @@ -96,13 +97,17 @@ func TestAnalytics(t *testing.T) { node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) defer node.Stop() - flags.ViewsDir = "../../cmd/gnoweb/views" + cfg := NewDefaultConfig() + cfg.RemoteAddr = remoteAddr + + logger := log.TestingLogger() + t.Run("with", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { - flags.RemoteAddr = remoteAddr - flags.WithAnalytics = true - app := makeApp() + ccfg := cfg // clone config + ccfg.WithAnalytics = true + app := MakeApp(logger, ccfg) request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() app.Router.ServeHTTP(response, request) @@ -113,9 +118,9 @@ func TestAnalytics(t *testing.T) { t.Run("without", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { - flags.RemoteAddr = remoteAddr - flags.WithAnalytics = false - app := makeApp() + ccfg := cfg // clone config + ccfg.WithAnalytics = false + app := MakeApp(logger, ccfg) request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() app.Router.ServeHTTP(response, request) diff --git a/gno.land/cmd/gnoweb/static/css/app.css b/gno.land/pkg/gnoweb/static/css/app.css similarity index 100% rename from gno.land/cmd/gnoweb/static/css/app.css rename to gno.land/pkg/gnoweb/static/css/app.css diff --git a/gno.land/cmd/gnoweb/static/css/normalize.css b/gno.land/pkg/gnoweb/static/css/normalize.css similarity index 100% rename from gno.land/cmd/gnoweb/static/css/normalize.css rename to gno.land/pkg/gnoweb/static/css/normalize.css diff --git a/gno.land/cmd/gnoweb/static/font/README.md b/gno.land/pkg/gnoweb/static/font/README.md similarity index 100% rename from gno.land/cmd/gnoweb/static/font/README.md rename to gno.land/pkg/gnoweb/static/font/README.md diff --git a/gno.land/cmd/gnoweb/static/font/roboto/LICENSE.txt b/gno.land/pkg/gnoweb/static/font/roboto/LICENSE.txt similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/LICENSE.txt rename to gno.land/pkg/gnoweb/static/font/roboto/LICENSE.txt diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Bold.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Bold.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Bold.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Bold.woff diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-BoldItalic.woff diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Italic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Italic.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Italic.woff diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Light.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Light.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Light.woff diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-LightItalic.woff diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Medium.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Medium.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Medium.woff diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-MediumItalic.woff diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Regular.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Regular.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Regular.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Regular.woff diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Thin.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-Thin.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-Thin.woff diff --git a/gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff b/gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff similarity index 100% rename from gno.land/cmd/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff rename to gno.land/pkg/gnoweb/static/font/roboto/RobotoMono-ThinItalic.woff diff --git a/gno.land/cmd/gnoweb/static/img/favicon.ico b/gno.land/pkg/gnoweb/static/img/favicon.ico similarity index 100% rename from gno.land/cmd/gnoweb/static/img/favicon.ico rename to gno.land/pkg/gnoweb/static/img/favicon.ico diff --git a/gno.land/cmd/gnoweb/static/img/github-mark-32px.png b/gno.land/pkg/gnoweb/static/img/github-mark-32px.png similarity index 100% rename from gno.land/cmd/gnoweb/static/img/github-mark-32px.png rename to gno.land/pkg/gnoweb/static/img/github-mark-32px.png diff --git a/gno.land/cmd/gnoweb/static/img/github-mark-64px.png b/gno.land/pkg/gnoweb/static/img/github-mark-64px.png similarity index 100% rename from gno.land/cmd/gnoweb/static/img/github-mark-64px.png rename to gno.land/pkg/gnoweb/static/img/github-mark-64px.png diff --git a/gno.land/cmd/gnoweb/static/img/ico-discord.svg b/gno.land/pkg/gnoweb/static/img/ico-discord.svg similarity index 100% rename from gno.land/cmd/gnoweb/static/img/ico-discord.svg rename to gno.land/pkg/gnoweb/static/img/ico-discord.svg diff --git a/gno.land/cmd/gnoweb/static/img/ico-email.svg b/gno.land/pkg/gnoweb/static/img/ico-email.svg similarity index 100% rename from gno.land/cmd/gnoweb/static/img/ico-email.svg rename to gno.land/pkg/gnoweb/static/img/ico-email.svg diff --git a/gno.land/cmd/gnoweb/static/img/ico-telegram.svg b/gno.land/pkg/gnoweb/static/img/ico-telegram.svg similarity index 100% rename from gno.land/cmd/gnoweb/static/img/ico-telegram.svg rename to gno.land/pkg/gnoweb/static/img/ico-telegram.svg diff --git a/gno.land/cmd/gnoweb/static/img/ico-twitter.svg b/gno.land/pkg/gnoweb/static/img/ico-twitter.svg similarity index 100% rename from gno.land/cmd/gnoweb/static/img/ico-twitter.svg rename to gno.land/pkg/gnoweb/static/img/ico-twitter.svg diff --git a/gno.land/cmd/gnoweb/static/img/ico-youtube.svg b/gno.land/pkg/gnoweb/static/img/ico-youtube.svg similarity index 100% rename from gno.land/cmd/gnoweb/static/img/ico-youtube.svg rename to gno.land/pkg/gnoweb/static/img/ico-youtube.svg diff --git a/gno.land/cmd/gnoweb/static/img/list-alt.png b/gno.land/pkg/gnoweb/static/img/list-alt.png similarity index 100% rename from gno.land/cmd/gnoweb/static/img/list-alt.png rename to gno.land/pkg/gnoweb/static/img/list-alt.png diff --git a/gno.land/cmd/gnoweb/static/img/list.png b/gno.land/pkg/gnoweb/static/img/list.png similarity index 100% rename from gno.land/cmd/gnoweb/static/img/list.png rename to gno.land/pkg/gnoweb/static/img/list.png diff --git a/gno.land/cmd/gnoweb/static/img/logo-square.png b/gno.land/pkg/gnoweb/static/img/logo-square.png similarity index 100% rename from gno.land/cmd/gnoweb/static/img/logo-square.png rename to gno.land/pkg/gnoweb/static/img/logo-square.png diff --git a/gno.land/cmd/gnoweb/static/img/logo-square.svg b/gno.land/pkg/gnoweb/static/img/logo-square.svg similarity index 100% rename from gno.land/cmd/gnoweb/static/img/logo-square.svg rename to gno.land/pkg/gnoweb/static/img/logo-square.svg diff --git a/gno.land/cmd/gnoweb/static/img/logo-v1.png b/gno.land/pkg/gnoweb/static/img/logo-v1.png similarity index 100% rename from gno.land/cmd/gnoweb/static/img/logo-v1.png rename to gno.land/pkg/gnoweb/static/img/logo-v1.png diff --git a/gno.land/cmd/gnoweb/static/img/logo.png b/gno.land/pkg/gnoweb/static/img/logo.png similarity index 100% rename from gno.land/cmd/gnoweb/static/img/logo.png rename to gno.land/pkg/gnoweb/static/img/logo.png diff --git a/gno.land/cmd/gnoweb/static/img/logo.svg b/gno.land/pkg/gnoweb/static/img/logo.svg similarity index 100% rename from gno.land/cmd/gnoweb/static/img/logo.svg rename to gno.land/pkg/gnoweb/static/img/logo.svg diff --git a/gno.land/cmd/gnoweb/static/invites.txt b/gno.land/pkg/gnoweb/static/invites.txt similarity index 100% rename from gno.land/cmd/gnoweb/static/invites.txt rename to gno.land/pkg/gnoweb/static/invites.txt diff --git a/gno.land/cmd/gnoweb/static/js/highlight.min.js b/gno.land/pkg/gnoweb/static/js/highlight.min.js similarity index 100% rename from gno.land/cmd/gnoweb/static/js/highlight.min.js rename to gno.land/pkg/gnoweb/static/js/highlight.min.js diff --git a/gno.land/cmd/gnoweb/static/js/marked.min.js b/gno.land/pkg/gnoweb/static/js/marked.min.js similarity index 100% rename from gno.land/cmd/gnoweb/static/js/marked.min.js rename to gno.land/pkg/gnoweb/static/js/marked.min.js diff --git a/gno.land/cmd/gnoweb/static/js/purify.min.js b/gno.land/pkg/gnoweb/static/js/purify.min.js similarity index 100% rename from gno.land/cmd/gnoweb/static/js/purify.min.js rename to gno.land/pkg/gnoweb/static/js/purify.min.js diff --git a/gno.land/cmd/gnoweb/static/js/realm_help.js b/gno.land/pkg/gnoweb/static/js/realm_help.js similarity index 100% rename from gno.land/cmd/gnoweb/static/js/realm_help.js rename to gno.land/pkg/gnoweb/static/js/realm_help.js diff --git a/gno.land/cmd/gnoweb/static/js/renderer.js b/gno.land/pkg/gnoweb/static/js/renderer.js similarity index 100% rename from gno.land/cmd/gnoweb/static/js/renderer.js rename to gno.land/pkg/gnoweb/static/js/renderer.js diff --git a/gno.land/cmd/gnoweb/static/js/umbrella.js b/gno.land/pkg/gnoweb/static/js/umbrella.js similarity index 100% rename from gno.land/cmd/gnoweb/static/js/umbrella.js rename to gno.land/pkg/gnoweb/static/js/umbrella.js diff --git a/gno.land/cmd/gnoweb/static/js/umbrella.min.js b/gno.land/pkg/gnoweb/static/js/umbrella.min.js similarity index 100% rename from gno.land/cmd/gnoweb/static/js/umbrella.min.js rename to gno.land/pkg/gnoweb/static/js/umbrella.min.js diff --git a/gno.land/cmd/gnoweb/static/static.go b/gno.land/pkg/gnoweb/static/static.go similarity index 100% rename from gno.land/cmd/gnoweb/static/static.go rename to gno.land/pkg/gnoweb/static/static.go diff --git a/gno.land/cmd/gnoweb/views/404.html b/gno.land/pkg/gnoweb/views/404.html similarity index 100% rename from gno.land/cmd/gnoweb/views/404.html rename to gno.land/pkg/gnoweb/views/404.html diff --git a/gno.land/cmd/gnoweb/views/faucet.html b/gno.land/pkg/gnoweb/views/faucet.html similarity index 100% rename from gno.land/cmd/gnoweb/views/faucet.html rename to gno.land/pkg/gnoweb/views/faucet.html diff --git a/gno.land/cmd/gnoweb/views/funcs.html b/gno.land/pkg/gnoweb/views/funcs.html similarity index 99% rename from gno.land/cmd/gnoweb/views/funcs.html rename to gno.land/pkg/gnoweb/views/funcs.html index c8d643ef655..391675755aa 100644 --- a/gno.land/cmd/gnoweb/views/funcs.html +++ b/gno.land/pkg/gnoweb/views/funcs.html @@ -155,7 +155,7 @@ {{- end -}} {{- define "analytics" -}} -{{- if .Data.Flags.WithAnalytics -}} +{{- if .Data.Config.WithAnalytics -}} diff --git a/gno.land/cmd/gnoweb/views/generic.html b/gno.land/pkg/gnoweb/views/generic.html similarity index 100% rename from gno.land/cmd/gnoweb/views/generic.html rename to gno.land/pkg/gnoweb/views/generic.html diff --git a/gno.land/cmd/gnoweb/views/package_dir.html b/gno.land/pkg/gnoweb/views/package_dir.html similarity index 100% rename from gno.land/cmd/gnoweb/views/package_dir.html rename to gno.land/pkg/gnoweb/views/package_dir.html diff --git a/gno.land/cmd/gnoweb/views/package_file.html b/gno.land/pkg/gnoweb/views/package_file.html similarity index 100% rename from gno.land/cmd/gnoweb/views/package_file.html rename to gno.land/pkg/gnoweb/views/package_file.html diff --git a/gno.land/cmd/gnoweb/views/realm_help.html b/gno.land/pkg/gnoweb/views/realm_help.html similarity index 100% rename from gno.land/cmd/gnoweb/views/realm_help.html rename to gno.land/pkg/gnoweb/views/realm_help.html diff --git a/gno.land/cmd/gnoweb/views/realm_render.html b/gno.land/pkg/gnoweb/views/realm_render.html similarity index 100% rename from gno.land/cmd/gnoweb/views/realm_render.html rename to gno.land/pkg/gnoweb/views/realm_render.html diff --git a/gno.land/cmd/gnoweb/views/redirect.html b/gno.land/pkg/gnoweb/views/redirect.html similarity index 100% rename from gno.land/cmd/gnoweb/views/redirect.html rename to gno.land/pkg/gnoweb/views/redirect.html diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 1ca7e11eb63..4705f4e3d73 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -2,7 +2,6 @@ package integration import ( "path/filepath" - "sync" "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" @@ -11,7 +10,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/node" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/std" "github.com/jaekwon/testify/require" @@ -33,7 +31,7 @@ func TestingInMemoryNode(t TestingTS, logger log.Logger, config *gnoland.InMemor require.NoError(t, err) select { - case <-waitForNodeReadiness(node): + case <-gnoland.GetNodeReadiness(node): case <-time.After(time.Second * 6): require.FailNow(t, "timeout while waiting for the node to start") } @@ -116,8 +114,7 @@ func LoadDefaultPackages(t TestingTS, creator bft.Address, gnoroot string) []std examplesDir := filepath.Join(gnoroot, "examples") defaultFee := std.NewFee(50000, std.MustParseCoin("1000000ugnot")) - defaultCreator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 - txs, err := gnoland.LoadPackagesFromDir(examplesDir, defaultCreator, defaultFee, nil) + txs, err := gnoland.LoadPackagesFromDir(examplesDir, creator, defaultFee, nil) require.NoError(t, err) return txs @@ -156,29 +153,3 @@ func DefaultTestingTMConfig(gnoroot string) *tmcfg.Config { tmconfig.P2P.ListenAddress = defaultListner return tmconfig } - -// waitForNodeReadiness waits until the node is ready, signaling via the EventNewBlock event. -// XXX: This should be replace by https://github.com/gnolang/gno/pull/1216 -func waitForNodeReadiness(n *node.Node) <-chan struct{} { - const listenerID = "first_block_listener" - - var once sync.Once - - nb := make(chan struct{}) - ready := func() { - close(nb) - n.EventSwitch().RemoveListener(listenerID) - } - - n.EventSwitch().AddListener(listenerID, func(ev events.Event) { - if _, ok := ev.(bft.EventNewBlock); ok { - once.Do(ready) - } - }) - - if n.BlockStore().Height() > 0 { - once.Do(ready) - } - - return nb -}