diff --git a/.run/Remote Debug Cardinal port 40000.run.xml b/.run/Remote Debug Cardinal port 40000.run.xml new file mode 100644 index 0000000..f131372 --- /dev/null +++ b/.run/Remote Debug Cardinal port 40000.run.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index 7f690eb..e16ab48 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ A persona tag is essentially a cardinal based user. To create a persona tag in y endpoint from the dropdown. Make sure to paste in a valid User ID into the User ID field. Set the request body to: ```json { - "persona_tag": "some-persona-tag", + "persona_tag": "some-persona-tag" } ``` diff --git a/cardinal/Dockerfile b/cardinal/Dockerfile index bd8b1f1..ac45fd2 100644 --- a/cardinal/Dockerfile +++ b/cardinal/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20 AS builder +FROM golang:1.21 AS builder WORKDIR /usr/src/app diff --git a/cardinal/Dockerfile.debug b/cardinal/Dockerfile.debug new file mode 100644 index 0000000..7f73426 --- /dev/null +++ b/cardinal/Dockerfile.debug @@ -0,0 +1,17 @@ +FROM golang:1.21 AS build + +# Build Delve +RUN go install github.com/go-delve/delve/cmd/dlv@latest + +WORKDIR /usr/src/app + +copy . . +copy vendor vendor/ + +RUN go build -gcflags="all=-N -l" -v -o /usr/local/bin/app + +FROM ubuntu:22.04 +COPY --from=build /go/bin/dlv /usr/local/bin +COPY --from=build /usr/local/bin/app /usr/local/bin/ + +CMD ["dlv", "--listen=:40000", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/usr/local/bin/app"] diff --git a/cardinal/component/health.go b/cardinal/component/health.go index 6ba18b3..9c6dc6f 100644 --- a/cardinal/component/health.go +++ b/cardinal/component/health.go @@ -6,4 +6,4 @@ type HealthComponent struct { HP int } -var Health = ecs.NewComponentType[HealthComponent]() +var Health = ecs.NewComponentType[HealthComponent]("Health") diff --git a/cardinal/component/player.go b/cardinal/component/player.go index b72dc60..388eaca 100644 --- a/cardinal/component/player.go +++ b/cardinal/component/player.go @@ -3,7 +3,7 @@ package component import "pkg.world.dev/world-engine/cardinal/ecs" type PlayerComponent struct { - Nickname string + Nickname string `json:"nickname"` } -var Player = ecs.NewComponentType[PlayerComponent]() +var Player = ecs.NewComponentType[PlayerComponent]("Player") diff --git a/cardinal/go.mod b/cardinal/go.mod index e5dffb5..bbe81f4 100644 --- a/cardinal/go.mod +++ b/cardinal/go.mod @@ -13,6 +13,7 @@ replace ( require github.com/rs/zerolog v1.30.0 require ( + github.com/alecthomas/participle/v2 v2.1.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.3 // indirect @@ -130,8 +131,8 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - pkg.world.dev/world-engine/cardinal v0.1.33-alpha + pkg.world.dev/world-engine/cardinal v0.1.36-alpha pkg.world.dev/world-engine/chain v0.1.11-alpha // indirect - pkg.world.dev/world-engine/sign v0.1.7-alpha // indirect + pkg.world.dev/world-engine/sign v0.1.8-alpha // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/cardinal/go.sum b/cardinal/go.sum index 4c6e10a..e64b7b2 100644 --- a/cardinal/go.sum +++ b/cardinal/go.sum @@ -425,6 +425,12 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4= +github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -866,6 +872,8 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU= github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -1903,12 +1911,12 @@ nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -pkg.world.dev/world-engine/cardinal v0.1.33-alpha h1:lSqsYIz+JsHCiZozqvlmEc+XOjB5ZIY5vH93JWne6wg= -pkg.world.dev/world-engine/cardinal v0.1.33-alpha/go.mod h1:cGVvoACq1bWWy13te3BRJW7+SE5MK4hUYa+0K/69imU= +pkg.world.dev/world-engine/cardinal v0.1.36-alpha h1:HsxCwTqBLA+43mG/8fZodoyLSsM+WvT8CiLBG8lltyI= +pkg.world.dev/world-engine/cardinal v0.1.36-alpha/go.mod h1:lT+hsjIGdT0rbjSmdCL4m3Mkl5ag27Lc+HkID5yVWDA= pkg.world.dev/world-engine/chain v0.1.11-alpha h1:a+a+eZUIG2XuO+PL5WBr+IgQZoGRnm8plk5bP1KKoR4= pkg.world.dev/world-engine/chain v0.1.11-alpha/go.mod h1:qpm1QXHj2RyXIiwkEolaZMMqeNVcMX+hH4OQ9nE0/5M= -pkg.world.dev/world-engine/sign v0.1.7-alpha h1:7Oaoc0OUBP0fAPGna+/fw5fbrNw88sIVzr0SujY0wgQ= -pkg.world.dev/world-engine/sign v0.1.7-alpha/go.mod h1:JcKXhdeYvCPRAgsCUSFEDcAJgf67VgMWWrA2Y2VZJSg= +pkg.world.dev/world-engine/sign v0.1.8-alpha h1:MZNsYdmQVMy1Fc29d1XSgtVl2T4HDRbdmQ3pruLyQwM= +pkg.world.dev/world-engine/sign v0.1.8-alpha/go.mod h1:vSLK2OoAN+ZVJsZK58YiLXLvQZOJwuhT9R05J664ypE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/cardinal/main.go b/cardinal/main.go index db0402c..f895589 100644 --- a/cardinal/main.go +++ b/cardinal/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "time" "github.com/argus-labs/starter-game-template/cardinal/component" @@ -73,9 +74,15 @@ func main() { world.StartGameLoop(context.Background(), time.Second) // TODO: When launching to production, you should enable signature verification. + fmt.Println("Serving Cardinal at: ", cfg.CardinalPort) h, err := server.NewHandler(world, server.WithPort(cfg.CardinalPort), server.DisableSignatureVerification()) if err != nil { panic(err) } - h.Serve() + err = h.Serve() + if err != nil { + log.Fatal().Err(err) + return + } + } diff --git a/docker-compose-debug.yml b/docker-compose-debug.yml new file mode 100644 index 0000000..115f572 --- /dev/null +++ b/docker-compose-debug.yml @@ -0,0 +1,91 @@ +version: "3" +services: + postgres: + command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all + environment: + - POSTGRES_DB=nakama + - POSTGRES_PASSWORD=localdb + expose: + - "8080" + - "5432" + image: postgres:12.2-alpine + ports: + - "5432:5432" + - "8080:8080" + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "nakama"] + interval: 3s + timeout: 3s + retries: 5 + volumes: + - data:/var/lib/postgresql/data + redis: # This doesn't have the correct persistence settings. Don't use on for prod. + image: redis:latest + command: redis-server # TODO: This runs without password. Don't use for prod. + expose: + - "6379" + ports: + - "6379:6379" + restart: always + cardinal: + build: + context: ./cardinal + dockerfile: Dockerfile.debug + depends_on: + - redis + expose: + - "3333" + - "40000" + ports: + - "3333:3333" + - "40000:40000" + environment: + - CARDINAL_PORT=3333 + - REDIS_ADDR=redis:6379 + - REDIS_MODE=normal + cap_add: + - SYS_PTRACE + security_opt: + - "seccomp:unconfined" + nakama: + platform: linux/amd4 + build: ./nakama + depends_on: + - postgres + - cardinal + environment: + - CARDINAL_ADDR=${CARDINAL_ADDR:-http://cardinal:3333} + - CARDINAL_NAMESPACE=world + entrypoint: + - "/bin/sh" + - "-ecx" + - > + /nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama && + exec /nakama/nakama --config /nakama/data/local.yml --database.address postgres:localdb@postgres:5432/nakama + extra_hosts: + - "host.docker.internal:host-gateway" + expose: + - "7349" + - "7350" + - "7351" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7350/"] + interval: 10s + timeout: 5s + retries: 5 + links: + - "postgres:db" + ports: + - "7349:7349" + - "7350:7350" + - "7351:7351" + restart: unless-stopped + testsuite: + build: ./testsuite + depends_on: + - cardinal + - nakama + environment: + - NAKAMA_ADDRESS=http://nakama:7350 +volumes: + data: diff --git a/docker-compose.yml b/docker-compose.yml index c689941..a942d7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,7 @@ services: - REDIS_ADDR=redis:6379 - REDIS_MODE=normal nakama: + platform: linux/amd4 build: ./nakama depends_on: - postgres diff --git a/magefiles/main.go b/magefiles/main.go index 25f6c47..7ee72ec 100644 --- a/magefiles/main.go +++ b/magefiles/main.go @@ -81,6 +81,19 @@ func Start() error { return nil } +// Start starts Nakama and cardinal in debug mode with cardinal debugger listening on port 40000 +// Note Cardinal server will not run until a debugger is attached port 40000 +func StartDebug() error { + mg.Deps(exitMagefilesDir) + if err := prepareDirs("cardinal", "nakama"); err != nil { + return err + } + if err := sh.RunV("docker", "compose", "-f", "docker-compose-debug.yml", "up", "--build", "cardinal", "nakama"); err != nil { + return err + } + return nil +} + // StartDetach starts Nakama and cardinal with detach and wait-timeout 60s (suit for CI workflow) func StartDetach() error { mg.Deps(exitMagefilesDir) diff --git a/nakama/cardinal.go b/nakama/cardinal.go index 23cc99e..11a3b16 100644 --- a/nakama/cardinal.go +++ b/nakama/cardinal.go @@ -17,11 +17,10 @@ import ( ) var ( - listTxEndpointsEndpoint = "list/tx-endpoints" - listReadEndpoints = "list/read-endpoints" - createPersonaEndpoint = "tx-create-persona" - readPersonaSignerEndpoint = "read-persona-signer" - transactionReceiptsEndpoint = "transaction-receipts" + listEndpoints = "query/http/endpoints" + createPersonaEndpoint = "tx/persona/create-persona" + readPersonaSignerEndpoint = "query/persona/signer" + transactionReceiptsEndpoint = "query/receipts/list" readPersonaSignerStatusUnknown = "unknown" readPersonaSignerStatusAvailable = "available" @@ -50,38 +49,32 @@ func makeURL(resource string) string { return fmt.Sprintf("%s/%s", globalCardinalAddress, resource) } -func cardinalListEndpoints(path string) ([]string, error) { - url := makeURL(path) - resp, err := http.Get(url) +type endpoints struct { + TxEndpoints []string `json:"tx_endpoints"` + QueryEndpoints []string `json:"query_endpoints"` +} + +func cardinalGetEndpointsStruct() (txEndpoints []string, queryEndpoints []string, err error) { + err = nil + var resp *http.Response + url := makeURL(listEndpoints) + resp, err = http.Post(url, "", nil) if err != nil { - return nil, err + return } if resp.StatusCode != 200 { buf, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("list endpoints (at %q) failed with status code %d: %v", url, resp.StatusCode, string(buf)) + err = fmt.Errorf("list endpoints (at %q) failed with status code %d: %v", url, resp.StatusCode, string(buf)) + return } dec := json.NewDecoder(resp.Body) - var endpoints []string - if err := dec.Decode(&endpoints); err != nil { - return nil, err - } - return endpoints, nil - -} - -func cardinalListAllEndpoints() ([]string, error) { - var endpoints []string - txs, err := cardinalListEndpoints(listTxEndpointsEndpoint) - if err != nil { - return nil, err - } - endpoints = append(endpoints, txs...) - reads, err := cardinalListEndpoints(listReadEndpoints) - if err != nil { - return nil, err + var endpointsStruct endpoints + if err = dec.Decode(&endpointsStruct); err != nil { + return } - endpoints = append(endpoints, reads...) - return endpoints, nil + txEndpoints = endpointsStruct.TxEndpoints + queryEndpoints = endpointsStruct.QueryEndpoints + return } func doRequest(req *http.Request) (*http.Response, error) { @@ -124,6 +117,7 @@ func cardinalCreatePersona(ctx context.Context, nk runtime.NakamaModule, persona if err != nil { return "", 0, fmt.Errorf("unable to make request to %q: %w", createPersonaEndpoint, err) } + req.Header.Set("Content-Type", "application/json") resp, err := doRequest(req) if err != nil { @@ -162,6 +156,7 @@ func cardinalQueryPersonaSigner(ctx context.Context, personaTag string, tick uin if err != nil { return "", err } + httpReq.Header.Set("Content-Type", "application/json") httpResp, err := doRequest(httpReq) if err != nil { return "", err diff --git a/nakama/dispatcher.go b/nakama/dispatcher.go index e5c17d0..f3241df 100644 --- a/nakama/dispatcher.go +++ b/nakama/dispatcher.go @@ -113,6 +113,7 @@ func (r *receiptsDispatcher) getBatchOfReceiptsFromCardinal(startTick uint64) (r if err != nil { return nil, err } + req.Header.Set("Content-Type", "application/json") resp, err := doRequest(req) if err != nil { return nil, fmt.Errorf("failed to query %q: %w", url, err) diff --git a/nakama/main.go b/nakama/main.go index ae6e7dc..63cad18 100644 --- a/nakama/main.go +++ b/nakama/main.go @@ -273,46 +273,81 @@ func handleShowPersona(ctx context.Context, logger runtime.Logger, db *sql.DB, n // initCardinalEndpoints queries the cardinal server to find the list of existing endpoints, and attempts to // set up RPC wrappers around each one. func initCardinalEndpoints(logger runtime.Logger, initializer runtime.Initializer) error { - endpoints, err := cardinalListAllEndpoints() + txEndpoints, queryEndpoints, err := cardinalGetEndpointsStruct() if err != nil { - return fmt.Errorf("failed to get list of cardinal endpoints: %w", err) + return err } - for _, e := range endpoints { - logger.Debug("registering: %v", e) - currEndpoint := e - if currEndpoint[0] == '/' { - currEndpoint = currEndpoint[1:] + createSignedPayload := func(payload string, endpoint string, nk runtime.NakamaModule, ctx context.Context) (io.Reader, error) { + logger.Debug("The %s endpoint requires a signed payload", endpoint) + signedPayload, err := makeSignedPayload(ctx, nk, payload) + if err != nil { + return nil, err } - err := initializer.RegisterRpc(currEndpoint, func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { - logger.Debug("Got request for %q", currEndpoint) + return signedPayload, nil + } - signedPayload, err := makeSignedPayload(ctx, nk, payload) - if err != nil { - return logError(logger, "unable to make signed payload: %v", err) - } + createUnsignedPayload := func(payload string, endpoint string, _ runtime.NakamaModule, _ context.Context) (io.Reader, error) { + payloadBytes := []byte(payload) + formattedPayloadBuffer := bytes.NewBuffer([]byte{}) + if !json.Valid(payloadBytes) { + return nil, fmt.Errorf("data %q is not valid json", string(payloadBytes)) + } + err = json.Compact(formattedPayloadBuffer, payloadBytes) + if err != nil { + return nil, err + } + return formattedPayloadBuffer, nil + } - req, err := http.NewRequestWithContext(ctx, "POST", makeURL(currEndpoint), signedPayload) - if err != nil { - return logError(logger, "request setup failed for endpoint %q: %v", currEndpoint, err) + registerEndpoints := func(endpoints []string, createPayload func(string, string, runtime.NakamaModule, context.Context) (io.Reader, error)) error { + for _, e := range endpoints { + logger.Debug("registering: %v", e) + currEndpoint := e + if currEndpoint[0] == '/' { + currEndpoint = currEndpoint[1:] } - resp, err := http.DefaultClient.Do(req) + err := initializer.RegisterRpc(currEndpoint, func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { + logger.Debug("Got request for %q", currEndpoint) + var resultPayload io.Reader + resultPayload, err = createPayload(payload, currEndpoint, nk, ctx) + if err != nil { + return logError(logger, "unable to make payload: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", makeURL(currEndpoint), resultPayload) + req.Header.Set("Content-Type", "application/json") + if err != nil { + return logError(logger, "request setup failed for endpoint %q: %w", currEndpoint, err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return logError(logger, "request failed for endpoint %q: %w", currEndpoint, err) + } + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + return logError(logger, "bad status code: %w: %s", resp.Status, body) + } + bodyStr, err := io.ReadAll(resp.Body) + if err != nil { + return logError(logger, "can't read body: %w", err) + } + return string(bodyStr), nil + }) if err != nil { - return logError(logger, "request failed for endpoint %q: %v", currEndpoint, err) - } - if resp.StatusCode != 200 { - body, _ := io.ReadAll(resp.Body) - return logError(logger, "bad status code: %v: %s", resp.Status, body) - } - str, err := io.ReadAll(resp.Body) - if err != nil { - return logError(logger, "can't read body: %v", err) + return err } - return string(str), nil - }) - if err != nil { - return err } + return nil + } + + err = registerEndpoints(txEndpoints, createSignedPayload) + if err != nil { + return err + } + err = registerEndpoints(queryEndpoints, createUnsignedPayload) + if err != nil { + return err } return nil } diff --git a/nakama/private_key.go b/nakama/private_key.go index 3cc7a8c..83c4353 100644 --- a/nakama/private_key.go +++ b/nakama/private_key.go @@ -110,7 +110,7 @@ func setNonce(ctx context.Context, nk runtime.NakamaModule, n uint64) error { func initPrivateKey(ctx context.Context, logger runtime.Logger, nk runtime.NakamaModule) error { privateKeyHex, err := getPrivateKeyHex(ctx, nk) if err != nil { - if err != ErrorNoStorageObjectFound { + if !errors.Is(err, ErrorNoStorageObjectFound) { return fmt.Errorf("failed to get private key: %w", err) } logger.Debug("no private key found; creating a new one") diff --git a/testsuite/Dockerfile b/testsuite/Dockerfile index 7547b2d..bddd7fb 100644 --- a/testsuite/Dockerfile +++ b/testsuite/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20 +FROM golang:1.21 WORKDIR /app diff --git a/testsuite/go.mod b/testsuite/go.mod index 06f9282..2b249cd 100644 --- a/testsuite/go.mod +++ b/testsuite/go.mod @@ -1,7 +1,21 @@ module github.com/argus-labs/starter-game-template -go 1.20 +go 1.21 + +toolchain go1.21.0 require gotest.tools/v3 v3.5.0 -require github.com/google/go-cmp v0.5.9 // indirect +require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/ethereum/go-ethereum v1.12.0 + github.com/holiman/uint256 v1.2.3 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/sys v0.11.0 // indirect +) + +require ( + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect +) diff --git a/testsuite/go.sum b/testsuite/go.sum index e43f77f..13dc878 100644 --- a/testsuite/go.sum +++ b/testsuite/go.sum @@ -1,4 +1,22 @@ +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/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +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.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +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/ethereum/go-ethereum v1.12.0 h1:bdnhLPtqETd4m3mS8BGMNvBTf36bO5bx/hxE2zljOa0= +github.com/ethereum/go-ethereum v1.12.0/go.mod h1:/oo2X/dZLJjf2mJ6YT9wcWxa4nNJDBKDBU6sFIpx1Gs= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= +github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/testsuite/testsuite_test.go b/testsuite/testsuite_test.go index 0a41f46..3c4294c 100644 --- a/testsuite/testsuite_test.go +++ b/testsuite/testsuite_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "math/rand" "net/http" "strings" @@ -13,8 +14,101 @@ import ( "time" "gotest.tools/v3/assert" + + "github.com/ethereum/go-ethereum/crypto" ) +func TestTransactionAndCQLAndRead(t *testing.T) { + + //Test persona + privateKey, err := crypto.GenerateKey() + assert.NilError(t, err) + signerAddr := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + username, deviceID, personaTag := triple(randomString()) + c := newClient(t) + assert.NilError(t, c.registerDevice(username, deviceID)) + + resp, err := c.rpc("nakama/claim-persona", map[string]any{ + "persona_tag": personaTag, + "signer_address": signerAddr, + }) + assert.NilError(t, err, "claim-persona failed") + assert.Equal(t, 200, resp.StatusCode, copyBody(resp)) + + assert.NilError(t, waitForAcceptedPersonaTag(c)) + type CreatePlayerTxMsg struct { + Nickname string `json:"nickname"` + } + payload := CreatePlayerTxMsg{"Bob"} + resp, err = c.rpc("tx/game/create-player", payload) + assert.NilError(t, err) + body := copyBody(resp) + assert.Equal(t, 200, resp.StatusCode, body) + + //Test CQL + type Data struct { + Nickname string `json:"nickname,omitempty"` + HP int `json:"HP,omitempty"` + } + + type Item struct { + ID int `json:"id"` + Data []Data `json:"data"` + } + finalResults := []Item{} + currentTs := time.Now() + maxTime := 10 * time.Second + + //hits the cql endpoint and eventually expects at least > 1 result + //Since the tests and http server move faster than the game loop, initial tests actually + //return 0 results so this loop will keep going until a timeout or the query gets one result. + for len(finalResults) <= 0 { + resp, err = c.rpc("query/game/cql", struct { + CQL string `json:CQL` + }{"CONTAINS(Player)"}) + assert.NilError(t, err) + assert.Equal(t, 200, resp.StatusCode) + results, err := io.ReadAll(resp.Body) + + err = json.Unmarshal(results, &finalResults) + assert.NilError(t, err) + for _, res := range finalResults { + for i, v := range res.Data { + if i == 0 { + assert.Equal(t, v.Nickname, "Bob") + assert.Equal(t, v.HP, 0) + } else if i == 1 { + assert.Equal(t, v.Nickname, "") + assert.Assert(t, v.HP != 0) + } else { + t.Fatal("Should not have anymore components") + } + } + } + if time.Now().Second()-currentTs.Second() > int(maxTime) { + assert.Assert(t, false, "timeout occured here, CQL query should return some results eventually") + } + } + + //Test Read + type ConstantRequest struct { + Label string `json:"label"` + } + type ConstantResponse struct { + Label string `json:"label"` + Value interface{} `json:"value"` + } + resp, err = c.rpc("query/game/constant", ConstantRequest{"all"}) + assert.NilError(t, err) + bodyBytes, err := io.ReadAll(resp.Body) + assert.NilError(t, err) + typedResp := ConstantResponse{} + err = json.Unmarshal(bodyBytes, &typedResp) + assert.Equal(t, typedResp.Label, "all") + assert.NilError(t, err) + +} + func TestCanShowPersona(t *testing.T) { username, deviceID, personaTag := triple(randomString()) c := newClient(t) diff --git a/testsuite/util.go b/testsuite/util.go index 66fc8ac..0717e56 100644 --- a/testsuite/util.go +++ b/testsuite/util.go @@ -8,8 +8,6 @@ import ( "net/http" "os" "testing" - - "gotest.tools/v3/assert" ) const ( @@ -23,8 +21,9 @@ type nakamaClient struct { func newClient(t *testing.T) *nakamaClient { host := os.Getenv(envNakamaAddress) - assert.Check(t, host != "", "nakama address must be set via environment variable %s", envNakamaAddress) - + if host == "" { + host = "http://127.0.0.1:7350" + } h := &nakamaClient{ addr: host, } @@ -44,6 +43,9 @@ func (c *nakamaClient) registerDevice(username, deviceID string) error { } reader := bytes.NewReader(buf) req, err := http.NewRequest("POST", url, reader) + if err != nil { + return err + } req.Header.Set("Content-Type", "application/json") // defaultkey is the default server key. See https://heroiclabs.com/docs/nakama/concepts/authentication/ for more // details.