diff --git a/README.md b/README.md index 44dc0416..002d4d59 100644 --- a/README.md +++ b/README.md @@ -48,13 +48,18 @@ make install ```shell GITHUB_USERNAME=rollchains +# Available Features: +# * tokenfactory,globalfee,ibc-packetforward,ibc-ratelimit,cosmwasm,wasm-light-client,optimistic-execution,ignite-cli,block-explorer + spawn new rollchain \ --consensus=proof-of-authority `# proof-of-authority,proof-of-stake,interchain-security` \ --bech32=roll `# the prefix for addresses` \ --denom=uroll `# the coin denomination to create` \ --bin=rolld `# the name of the binary` \ ---disabled=cosmwasm,globalfee `# disable features. [tokenfactory,globalfee,ibc-packetforward,ibc-ratelimit,cosmwasm,wasm-light-client,ignite-cli]` \ +--disabled=cosmwasm,globalfee,block-explorer `# disable features.` \ --org=${GITHUB_USERNAME} `# the github username or organization to use for the module imports, optional` + + ``` > *NOTE:* `spawn` creates a ready to use repository complete with `git` and GitHub CI. It can be quickly pushed to a new repository getting you and your team up and running quickly. diff --git a/cmd/spawn/new_chain.go b/cmd/spawn/new_chain.go index 73b98cd7..3ba8c881 100644 --- a/cmd/spawn/new_chain.go +++ b/cmd/spawn/new_chain.go @@ -32,6 +32,7 @@ var ( {ID: "wasm-light-client", IsSelected: false, Details: "08 Wasm Light Client"}, {ID: "optimistic-execution", IsSelected: false, Details: "Pre-process blocks ahead of consensus request"}, {ID: "ignite-cli", IsSelected: false, Details: "Ignite-CLI Support"}, + {ID: "block-explorer", IsSelected: true, Details: "Ping Pub Explorer"}, }...) // parentDeps is a list of modules that are disabled if a parent module is disabled. diff --git a/simapp/Makefile b/simapp/Makefile index f7a11eea..f76ab310 100644 --- a/simapp/Makefile +++ b/simapp/Makefile @@ -309,6 +309,10 @@ sh-testnet: mod-tidy ### help ### ############################################################################### +.PHONY: explorer +explorer: + docker compose up + help: @echo "Usage: make " @echo "" diff --git a/simapp/README.md b/simapp/README.md index 3fa44399..bfcec73d 100644 --- a/simapp/README.md +++ b/simapp/README.md @@ -21,4 +21,8 @@ ## Testing - `go test ./... -v` *Unit test* -- `make ictest-*` *E2E testing* \ No newline at end of file +- `make ictest-*` *E2E testing* + +## Launch Block Explorer Locally + +[Reference Guide](./nginx/README.md) \ No newline at end of file diff --git a/simapp/docker-compose.yml b/simapp/docker-compose.yml new file mode 100644 index 00000000..c96bb2b1 --- /dev/null +++ b/simapp/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3' + +# Runs the explorer & reverse proxy +# +# NOTE: Must add the following to your /etc/hosts file: +# +# 127.0.0.1 api.localhost +# 127.0.0.1 rpc.localhost +# 127.0.0.1:5173 pingpub.localhost +# +# Then: +# docker compose up + +services: + nginx: + image: nginx + network_mode: "host" + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/nginx.conf + - ./nginx/nginx-selfsigned.crt:/etc/nginx/nginx-selfsigned.crt + - ./nginx/nginx-selfsigned.key:/etc/nginx/nginx-selfsigned.key + pingpub: + depends_on: + - nginx + image: pingpub:latest + network_mode: "host" + build: + context: pingpub + dockerfile: ./explorer/Dockerfile + + volumes: + - ./explorer/chains:/app/chains/ + ports: + - "80:80" + - "443:443" \ No newline at end of file diff --git a/simapp/embed.go b/simapp/embed.go index 96d55bc7..a88359c6 100644 --- a/simapp/embed.go +++ b/simapp/embed.go @@ -6,7 +6,7 @@ import ( // !IMPORTANT: interchaintest/ has its own `InterchainTest` embed.FS that will need to be iterated on. -//go:embed .github/* app/* chains/* cmd/* contrib/* scripts/* Makefile Dockerfile proto/*.* *.* +//go:embed .github/* app/* chains/* cmd/* contrib/* scripts/* Makefile Dockerfile nginx/* proto/*.* *.* var SimAppFS embed.FS // To embed the interchaintest/ directory, rename the go.mod file to `go.mod_` diff --git a/simapp/nginx/README.md b/simapp/nginx/README.md new file mode 100644 index 00000000..2bd9025f --- /dev/null +++ b/simapp/nginx/README.md @@ -0,0 +1,22 @@ +# Nginx Explorer + +To run the explorer locally, this nginx configuration is required to get local https to work. + +## Running + +Update your `/etc/hosts` file to include the following: + +``` +127.0.0.1 api.localhost +127.0.0.1 rpc.localhost +127.0.0.1 pingpub.localhost +``` + +Start the testnet with: `make sh-testnet` or the full IBC network with `make testnet` + +Then `docker compose up` to start the reverse proxy, explorer, and the RPC/REST API Services. + + +Visit: https://pingpub.localhost to view the explorer. + +> Attempting to view as a standard http:// instance will break the block explorer due to pesky CORS errors. diff --git a/simapp/nginx/nginx-selfsigned.crt b/simapp/nginx/nginx-selfsigned.crt new file mode 100644 index 00000000..4309c1bf --- /dev/null +++ b/simapp/nginx/nginx-selfsigned.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUVgGX1ixwRoR1zlI8hTotdUKYLv4wDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA3MjEyMTM4MDVaFw0yNDA4 +MjAyMTM4MDVaMEUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDPiqGL1t3RFFkhFJXRb1qO249wODwhUmMIYr3Kn4Tv +RzgZg8+6U95djp6j5ToOnZpW+HGk87y/QKdnc4j23rVLUhWjIOJNsdttB/iBY3bY +4kGpTByeM0/INE1ccjjE/+0e8Zmi/EpAGOtaQc8XWW4w7XUr7PtvXt8hlOSJWfd6 ++1gtiN/uHiAjWAudDyQw06ldhFyBBbIjTgF5jFVUFflPg7rB1RJFquDJGjaW3kDQ +Jz4BLlRhspgQqz1bzJTwqXivXp3nk9na2wesMcMDK0NSLBoqxqqTx06UZmlMm2/R +vRFg6F66/wqUhhxHvw2iLqBhoirhRiIL9IgTu388cE/DAgMBAAGjUzBRMB0GA1Ud +DgQWBBQD++PJjWJKDdNPv+t6aoMyCiF6TDAfBgNVHSMEGDAWgBQD++PJjWJKDdNP +v+t6aoMyCiF6TDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCn ++nuVpVpetc83BTOoJopqosaVZuk87NGutvMiua/dHosfwgVqz0PAmg3DZ47eVOeH +trgFW3XINXxPdV8RL9cLAq3sppmSMBHMxHDu54uJ/9nJcEX2zCXYlCeJgcLShLv8 +BB7M1VFPRiaPpAwrE/UgaVumeD+XWNVs6INGB0n74SZG1aRr7JWbm9HG02Hon/MG +5OpUfhpCL13P78x1KuHCMAxAK16Ifpb0Vk17RNqbrDx6yHnhcDOWOCH29RdMB7I9 +fwf62r70hGjhOavgaVKGcbiOIfQoGh8rLOOQUcHXdpbx7lKtjgAnytMIQ6Fx9wNI +WMRBwh/Vql5nhzioWSmB +-----END CERTIFICATE----- diff --git a/simapp/nginx/nginx-selfsigned.key b/simapp/nginx/nginx-selfsigned.key new file mode 100755 index 00000000..d39ecb23 --- /dev/null +++ b/simapp/nginx/nginx-selfsigned.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPiqGL1t3RFFkh +FJXRb1qO249wODwhUmMIYr3Kn4TvRzgZg8+6U95djp6j5ToOnZpW+HGk87y/QKdn +c4j23rVLUhWjIOJNsdttB/iBY3bY4kGpTByeM0/INE1ccjjE/+0e8Zmi/EpAGOta +Qc8XWW4w7XUr7PtvXt8hlOSJWfd6+1gtiN/uHiAjWAudDyQw06ldhFyBBbIjTgF5 +jFVUFflPg7rB1RJFquDJGjaW3kDQJz4BLlRhspgQqz1bzJTwqXivXp3nk9na2wes +McMDK0NSLBoqxqqTx06UZmlMm2/RvRFg6F66/wqUhhxHvw2iLqBhoirhRiIL9IgT +u388cE/DAgMBAAECggEALQBn3/0SsuvBGcmvZK7LCY/1LcWb0DPfkmlqst1dA09D +jFDHAaV+4XVz06D4MkQdO796UOSi6Ct6QRXNvI307KSbWXhfaa5noGAqk8+/7O4+ +g5mj2O/SXFxu690+jwTZYyzK/grLhNOCcNs1LuBu4sASeJhVusPtCQiSd2/hGDK0 +3AtN561z3QEAseV+yIVyAutAL0Xj+w8g1g0sKGkojypAwWnhX9I02xBhNFCyMkBp +/Tuts48QEomYbEI91wEa6PumD9EMtHV5JSiHJm59jbn7GPd38fqORB2Qhh580lg6 +zINAOHZb9BwXJYo5tRLZI/tKUQKj8PCactDM+hmQiQKBgQDuFB96a44R7xII8P6o +OE3ECHEsaaiywQ6FX3rVfC2AYySIXVIcrng//ITbwW8Tv26jAfCxkaTV+XN2UplH +tksSj3Ll9f9qCdeog9UJhT7TFR6uwSHTZ9ninhS0J5HEalkYwYo7i4uQaIrjK4Ll +4fLMqiGoFVU1g4REJlNiz/ecqwKBgQDfKgujoCL73akWqVKdEFNWKWlHaTr7Ihhb +wMAmSkK6L29+Ig+0Q+Cs5ndtZP8UFGTndVoi++yU2oXYdXwUcyS+cyzi7X9Zmp+F +xSa8zq5+oOGOfRAOtlp/Sf3/xJO6NxCT7uS0Q5u3BHuldU/Lx5wagA20uCldh5dO +Nx5zubfpSQKBgCvHMX6eVnJ/xo40Wm9uYwZgEwd6qlWsYFIwG3M0MV3BXU9h8Z5q +ipwhgAC00gsMkXiR+8N7J5ddFlk0mRDxuV5BWHxmvr+t7aUEEOF+Se4gnRK/Wsv3 +9b3RGbeC6y/16ko+FIAcid5VCuz47En/QVlXE3dH7PI5K9IoRf8OhNafAoGAKNaP +5LSUUlUA8WWw+Y8YQQc4/dly8qwNmxTN1PP3/AxcMc/X4dweDGXsavd1el41DOo7 +wXUqmR7YKYFuYGulyLhY+XoOuP4DvT4T1a9Y3VFhlWqrepXCP9LxiVGW2xfij7/C +2H4ay8YlPmUWYis4FN1kJLMi1rvOY4DQsMrGrgkCgYEAg/nASsYYpOUxEg8Z2X7r +PvDIPu6S51o74Qwukq4tXG0ALpNHQQJfh4kG2+Dy9gYYuSYHyyRw3I1P68JDQzHB +4qnRaVLQn5rk4++xD01P22juvT+XHqeiRAGl+8n/xqj/qdwoI4LEyhgXH5TpjpCw +gcH/cDxQQbmkHxLCQ3bzuJ8= +-----END PRIVATE KEY----- diff --git a/simapp/nginx/nginx.conf b/simapp/nginx/nginx.conf new file mode 100644 index 00000000..54056556 --- /dev/null +++ b/simapp/nginx/nginx.conf @@ -0,0 +1,53 @@ +server { + listen 80; + listen 443 ssl; + + server_name api.localhost; + ssl_certificate nginx-selfsigned.crt; + ssl_certificate_key nginx-selfsigned.key; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Max-Age 3600; + add_header Access-Control-Expose-Headers Content-Length; + + location / { + proxy_pass http://127.0.0.1:1317; + } +} + +server { + listen 80; + listen 443 ssl; + + server_name rpc.localhost; + ssl_certificate nginx-selfsigned.crt; + ssl_certificate_key nginx-selfsigned.key; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Max-Age 3600; + add_header Access-Control-Expose-Headers Content-Length; + + location / { + proxy_pass http://127.0.0.1:26657; + } +} + +server { + listen 80; + listen 443 ssl; + + server_name pingpub.localhost; + ssl_certificate nginx-selfsigned.crt; + ssl_certificate_key nginx-selfsigned.key; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Max-Age 3600; + add_header Access-Control-Expose-Headers Content-Length; + + location / { + proxy_pass http://127.0.0.1:5173; + } +} diff --git a/spawn/cfg.go b/spawn/cfg.go index 02f45269..f0b14974 100644 --- a/spawn/cfg.go +++ b/spawn/cfg.go @@ -207,6 +207,10 @@ func (cfg *NewChainConfig) CreateNewChain() error { cfg.GitInitNewProjectRepo() } + if !cfg.IsFeatureDisabled("block-explorer") { + cfg.NewPingPubExplorer() + } + return nil } diff --git a/spawn/explorer.go b/spawn/explorer.go new file mode 100644 index 00000000..8b41ebde --- /dev/null +++ b/spawn/explorer.go @@ -0,0 +1,130 @@ +package spawn + +import ( + "encoding/json" + "fmt" + "os" + "path" + "strings" +) + +type ( + ChainExplorerAsset struct { + Base string `json:"base"` + Symbol string `json:"symbol"` + Exponent string `json:"exponent"` + CoingeckoId string `json:"coingecko_id"` + Logo string `json:"logo"` + } + + Endpoint struct { + Provider string `json:"provider"` + Address string `json:"address"` + } + + ChainExplorer struct { + ChainName string `json:"chain_name"` + Api []Endpoint `json:"api"` + Rpc []Endpoint `json:"rpc"` + SdkVersion string `json:"sdk_version"` + CoinType string `json:"coin_type"` + MinTxFee string `json:"min_tx_fee"` + AddrPrefix string `json:"addr_prefix"` + Logo string `json:"logo"` + ThemeColor string `json:"theme_color"` + Assets []ChainExplorerAsset `json:"assets"` + } +) + +// hacky: pingpub does not have a docker file for some reason... +const dockerFile = `# docker build . -t pingpub:latest + +FROM node:20-alpine + +RUN apk add --no-cache yarn npm + +WORKDIR /app + +COPY . . + +# install node_modules to the image +RUN yarn --ignore-engines + +CMD [ "yarn", "--ignore-engines", "serve", "--host", "0.0.0.0" ]` + +func NewEndpoint(provider, address string) Endpoint { + return Endpoint{ + Provider: provider, + Address: address, + } +} + +func (cfg NewChainConfig) NewPingPubExplorer() error { + if err := os.Chdir(cfg.ProjectName); err != nil { + cfg.Logger.Error("chdir", "err", err) + } + if err := ExecCommand("git", "clone", "https://github.com/ping-pub/explorer.git"); err != nil { + cfg.Logger.Error("git clone", "err", err) + } + if err := os.Chdir(".."); err != nil { + cfg.Logger.Error("chdir", "err", err) + } + + mainnet := path.Join(cfg.ProjectName, "explorer", "chains", "mainnet") + cfg.clearDir(mainnet) + cfg.clearDir(path.Join(cfg.ProjectName, "explorer", "chains", "testnet")) + + // Create JSON config file for explorer + explorer := cfg.NewChainExplorerConfig() + bz, err := json.MarshalIndent(explorer, "", " ") + if err != nil { + cfg.Logger.Error("Error marshalling chain explorer config", "err", err) + } + + err = os.WriteFile(path.Join(mainnet, fmt.Sprintf("%s.json", cfg.ProjectName)), bz, 0644) + if err != nil { + cfg.Logger.Error("Error writing chain explorer config", "err", err) + } + + err = os.WriteFile(path.Join(cfg.ProjectName, "explorer", "Dockerfile"), []byte(dockerFile), 0644) + if err != nil { + cfg.Logger.Error("Error writing docker file", "err", err) + } + + return nil +} + +func (cfg NewChainConfig) NewChainExplorerConfig() ChainExplorer { + logo := "https://img.freepik.com/free-vector/letter-s-box-logo-design-template_474888-3345.jpg?size=338&ext=jpg&ga=GA1.1.2008272138.1721260800&semt=ais_user" + return ChainExplorer{ + ChainName: cfg.ProjectName, + Api: []Endpoint{NewEndpoint("api.localhost", "https://api.localhost")}, + Rpc: []Endpoint{NewEndpoint("rpc.localhost", "https://rpc.localhost")}, + SdkVersion: "0.50", + CoinType: "118", + MinTxFee: "800", + AddrPrefix: cfg.Bech32Prefix, + Logo: logo, + ThemeColor: "#001be7", + Assets: []ChainExplorerAsset{ + { + Base: cfg.Denom, + Symbol: strings.ToUpper(cfg.Denom), + Exponent: "6", + CoingeckoId: "", + Logo: logo, + }, + }, + } +} + +func (cfg NewChainConfig) clearDir(dirLoc ...string) { + dir := path.Join(dirLoc...) + + if err := os.RemoveAll(dir); err != nil { + cfg.Logger.Error("Error removing directory", "err", err) + } + if err := os.MkdirAll(dir, 0755); err != nil { + cfg.Logger.Error("Error creating directory", "err", err) + } +} diff --git a/spawn/file_content.go b/spawn/file_content.go index 7b6e84d3..4fad810e 100644 --- a/spawn/file_content.go +++ b/spawn/file_content.go @@ -88,9 +88,7 @@ func (fc *FileContent) DeleteFile(path string) { } func (fc *FileContent) DeleteDirectoryContents(path string) { - if fc.ContainsPath(path) && !AlreadyCheckedDeletion[path] { - AlreadyCheckedDeletion[path] = true - + if fc.ContainsPath(path) { fc.Logger.Debug("Deleting contents for", "path", path) fc.Contents = "" } diff --git a/spawn/remove_features.go b/spawn/remove_features.go index e8b308ce..e5f73cf6 100644 --- a/spawn/remove_features.go +++ b/spawn/remove_features.go @@ -22,6 +22,7 @@ var ( Ignite = "ignite" InterchainSecurity = "ics" OptimisticExecution = "optimistic-execution" + BlockExplorer = "block-explorer" appGo = path.Join("app", "app.go") appAnte = path.Join("app", "ante.go") @@ -60,6 +61,8 @@ func AliasName(name string) string { return IBCRateLimit case InterchainSecurity, "interchain-security": return InterchainSecurity + case BlockExplorer, "explorer", "pingpub": + return BlockExplorer default: panic(fmt.Sprintf("AliasName: unknown feature to remove: %s", name)) } @@ -97,6 +100,8 @@ func (fc *FileContent) RemoveDisabledFeatures(cfg *NewChainConfig) { fc.RemoveIgniteCLI() case OptimisticExecution: fc.RemoveOptimisticExecution() + case BlockExplorer: + fc.RemoveExplorer() default: panic(fmt.Sprintf("unknown feature to remove %s", name)) } @@ -259,6 +264,10 @@ func (fc *FileContent) RemoveOptimisticExecution() { fc.RemoveTaggedLines(OptimisticExecution, true) } +func (fc *FileContent) RemoveExplorer() { + fc.DeleteDirectoryContents("nginx") +} + func (fc *FileContent) RemoveInterchainSecurity() { text := "ics"