Dial /v1, the only prefix luxd serves

luxd pins baseURL="/v1" (node/server/http/server.go:33) and has never
served /ext, so both exported constructors — plugin/evm/client.NewClient
and warp.NewClient — built a path no node answers. They are the seam
every out-of-tree caller goes through: cli/pkg/utils/staking.go,
sdk/validatormanager/uptime.go and netrunner/api all hand them a bare
node URI, and staking.go's own SplitRPCURI already parsed /v1/bc/<id>/rpc.
Those callers were already on /v1; this package was the lagging outlier,
so fixing it here fixes all of them without touching one.

Also corrects the simulator's default websocket endpoint, the hardhat
default, and the docs — including api.testnet.lux.network, which does not
serve RPC at all. The testnet host is api.lux-test.network (chainId
0x17870). Prefix-only sweeps miss that class of error.

Tests pin the constructed path against an httptest server, with both an
alias and a blockchain ID, so a future prefix change fails loudly instead
of silently 404ing in production.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-27 19:07:09 -07:00
co-authored by Hanzo Dev
parent 1a5185588a
commit e8187ae5a5
20 changed files with 123 additions and 56 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ type client struct {
// NewClient returns a Client for interacting with EVM [chain]
func NewClient(uri, chain string) Client {
requestUri := fmt.Sprintf("%s/ext/bc/%s", uri, chain)
requestUri := fmt.Sprintf("%s/v1/bc/%s", uri, chain)
return NewClientWithURL(requestUri)
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package client
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// NewClient is the exported seam every out-of-tree caller goes through —
// cli, sdk and netrunner all hand it a bare node URI and let it build the
// path. luxd serves exactly one prefix, /v1 (node/server/http/server.go),
// so the prefix this function bakes in decides whether those callers reach
// the node at all.
func TestNewClientRequestsV1Paths(t *testing.T) {
seen := make(chan string, 4)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen <- r.URL.Path
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{}}`))
}))
defer srv.Close()
c := NewClient(srv.URL, "C")
if err := c.StartCPUProfiler(context.Background()); err != nil {
t.Fatalf("StartCPUProfiler: %v", err)
}
got := <-seen
if want := "/v1/bc/C/admin"; got != want {
t.Fatalf("admin endpoint = %q, want %q", got, want)
}
if strings.Contains(got, "/ext/") {
t.Fatalf("request still carries the legacy /ext prefix: %q", got)
}
if _, err := c.GetCurrentValidators(context.Background(), nil); err != nil {
t.Fatalf("GetCurrentValidators: %v", err)
}
if got, want := <-seen, "/v1/bc/C/validators"; got != want {
t.Fatalf("validators endpoint = %q, want %q", got, want)
}
}
// The chain segment must be interpolated, not hardcoded, so a blockchain ID
// works as well as an alias.
func TestNewClientUsesChainArgument(t *testing.T) {
seen := make(chan string, 1)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen <- r.URL.Path
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{}}`))
}))
defer srv.Close()
const chainID = "2T11SNJM9KYqSenoPb5yv6qahmj8HLefCjxt4gTD2iQWTHwdRu"
if err := NewClient(srv.URL, chainID).StartCPUProfiler(context.Background()); err != nil {
t.Fatalf("StartCPUProfiler: %v", err)
}
if got, want := <-seen, "/v1/bc/"+chainID+"/admin"; got != want {
t.Fatalf("endpoint = %q, want %q", got, want)
}
}
+6 -6
View File
@@ -32,7 +32,7 @@ curl -X POST --data '{
"latest"
],
"id": 1
}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/2ebCneCbwthjQ1rYT41nhd7M76Hc6YmosMAQrTFhBq8qeqh6tt/rpc
}' -H 'content-type:application/json;' 127.0.0.1:9650/v1/bc/2ebCneCbwthjQ1rYT41nhd7M76Hc6YmosMAQrTFhBq8qeqh6tt/rpc
```
**Example Response:**
@@ -78,7 +78,7 @@ curl -X POST --data '{
"id" :1,
"method" :"eth_getChainConfig",
"params" :[]
}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/Nvqcm33CX2XABS62iZsAcVUkavfnzp1Sc5k413wn5Nrf7Qjt7/rpc
}' -H 'content-type:application/json;' 127.0.0.1:9650/v1/bc/Nvqcm33CX2XABS62iZsAcVUkavfnzp1Sc5k413wn5Nrf7Qjt7/rpc
```
**Example Response:**
@@ -168,7 +168,7 @@ curl -X POST --data '{
"method": "eth_getActivePrecompilesAt",
"params": [],
"id": 1
}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/Nvqcm33CX2XABS62iZsAcVUkavfnzp1Sc5k413wn5Nrf7Qjt7/rpc
}' -H 'content-type:application/json;' 127.0.0.1:9650/v1/bc/Nvqcm33CX2XABS62iZsAcVUkavfnzp1Sc5k413wn5Nrf7Qjt7/rpc
```
**Example Response:**
@@ -216,7 +216,7 @@ curl -X POST --data '{
"method": "eth_getActiveRulesAt",
"params": [],
"id": 1
}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/Nvqcm33CX2XABS62iZsAcVUkavfnzp1Sc5k413wn5Nrf7Qjt7/rpc
}' -H 'content-type:application/json;' 127.0.0.1:9650/v1/bc/Nvqcm33CX2XABS62iZsAcVUkavfnzp1Sc5k413wn5Nrf7Qjt7/rpc
```
**Example Response:**
@@ -261,7 +261,7 @@ curl -X POST --data '{
This API retrieves the list of current validators for the Subnet/L1. It provides detailed information about each validator, including their ID, status, weight, connection, and uptime.
URL: `http://<server-uri>/ext/bc/<blockchainID>/validators`
URL: `http://<server-uri>/v1/bc/<blockchainID>/validators`
**Signature:**
@@ -281,7 +281,7 @@ curl -X POST --data '{
"nodeIDs": []
},
"id": 1
}' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/C49rHzk3vLr1w9Z8sY7scrZ69TU4WcD2pRS6ZyzaSn9xA2U9F/validators
}' -H 'content-type:application/json;' 127.0.0.1:9650/v1/bc/C49rHzk3vLr1w9Z8sY7scrZ69TU4WcD2pRS6ZyzaSn9xA2U9F/validators
```
**Example Response:**