From 357b4734105115003dfc92d8167896ea74080e9e Mon Sep 17 00:00:00 2001 From: bulkcade Date: Tue, 13 Jan 2026 11:34:37 +0000 Subject: [PATCH 01/13] cache setting addition to transport --- core/tnclient/transport_cre.go | 60 ++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/core/tnclient/transport_cre.go b/core/tnclient/transport_cre.go index 504e4fa..dbdb43d 100644 --- a/core/tnclient/transport_cre.go +++ b/core/tnclient/transport_cre.go @@ -24,6 +24,7 @@ import ( "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" "github.com/smartcontractkit/cre-sdk-go/cre" + "google.golang.org/protobuf/types/known/durationpb" ) // CRETransport implements Transport using Chainlink CRE's HTTP client. @@ -112,6 +113,26 @@ func (t *CRETransport) nextReqID() string { return strconv.FormatUint(id, 10) } +const defaultBroadcastCacheMaxAge = 2 * time.Minute + +// cacheSettingsForJSONRPC determines caching behavior per JSON-RPC method. +// Uses paramsJSON so callers can evolve this policy without changing call sites. +func (t *CRETransport) cacheSettingsForJSONRPC(method string, paramsJSON []byte) *http.CacheSettings { + _ = paramsJSON // reserved for future policy logic; intentionally not used today + + switch method { + case "user.broadcast": + // Non-idempotent at the gateway boundary; enable shared cache so other nodes + // can reuse the first node's response if the request matches. + return &http.CacheSettings{ + Store: true, + MaxAge: durationpb.New(defaultBroadcastCacheMaxAge), + } + default: + return nil + } +} + // callJSONRPC makes a JSON-RPC call via CRE HTTP client // It automatically handles authentication if the endpoint returns 401 func (t *CRETransport) callJSONRPC(ctx context.Context, method string, params any, result any) error { @@ -147,6 +168,8 @@ func (t *CRETransport) doJSONRPC(ctx context.Context, method string, params any, return fmt.Errorf("failed to marshal params: %w", err) } + cacheSettings := t.cacheSettingsForJSONRPC(method, paramsJSON) + // Create JSON-RPC request reqID := t.nextReqID() rpcReq := jsonrpc.NewRequest(reqID, method, paramsJSON) @@ -171,10 +194,11 @@ func (t *CRETransport) doJSONRPC(ctx context.Context, method string, params any, // Create CRE HTTP request httpReq := &http.Request{ - Url: t.endpoint, - Method: "POST", - Body: requestBody, - Headers: headers, + Url: t.endpoint, + Method: "POST", + Body: requestBody, + Headers: headers, + CacheSettings: cacheSettings, } // Execute via CRE client (returns Promise) @@ -448,6 +472,16 @@ func (t *CRETransport) executeOnce(ctx context.Context, namespace string, action `{"jsonrpc":"2.0","id":"%s","method":"user.broadcast","params":{"tx":%s}}`, reqID, string(txJSON)) + // Build paramsJSON for cache policy evaluation (without changing request construction) + type broadcastParams struct { + Tx json.RawMessage `json:"tx"` + } + paramsJSON, err := json.Marshal(&broadcastParams{Tx: txJSON}) + if err != nil { + return types.Hash{}, fmt.Errorf("failed to marshal broadcast params: %w", err) + } + cacheSettings := t.cacheSettingsForJSONRPC("user.broadcast", paramsJSON) + // Create headers headers := map[string]string{ "Content-Type": "application/json", @@ -462,10 +496,11 @@ func (t *CRETransport) executeOnce(ctx context.Context, namespace string, action // Create CRE HTTP request httpReq := &http.Request{ - Url: t.endpoint, - Method: "POST", - Body: []byte(rpcReqJSON), - Headers: headers, + Url: t.endpoint, + Method: "POST", + Body: []byte(rpcReqJSON), + Headers: headers, + CacheSettings: cacheSettings, } // Execute via CRE client @@ -684,8 +719,7 @@ func (t *CRETransport) Signer() auth.Signer { return t.signer } -// authenticate performs gateway authentication and stores the cookie. -// This is called automatically when a 401 error is received. +// authenticate performs gateway authentication and stores the cookie.// This is called automatically when a 401 error is received. func (t *CRETransport) authenticate(ctx context.Context) error { if t.signer == nil { return fmt.Errorf("cannot authenticate without a signer") @@ -767,6 +801,8 @@ func (t *CRETransport) doJSONRPCWithResponse(ctx context.Context, method string, return nil, fmt.Errorf("failed to marshal params: %w", err) } + cacheSettings := t.cacheSettingsForJSONRPC(method, paramsJSON) + // Create JSON-RPC request reqID := t.nextReqID() rpcReq := jsonrpc.NewRequest(reqID, method, paramsJSON) @@ -785,6 +821,7 @@ func (t *CRETransport) doJSONRPCWithResponse(ctx context.Context, method string, Headers: map[string]string{ "Content-Type": "application/json", }, + CacheSettings: cacheSettings, } // Execute via CRE client (returns Promise) @@ -814,9 +851,6 @@ func (t *CRETransport) doJSONRPCWithResponse(ctx context.Context, method string, } // composeGatewayAuthMessage composes a SIWE-like authentication message. -// This matches the format used by kwil-db gateway client. -// Note: This is a custom format, not standard SIWE - it omits the account address line -// and uses "Issue At" instead of "Issued At" to match kgw's expectations. func composeGatewayAuthMessage(param *gateway.AuthnParameterResponse, domain string, uri string, version string, chainID string) string { var msg bytes.Buffer msg.WriteString(domain + " wants you to sign in with your account:\n") From 5a1d1f0bab211bb62ab00357db2faf0aae1e987d Mon Sep 17 00:00:00 2001 From: bulkcade Date: Tue, 13 Jan 2026 13:35:48 +0000 Subject: [PATCH 02/13] add initial test gen --- core/tnclient/transport_cre_test.go | 205 ++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/core/tnclient/transport_cre_test.go b/core/tnclient/transport_cre_test.go index b1d9ff6..d5ff4bd 100644 --- a/core/tnclient/transport_cre_test.go +++ b/core/tnclient/transport_cre_test.go @@ -4,9 +4,16 @@ package tnclient import ( "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "strconv" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Note: These are basic structural tests for CRE transport. @@ -162,3 +169,201 @@ func TestIsTransientTxError(t *testing.T) { }) } } + +// ----------------------------------------------------------------------------- +// New tests for CRE HTTP cache settings +// ----------------------------------------------------------------------------- + +func TestCRETransport_CacheSettingsForJSONRPC(t *testing.T) { + t.Run("broadcast_is_cached", func(t *testing.T) { + tr := &CRETransport{} + cs := tr.cacheSettingsForJSONRPC("user.broadcast", []byte(`{"tx":"abc"}`)) + require.NotNil(t, cs, "user.broadcast should return non-nil CacheSettings") + assert.True(t, cs.Store, "user.broadcast should set Store=true") + require.NotNil(t, cs.MaxAge, "user.broadcast should set MaxAge") + assert.Equal(t, defaultBroadcastCacheMaxAge, cs.MaxAge.AsDuration(), "MaxAge should match defaultBroadcastCacheMaxAge") + }) + + t.Run("non_broadcast_is_not_cached", func(t *testing.T) { + tr := &CRETransport{} + assert.Nil(t, tr.cacheSettingsForJSONRPC("user.call", []byte(`{}`))) + assert.Nil(t, tr.cacheSettingsForJSONRPC("user.tx_query", []byte(`{}`))) + assert.Nil(t, tr.cacheSettingsForJSONRPC("kgw.authn", []byte(`{}`))) + }) + + t.Run("paramsJSON_is_accepted", func(t *testing.T) { + // Policy currently ignores paramsJSON, but this test ensures we can pass + // arbitrary JSON without panicking and still get the expected outcome. + tr := &CRETransport{} + cs := tr.cacheSettingsForJSONRPC("user.broadcast", []byte(`{"nested":{"a":[1,2,3]}}`)) + require.NotNil(t, cs) + assert.Equal(t, defaultBroadcastCacheMaxAge, cs.MaxAge.AsDuration()) + }) +} + +// Test that the caching change is actually wired into HTTP request construction. +// +// We cannot execute the CRE HTTP client outside a CRE runtime, so this test parses +// transport_cre.go and verifies that: +// - doJSONRPC, doJSONRPCWithResponse, and executeOnce each set CacheSettings in +// their &http.Request literals +// - the CacheSettings value is the local variable named `cacheSettings` +// - `cacheSettings` is assigned from t.cacheSettingsForJSONRPC(..., paramsJSON) +func TestCRETransport_HTTPRequestsIncludeCacheSettings(t *testing.T) { + src, err := os.ReadFile("transport_cre.go") + require.NoError(t, err, "failed to read transport_cre.go") + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "transport_cre.go", src, 0) + require.NoError(t, err, "failed to parse transport_cre.go") + + targets := []struct { + funcName string + expectFirstArgIsMethod bool // if true, expects first arg ident "method" + expectFirstArgString string // if non-empty, expects string literal + }{ + {funcName: "doJSONRPC", expectFirstArgIsMethod: true}, + {funcName: "doJSONRPCWithResponse", expectFirstArgIsMethod: true}, + {funcName: "executeOnce", expectFirstArgString: "user.broadcast"}, + } + + for _, tc := range targets { + t.Run(tc.funcName, func(t *testing.T) { + fd := findFuncDecl(t, f, tc.funcName) + require.NotNil(t, fd, "function %s not found", tc.funcName) + + // 1) Ensure we assign: cacheSettings := t.cacheSettingsForJSONRPC(, paramsJSON) + assert.True(t, hasCacheSettingsAssignment(t, fd, tc.expectFirstArgIsMethod, tc.expectFirstArgString), + "%s should assign cacheSettings := t.cacheSettingsForJSONRPC(..., paramsJSON)", tc.funcName) + + // 2) Ensure &http.Request{..., CacheSettings: cacheSettings, ...} exists + assert.True(t, hasHttpRequestWithCacheSettings(t, fd), + "%s should set CacheSettings on http.Request literal", tc.funcName) + }) + } +} + +func findFuncDecl(t *testing.T, file *ast.File, name string) *ast.FuncDecl { + t.Helper() + for _, decl := range file.Decls { + if fd, ok := decl.(*ast.FuncDecl); ok && fd.Name != nil && fd.Name.Name == name { + return fd + } + } + return nil +} + +func hasCacheSettingsAssignment(t *testing.T, fd *ast.FuncDecl, firstArgIsMethod bool, firstArgString string) bool { + t.Helper() + + found := false + ast.Inspect(fd, func(n ast.Node) bool { + as, ok := n.(*ast.AssignStmt) + if !ok || len(as.Lhs) != 1 || len(as.Rhs) != 1 { + return true + } + + lhs, ok := as.Lhs[0].(*ast.Ident) + if !ok || lhs.Name != "cacheSettings" { + return true + } + + // RHS must be t.cacheSettingsForJSONRPC(...) + call, ok := as.Rhs[0].(*ast.CallExpr) + if !ok { + return true + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + recv, ok := sel.X.(*ast.Ident) + if !ok || recv.Name != "t" || sel.Sel == nil || sel.Sel.Name != "cacheSettingsForJSONRPC" { + return true + } + + if len(call.Args) != 2 { + return true + } + + // Second arg must be paramsJSON ident + if id2, ok := call.Args[1].(*ast.Ident); !ok || id2.Name != "paramsJSON" { + return true + } + + // First arg check + if firstArgIsMethod { + id1, ok := call.Args[0].(*ast.Ident) + if !ok || id1.Name != "method" { + return true + } + } + if firstArgString != "" { + bl, ok := call.Args[0].(*ast.BasicLit) + if !ok || bl.Kind != token.STRING { + return true + } + unquoted, err := strconv.Unquote(bl.Value) + if err != nil || unquoted != firstArgString { + return true + } + } + + found = true + return true + }) + return found +} + +func hasHttpRequestWithCacheSettings(t *testing.T, fd *ast.FuncDecl) bool { + t.Helper() + + found := false + ast.Inspect(fd, func(n ast.Node) bool { + // Look for: &http.Request{ ... CacheSettings: cacheSettings ... } + ue, ok := n.(*ast.UnaryExpr) + if !ok || ue.Op != token.AND { + return true + } + cl, ok := ue.X.(*ast.CompositeLit) + if !ok { + return true + } + // Type must be http.Request + se, ok := cl.Type.(*ast.SelectorExpr) + if !ok { + return true + } + pkg, ok := se.X.(*ast.Ident) + if !ok || pkg.Name != "http" || se.Sel == nil || se.Sel.Name != "Request" { + return true + } + + // Must include CacheSettings: cacheSettings + for _, elt := range cl.Elts { + kv, ok := elt.(*ast.KeyValueExpr) + if !ok { + continue + } + key, ok := kv.Key.(*ast.Ident) + if !ok || key.Name != "CacheSettings" { + continue + } + val, ok := kv.Value.(*ast.Ident) + if !ok || val.Name != "cacheSettings" { + continue + } + found = true + return true + } + + return true + }) + return found +} + +func TestDefaultBroadcastCacheMaxAge(t *testing.T) { + // Defensive check: ensure the constant remains what the transport expects. + // If this is intentionally changed, update tests accordingly. + assert.Equal(t, 2*time.Minute, defaultBroadcastCacheMaxAge) +} From ffe720bb6e643cb5ad13cb53d658a16357a1a346 Mon Sep 17 00:00:00 2001 From: bulkcade Date: Tue, 13 Jan 2026 14:02:13 +0000 Subject: [PATCH 03/13] documentation update --- docs/CRE_INTEGRATION.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/CRE_INTEGRATION.md b/docs/CRE_INTEGRATION.md index 06d1d0c..da351f8 100644 --- a/docs/CRE_INTEGRATION.md +++ b/docs/CRE_INTEGRATION.md @@ -168,6 +168,36 @@ Use for: --- +### CRE HTTP caching (recommended for non-idempotent writes) + +CRE executes workflow logic across multiple DON nodes. Without additional controls, **each node may independently issue the same HTTP request**, including requests that have side effects. For non-idempotent operations (for example, submitting transactions or creating resources), this can result in duplicate external calls. + +CRE’s Go HTTP client supports best-effort request de-duplication via `CacheSettings` on `http.Request`. When enabled, one node performs the request and stores the response; other nodes can reuse it if it is still fresh (`MaxAge`). This is most appropriate for **POST/PUT/PATCH/DELETE-style** operations that should not be executed multiple times. + +Example (raw CRE HTTP request): + +```go +import ( + "time" + "google.golang.org/protobuf/types/known/durationpb" + crehttp "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http" +) + +// ... +req := &crehttp.Request{ + Url: "https://example.com/api", + Method: "POST", + Body: payloadBytes, + CacheSettings: &crehttp.CacheSettings{ + Store: true, + MaxAge: durationpb.New(2 * time.Minute), + }, +} +resp, err := client.SendRequest(nodeRuntime, req).Await() + +``` +--- + ## API Reference For detailed API documentation including function signatures, parameters, and usage examples, see: @@ -186,6 +216,7 @@ For detailed API documentation including function signatures, parameters, and us - [Chainlink CRE Documentation](https://docs.chain.link/cre) - [TRUF.NETWORK SDK Documentation](./api-reference.md) - [CRE SDK Go Documentation](https://pkg.go.dev/github.com/smartcontractkit/cre-sdk-go) +- [CRE HTTP documentation](https://docs.chain.link/cre/guides/workflow/using-http-client/post-request-go) ### Examples - [TRUF + CRE Complete Demo](../examples/truf-cre-demo/) - 3-workflow pattern demonstrating full CRUD lifecycle From 1fed4029ed114e15abacef9da0b9586bf5c589c5 Mon Sep 17 00:00:00 2001 From: bulkcade Date: Tue, 13 Jan 2026 15:43:29 +0000 Subject: [PATCH 04/13] make configurable in transport and helper functions for initialisation --- core/tnclient/transport_cre.go | 84 ++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/core/tnclient/transport_cre.go b/core/tnclient/transport_cre.go index dbdb43d..1bccfc1 100644 --- a/core/tnclient/transport_cre.go +++ b/core/tnclient/transport_cre.go @@ -68,6 +68,8 @@ type CRETransport struct { currentNonce int64 // Track nonce for sequential transactions nonceMu sync.Mutex nonceFetched bool + httpCacheStore bool + httpCacheMaxAge time.Duration } // Verify CRETransport implements Transport interface at compile time @@ -99,37 +101,83 @@ func NewCRETransport(runtime cre.NodeRuntime, endpoint string, signer auth.Signe } return &CRETransport{ - runtime: runtime, - client: &http.Client{}, - endpoint: endpoint, - signer: signer, - chainID: "", // Will be fetched on first call if needed + runtime: runtime, + client: &http.Client{}, + endpoint: endpoint, + signer: signer, + chainID: "", // Will be fetched on first call if needed + httpCacheStore: defaultHTTPCacheStore, + httpCacheMaxAge: defaultHTTPCacheMaxAge, }, nil } +func NewCRETransportWithHTTPCache(runtime cre.NodeRuntime, endpoint string, signer auth.Signer, cacheCfg *CREHTTPCacheConfig) (*CRETransport, error) { + t, err := NewCRETransport(runtime, endpoint, signer) + if err != nil { + return nil, err + } + t.ApplyHTTPCacheConfig(cacheCfg) + return t, nil +} + +func (t *CRETransport) ApplyHTTPCacheConfig(cfg *CREHTTPCacheConfig) { + if cfg == nil { + return + } + + if cfg.Store != nil { + t.httpCacheStore = *cfg.Store + } + + if cfg.MaxAgeSeconds != nil { + secs := *cfg.MaxAgeSeconds + if secs < 0 { + secs = 0 + } + d := time.Duration(secs) * time.Second + + // Clamp to CRE max. + if d > maxHTTPCacheMaxAge { + d = maxHTTPCacheMaxAge + } + t.httpCacheMaxAge = d + } +} + // nextReqID generates the next JSON-RPC request ID func (t *CRETransport) nextReqID() string { id := t.reqID.Add(1) return strconv.FormatUint(id, 10) } -const defaultBroadcastCacheMaxAge = 2 * time.Minute +// Transport-wide defaults (applied when workflow config does not specify cache settings). +const ( + defaultHTTPCacheStore = true + defaultHTTPCacheMaxAge = 60 * time.Second + + // CRE documented max for cache MaxAge. + maxHTTPCacheMaxAge = 10 * time.Minute +) + +// CREHTTPCacheConfig is intended to be populated from workflow config and then +// passed into the SDK client wiring (Option A). +type CREHTTPCacheConfig struct { + // If nil, defaults to true. + Store *bool `json:"store,omitempty"` + + // If nil, defaults to 60 seconds. Values > 600 are clamped to 600 (10 minutes). + MaxAgeSeconds *int64 `json:"maxAgeSeconds,omitempty"` +} // cacheSettingsForJSONRPC determines caching behavior per JSON-RPC method. // Uses paramsJSON so callers can evolve this policy without changing call sites. func (t *CRETransport) cacheSettingsForJSONRPC(method string, paramsJSON []byte) *http.CacheSettings { - _ = paramsJSON // reserved for future policy logic; intentionally not used today - - switch method { - case "user.broadcast": - // Non-idempotent at the gateway boundary; enable shared cache so other nodes - // can reuse the first node's response if the request matches. - return &http.CacheSettings{ - Store: true, - MaxAge: durationpb.New(defaultBroadcastCacheMaxAge), - } - default: - return nil + // Always attach cache settings so every CRE HTTP request participates. + // Note: per CRE docs, if MaxAge is nil/zero it won't read from cache, + // but may still store if Store is true. :contentReference[oaicite:2]{index=2} + return &http.CacheSettings{ + Store: t.httpCacheStore, + MaxAge: durationpb.New(t.httpCacheMaxAge), } } From 0b1d36b1db3bd443e609f012e99ea861ff0153b4 Mon Sep 17 00:00:00 2001 From: bulkcade Date: Tue, 13 Jan 2026 16:21:09 +0000 Subject: [PATCH 05/13] make reqId guard against potential caching issues --- core/tnclient/transport_cre.go | 38 +++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/core/tnclient/transport_cre.go b/core/tnclient/transport_cre.go index 1bccfc1..1d8337a 100644 --- a/core/tnclient/transport_cre.go +++ b/core/tnclient/transport_cre.go @@ -5,6 +5,8 @@ package tnclient import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "math/big" @@ -144,8 +146,34 @@ func (t *CRETransport) ApplyHTTPCacheConfig(cfg *CREHTTPCacheConfig) { } } -// nextReqID generates the next JSON-RPC request ID -func (t *CRETransport) nextReqID() string { +func shouldUseDeterministicID(cs *http.CacheSettings) bool { + if cs == nil { + return false + } + if cs.Store { + return true + } + if cs.MaxAge != nil && cs.MaxAge.AsDuration() > 0 { + return true + } + return false +} + +func (t *CRETransport) nextReqID(method string, paramsJSON []byte) string { + // Cache is "active" if we store or we attempt cache reads (MaxAge > 0). + cacheActive := t.httpCacheStore || (t.httpCacheMaxAge > 0) + + if cacheActive { + //required because multiple node triggers could mean reqID overrides intended caching + h := sha256.New() + h.Write([]byte(method)) + h.Write([]byte{0}) + h.Write(paramsJSON) + sum := h.Sum(nil) + return "tn:" + hex.EncodeToString(sum[:8]) + } + + // Default behavior (unchanged): monotonically increasing per process id := t.reqID.Add(1) return strconv.FormatUint(id, 10) } @@ -219,7 +247,7 @@ func (t *CRETransport) doJSONRPC(ctx context.Context, method string, params any, cacheSettings := t.cacheSettingsForJSONRPC(method, paramsJSON) // Create JSON-RPC request - reqID := t.nextReqID() + reqID := t.nextReqID(method, paramsJSON) rpcReq := jsonrpc.NewRequest(reqID, method, paramsJSON) // Marshal the full request @@ -515,7 +543,7 @@ func (t *CRETransport) executeOnce(ctx context.Context, namespace string, action } // Manually construct JSON-RPC request to bypass params map - reqID := t.nextReqID() + reqID := t.nextReqID("user.broadcast", txJSON) rpcReqJSON := fmt.Sprintf( `{"jsonrpc":"2.0","id":"%s","method":"user.broadcast","params":{"tx":%s}}`, reqID, string(txJSON)) @@ -852,7 +880,7 @@ func (t *CRETransport) doJSONRPCWithResponse(ctx context.Context, method string, cacheSettings := t.cacheSettingsForJSONRPC(method, paramsJSON) // Create JSON-RPC request - reqID := t.nextReqID() + reqID := t.nextReqID(method, paramsJSON) rpcReq := jsonrpc.NewRequest(reqID, method, paramsJSON) // Marshal the full request From a67db996a561d4e73c2ada3c05f04f8812287524 Mon Sep 17 00:00:00 2001 From: bulkcade Date: Wed, 14 Jan 2026 09:25:59 +0000 Subject: [PATCH 06/13] modify structural tests given new reqID implementation and transport config --- core/tnclient/transport_cre_test.go | 212 ++++++++++++++++++++-------- 1 file changed, 155 insertions(+), 57 deletions(-) diff --git a/core/tnclient/transport_cre_test.go b/core/tnclient/transport_cre_test.go index d5ff4bd..e944010 100644 --- a/core/tnclient/transport_cre_test.go +++ b/core/tnclient/transport_cre_test.go @@ -21,57 +21,56 @@ import ( // See the examples/cre_integration/ directory for complete working examples. func TestNewCRETransport(t *testing.T) { - // Note: We cannot create a real NodeRuntime outside of CRE environment, - // so this test just verifies the function signature and basic structure. + t.Run("constructor_exists", func(t *testi + assert.NotNil(t, NewCRETransport) + }) - t.Run("constructor_exists", func(t *testing.T) { - // This test just verifies that the NewCRETransport function exists - // and has the expected signature. - // Actual testing requires CRE simulation environment. + t.Run("defaults_and_endpoint_normalization", func(t *testing.T) { + tr, err := NewCRETransport(nil, "https://example.com", nil) + require.NoError(t, err) - // Verify the function is not nil - assert.NotNil(t, NewCRETransport) + // Endpoint should have /rpc/v1 suffix + assert.Equal(t, "https://example.com/rpc/v1", tr.endpoint) + + // Cache defaults should be applied + assert.Equal(t, defaultHTTPCacheStore, tr.httpCacheStore) + assert.Equal(t, defaultHTTPCacheMaxAge, tr.httpCacheMaxAge) + + // Sanity: cache settings should be non-nil for any method + cs := tr.cacheSettingsForJSONRPC("user.call", []byte(`{}`)) + require.NotNil(t, cs) + assert.Equal(t, defaultHTTPCacheStore, cs.Store) + require.NotNil(t, cs.MaxAge) + assert.Equal(t, defaultHTTPCacheMaxAge, cs.MaxAge.AsDuration()) }) } func TestCRETransport_Implements_Transport_Interface(t *testing.T) { - // This compile-time check verifies that CRETransport implements Transport - // The var _ Transport = (*CRETransport)(nil) line in transport_cre.go - // ensures this at compile time, but we include this test for documentation. - t.Run("implements_interface", func(t *testing.T) { - // If this compiles, the interface is implemented var _ Transport = (*CRETransport)(nil) }) } func TestWithCRETransport(t *testing.T) { t.Run("option_exists", func(t *testing.T) { - // Verify the WithCRETransport option function exists assert.NotNil(t, WithCRETransport) }) t.Run("option_signature", func(t *testing.T) { - // Verify the function returns an Option - // This test documents the expected signature var _ Option = WithCRETransport(nil, "http://example.com") }) } func TestWithCRETransportAndSigner(t *testing.T) { t.Run("option_exists", func(t *testing.T) { - // Verify the WithCRETransportAndSigner option function exists assert.NotNil(t, WithCRETransportAndSigner) }) t.Run("option_signature", func(t *testing.T) { - // Verify the function returns an Option var _ Option = WithCRETransportAndSigner(nil, "http://example.com", nil) }) } -// Unit tests for error classification - func TestIsTransientTxError(t *testing.T) { tests := []struct { name string @@ -95,7 +94,7 @@ func TestIsTransientTxError(t *testing.T) { name: "Multi-word message with code", err: fmt.Errorf("JSON-RPC error: transaction not found in mempool or ledger (code: -202)"), want: true, - reasoning: "Regex should handle multi-word messages (fixed from %*s limitation)", + reasoning: "Regex should handle multi-word messages", }, { name: "ErrorTimeout code", @@ -170,34 +169,141 @@ func TestIsTransientTxError(t *testing.T) { } } -// ----------------------------------------------------------------------------- -// New tests for CRE HTTP cache settings -// ----------------------------------------------------------------------------- +func TestCRETransport_ApplyHTTPCacheConfig(t *testing.T) { + t.Run("nil_config_is_noop", func(t *testing.T) { + tr, err := NewCRETransport(nil, "https://example.com", nil) + require.NoError(t, err) -func TestCRETransport_CacheSettingsForJSONRPC(t *testing.T) { - t.Run("broadcast_is_cached", func(t *testing.T) { - tr := &CRETransport{} - cs := tr.cacheSettingsForJSONRPC("user.broadcast", []byte(`{"tx":"abc"}`)) - require.NotNil(t, cs, "user.broadcast should return non-nil CacheSettings") - assert.True(t, cs.Store, "user.broadcast should set Store=true") - require.NotNil(t, cs.MaxAge, "user.broadcast should set MaxAge") - assert.Equal(t, defaultBroadcastCacheMaxAge, cs.MaxAge.AsDuration(), "MaxAge should match defaultBroadcastCacheMaxAge") + beforeStore := tr.httpCacheStore + beforeAge := tr.httpCacheMaxAge + + tr.ApplyHTTPCacheConfig(nil) + + assert.Equal(t, beforeStore, tr.httpCacheStore) + assert.Equal(t, beforeAge, tr.httpCacheMaxAge) + }) + + t.Run("overrides_are_applied", func(t *testing.T) { + tr, err := NewCRETransport(nil, "https://example.com", nil) + require.NoError(t, err) + + store := false + secs := int64(30) + cfg := &CREHTTPCacheConfig{Store: &store, MaxAgeSeconds: &secs} + + tr.ApplyHTTPCacheConfig(cfg) + + assert.False(t, tr.httpCacheStore) + assert.Equal(t, 30*time.Second, tr.httpCacheMaxAge) + + cs := tr.cacheSettingsForJSONRPC("user.call", []byte(`{}`)) + require.NotNil(t, cs) + assert.False(t, cs.Store) + require.NotNil(t, cs.MaxAge) + assert.Equal(t, 30*time.Second, cs.MaxAge.AsDuration()) + }) + + t.Run("negative_max_age_is_clamped_to_zero", func(t *testing.T) { + tr, err := NewCRETransport(nil, "https://example.com", nil) + require.NoError(t, err) + + secs := int64(-5) + cfg := &CREHTTPCacheConfig{MaxAgeSeconds: &secs} + tr.ApplyHTTPCacheConfig(cfg) + + assert.Equal(t, 0*time.Second, tr.httpCacheMaxAge) + + cs := tr.cacheSettingsForJSONRPC("user.call", []byte(`{}`)) + require.NotNil(t, cs) + require.NotNil(t, cs.MaxAge) + assert.Equal(t, 0*time.Second, cs.MaxAge.AsDuration()) + }) + + t.Run("max_age_is_clamped_to_cre_max", func(t *testing.T) { + tr, err := NewCRETransport(nil, "https://example.com", nil) + require.NoError(t, err) + + secs := int64(999999) + cfg := &CREHTTPCacheConfig{MaxAgeSeconds: &secs} + tr.ApplyHTTPCacheConfig(cfg) + + assert.Equal(t, maxHTTPCacheMaxAge, tr.httpCacheMaxAge) + + cs := tr.cacheSettingsForJSONRPC("user.call", []byte(`{}`)) + require.NotNil(t, cs) + require.NotNil(t, cs.MaxAge) + assert.Equal(t, maxHTTPCacheMaxAge, cs.MaxAge.AsDuration()) }) +} - t.Run("non_broadcast_is_not_cached", func(t *testing.T) { - tr := &CRETransport{} - assert.Nil(t, tr.cacheSettingsForJSONRPC("user.call", []byte(`{}`))) - assert.Nil(t, tr.cacheSettingsForJSONRPC("user.tx_query", []byte(`{}`))) - assert.Nil(t, tr.cacheSettingsForJSONRPC("kgw.authn", []byte(`{}`))) +func TestCRETransport_CacheSettingsForJSONRPC(t *testing.T) { + t.Run("all_methods_return_cache_settings", func(t *testing.T) { + tr, err := NewCRETransport(nil, "https://example.com", nil) + require.NoError(t, err) + + methods := []string{ + "user.call", + "user.tx_query", + "user.account", + "user.chain_info", + "kgw.authn_param", + "kgw.authn", + "user.broadcast", + } + + for _, m := range methods { + cs := tr.cacheSettingsForJSONRPC(m, []byte(`{}`)) + require.NotNil(t, cs, "method %s should return non-nil CacheSettings", m) + assert.Equal(t, defaultHTTPCacheStore, cs.Store, "method %s Store should match transport default", m) + require.NotNil(t, cs.MaxAge, "method %s should set MaxAge", m) + assert.Equal(t, defaultHTTPCacheMaxAge, cs.MaxAge.AsDuration(), "method %s MaxAge should match transport default", m) + } }) t.Run("paramsJSON_is_accepted", func(t *testing.T) { - // Policy currently ignores paramsJSON, but this test ensures we can pass - // arbitrary JSON without panicking and still get the expected outcome. - tr := &CRETransport{} - cs := tr.cacheSettingsForJSONRPC("user.broadcast", []byte(`{"nested":{"a":[1,2,3]}}`)) + tr, err := NewCRETransport(nil, "https://example.com", nil) + require.NoError(t, err) + + cs := tr.cacheSettingsForJSONRPC("user.call", []byte(`{"nested":{"a":[1,2,3]}}`)) require.NotNil(t, cs) - assert.Equal(t, defaultBroadcastCacheMaxAge, cs.MaxAge.AsDuration()) + require.NotNil(t, cs.MaxAge) + assert.Equal(t, defaultHTTPCacheMaxAge, cs.MaxAge.AsDuration()) + }) +} + +func TestCRETransport_NextReqID(t *testing.T) { + t.Run("deterministic_when_caching_active", func(t *testing.T) { + tr, err := NewCRETransport(nil, "https://example.com", nil) + require.NoError(t, err) + + params := []byte(`{"a":1}`) + id1 := tr.nextReqID("user.call", params) + id2 := tr.nextReqID("user.call", params) + + assert.Equal(t, id1, id2) + assert.True(t, stringsHasPrefix(id1, "tn:"), "expected deterministic id to have tn: prefix") + assert.Equal(t, 19, len(id1), "expected tn: + 16 hex chars") + + id3 := tr.nextReqID("user.call", []byte(`{"a":2}`)) + assert.NotEqual(t, id1, id3) + + // Different method -> different id + id4 := tr.nextReqID("user.tx_query", params) + assert.NotEqual(t, id1, id4) + }) + + t.Run("monotonic_sequence_when_caching_disabled", func(t *testing.T) { + // Construct minimal transport with caching disabled and fresh reqID counter. + tr := &CRETransport{ + httpCacheStore: false, + httpCacheMaxAge: 0, + } + + id1 := tr.nextReqID("user.call", []byte(`{}`)) + id2 := tr.nextReqID("user.call", []byte(`{}`)) + + assert.Equal(t, "1", id1) + assert.Equal(t, "2", id2) }) } @@ -219,8 +325,8 @@ func TestCRETransport_HTTPRequestsIncludeCacheSettings(t *testing.T) { targets := []struct { funcName string - expectFirstArgIsMethod bool // if true, expects first arg ident "method" - expectFirstArgString string // if non-empty, expects string literal + expectFirstArgIsMethod bool + expectFirstArgString string }{ {funcName: "doJSONRPC", expectFirstArgIsMethod: true}, {funcName: "doJSONRPCWithResponse", expectFirstArgIsMethod: true}, @@ -232,11 +338,9 @@ func TestCRETransport_HTTPRequestsIncludeCacheSettings(t *testing.T) { fd := findFuncDecl(t, f, tc.funcName) require.NotNil(t, fd, "function %s not found", tc.funcName) - // 1) Ensure we assign: cacheSettings := t.cacheSettingsForJSONRPC(, paramsJSON) assert.True(t, hasCacheSettingsAssignment(t, fd, tc.expectFirstArgIsMethod, tc.expectFirstArgString), "%s should assign cacheSettings := t.cacheSettingsForJSONRPC(..., paramsJSON)", tc.funcName) - // 2) Ensure &http.Request{..., CacheSettings: cacheSettings, ...} exists assert.True(t, hasHttpRequestWithCacheSettings(t, fd), "%s should set CacheSettings on http.Request literal", tc.funcName) }) @@ -268,7 +372,6 @@ func hasCacheSettingsAssignment(t *testing.T, fd *ast.FuncDecl, firstArgIsMethod return true } - // RHS must be t.cacheSettingsForJSONRPC(...) call, ok := as.Rhs[0].(*ast.CallExpr) if !ok { return true @@ -286,12 +389,9 @@ func hasCacheSettingsAssignment(t *testing.T, fd *ast.FuncDecl, firstArgIsMethod return true } - // Second arg must be paramsJSON ident if id2, ok := call.Args[1].(*ast.Ident); !ok || id2.Name != "paramsJSON" { return true } - - // First arg check if firstArgIsMethod { id1, ok := call.Args[0].(*ast.Ident) if !ok || id1.Name != "method" { @@ -320,7 +420,6 @@ func hasHttpRequestWithCacheSettings(t *testing.T, fd *ast.FuncDecl) bool { found := false ast.Inspect(fd, func(n ast.Node) bool { - // Look for: &http.Request{ ... CacheSettings: cacheSettings ... } ue, ok := n.(*ast.UnaryExpr) if !ok || ue.Op != token.AND { return true @@ -329,7 +428,6 @@ func hasHttpRequestWithCacheSettings(t *testing.T, fd *ast.FuncDecl) bool { if !ok { return true } - // Type must be http.Request se, ok := cl.Type.(*ast.SelectorExpr) if !ok { return true @@ -339,7 +437,6 @@ func hasHttpRequestWithCacheSettings(t *testing.T, fd *ast.FuncDecl) bool { return true } - // Must include CacheSettings: cacheSettings for _, elt := range cl.Elts { kv, ok := elt.(*ast.KeyValueExpr) if !ok { @@ -362,8 +459,9 @@ func hasHttpRequestWithCacheSettings(t *testing.T, fd *ast.FuncDecl) bool { return found } -func TestDefaultBroadcastCacheMaxAge(t *testing.T) { - // Defensive check: ensure the constant remains what the transport expects. - // If this is intentionally changed, update tests accordingly. - assert.Equal(t, 2*time.Minute, defaultBroadcastCacheMaxAge) +func stringsHasPrefix(s, prefix string) bool { + if len(prefix) > len(s) { + return false + } + return s[:len(prefix)] == prefix } From fdbbfca5ab42a4386d04bbba999aa66539f1bc3b Mon Sep 17 00:00:00 2001 From: bulkcade Date: Wed, 14 Jan 2026 09:38:27 +0000 Subject: [PATCH 07/13] compile deletion error correction --- core/tnclient/transport_cre_test.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/core/tnclient/transport_cre_test.go b/core/tnclient/transport_cre_test.go index e944010..61df7d5 100644 --- a/core/tnclient/transport_cre_test.go +++ b/core/tnclient/transport_cre_test.go @@ -21,7 +21,7 @@ import ( // See the examples/cre_integration/ directory for complete working examples. func TestNewCRETransport(t *testing.T) { - t.Run("constructor_exists", func(t *testi + t.Run("constructor_exists", func(t *testing.T) { assert.NotNil(t, NewCRETransport) }) @@ -259,16 +259,6 @@ func TestCRETransport_CacheSettingsForJSONRPC(t *testing.T) { assert.Equal(t, defaultHTTPCacheMaxAge, cs.MaxAge.AsDuration(), "method %s MaxAge should match transport default", m) } }) - - t.Run("paramsJSON_is_accepted", func(t *testing.T) { - tr, err := NewCRETransport(nil, "https://example.com", nil) - require.NoError(t, err) - - cs := tr.cacheSettingsForJSONRPC("user.call", []byte(`{"nested":{"a":[1,2,3]}}`)) - require.NotNil(t, cs) - require.NotNil(t, cs.MaxAge) - assert.Equal(t, defaultHTTPCacheMaxAge, cs.MaxAge.AsDuration()) - }) } func TestCRETransport_NextReqID(t *testing.T) { @@ -280,10 +270,12 @@ func TestCRETransport_NextReqID(t *testing.T) { id1 := tr.nextReqID("user.call", params) id2 := tr.nextReqID("user.call", params) + // Deterministic for identical (method, paramsJSON) assert.Equal(t, id1, id2) assert.True(t, stringsHasPrefix(id1, "tn:"), "expected deterministic id to have tn: prefix") assert.Equal(t, 19, len(id1), "expected tn: + 16 hex chars") + // Different params -> different id id3 := tr.nextReqID("user.call", []byte(`{"a":2}`)) assert.NotEqual(t, id1, id3) @@ -389,9 +381,12 @@ func hasCacheSettingsAssignment(t *testing.T, fd *ast.FuncDecl, firstArgIsMethod return true } + // Second arg must be paramsJSON ident if id2, ok := call.Args[1].(*ast.Ident); !ok || id2.Name != "paramsJSON" { return true } + + // First arg check if firstArgIsMethod { id1, ok := call.Args[0].(*ast.Ident) if !ok || id1.Name != "method" { @@ -428,6 +423,7 @@ func hasHttpRequestWithCacheSettings(t *testing.T, fd *ast.FuncDecl) bool { if !ok { return true } + se, ok := cl.Type.(*ast.SelectorExpr) if !ok { return true From b279ab97f045136b965e4d7420ce92963b0ba8d0 Mon Sep 17 00:00:00 2001 From: bulkcade Date: Wed, 14 Jan 2026 12:45:28 +0000 Subject: [PATCH 08/13] add option, remove redundant old logic function and comments --- core/tnclient/options_cre.go | 9 +++++++++ core/tnclient/transport_cre.go | 13 ------------- core/tnclient/transport_cre_test.go | 4 ---- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/core/tnclient/options_cre.go b/core/tnclient/options_cre.go index 14f73ab..0d244f1 100644 --- a/core/tnclient/options_cre.go +++ b/core/tnclient/options_cre.go @@ -107,3 +107,12 @@ func WithCRETransportAndSigner(runtime cre.NodeRuntime, endpoint string, signer c.transport, _ = NewCRETransport(runtime, endpoint, signer) } } + +func WithCRETransportAndSignerWithHTTPCache(runtime cre.NodeRuntime, endpoint string, signer auth.Signer, cacheCfg *CREHTTPCacheConfig) Option { + return func(c *Client) { + // Set signer first + c.signer = signer + // Then create CRE transport with the signer and HTTP cache + c.transport, _ = NewCRETransportWithHTTPCache(runtime, endpoint, signer, cacheCfg) + } +} diff --git a/core/tnclient/transport_cre.go b/core/tnclient/transport_cre.go index 1d8337a..4a2c4ce 100644 --- a/core/tnclient/transport_cre.go +++ b/core/tnclient/transport_cre.go @@ -146,19 +146,6 @@ func (t *CRETransport) ApplyHTTPCacheConfig(cfg *CREHTTPCacheConfig) { } } -func shouldUseDeterministicID(cs *http.CacheSettings) bool { - if cs == nil { - return false - } - if cs.Store { - return true - } - if cs.MaxAge != nil && cs.MaxAge.AsDuration() > 0 { - return true - } - return false -} - func (t *CRETransport) nextReqID(method string, paramsJSON []byte) string { // Cache is "active" if we store or we attempt cache reads (MaxAge > 0). cacheActive := t.httpCacheStore || (t.httpCacheMaxAge > 0) diff --git a/core/tnclient/transport_cre_test.go b/core/tnclient/transport_cre_test.go index 61df7d5..00990f8 100644 --- a/core/tnclient/transport_cre_test.go +++ b/core/tnclient/transport_cre_test.go @@ -270,12 +270,10 @@ func TestCRETransport_NextReqID(t *testing.T) { id1 := tr.nextReqID("user.call", params) id2 := tr.nextReqID("user.call", params) - // Deterministic for identical (method, paramsJSON) assert.Equal(t, id1, id2) assert.True(t, stringsHasPrefix(id1, "tn:"), "expected deterministic id to have tn: prefix") assert.Equal(t, 19, len(id1), "expected tn: + 16 hex chars") - // Different params -> different id id3 := tr.nextReqID("user.call", []byte(`{"a":2}`)) assert.NotEqual(t, id1, id3) @@ -381,12 +379,10 @@ func hasCacheSettingsAssignment(t *testing.T, fd *ast.FuncDecl, firstArgIsMethod return true } - // Second arg must be paramsJSON ident if id2, ok := call.Args[1].(*ast.Ident); !ok || id2.Name != "paramsJSON" { return true } - // First arg check if firstArgIsMethod { id1, ok := call.Args[0].(*ast.Ident) if !ok || id1.Name != "method" { From 59e928e3ddb476369598a601a207e21c3f356c10 Mon Sep 17 00:00:00 2001 From: bulkcade Date: Wed, 14 Jan 2026 13:36:15 +0000 Subject: [PATCH 09/13] comment fix --- core/tnclient/transport_cre.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/tnclient/transport_cre.go b/core/tnclient/transport_cre.go index 4a2c4ce..9dec417 100644 --- a/core/tnclient/transport_cre.go +++ b/core/tnclient/transport_cre.go @@ -782,7 +782,8 @@ func (t *CRETransport) Signer() auth.Signer { return t.signer } -// authenticate performs gateway authentication and stores the cookie.// This is called automatically when a 401 error is received. +// authenticate performs gateway authentication and stores the cookie. +// This is called automatically when a 401 error is received. func (t *CRETransport) authenticate(ctx context.Context) error { if t.signer == nil { return fmt.Errorf("cannot authenticate without a signer") From a62a37f8ed5ef1de3536dc6727d3c8172dd506aa Mon Sep 17 00:00:00 2001 From: bulkcade Date: Wed, 14 Jan 2026 13:37:07 +0000 Subject: [PATCH 10/13] comment fix --- core/tnclient/transport_cre.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/tnclient/transport_cre.go b/core/tnclient/transport_cre.go index 9dec417..a75deba 100644 --- a/core/tnclient/transport_cre.go +++ b/core/tnclient/transport_cre.go @@ -915,6 +915,9 @@ func (t *CRETransport) doJSONRPCWithResponse(ctx context.Context, method string, } // composeGatewayAuthMessage composes a SIWE-like authentication message. +// This matches the format used by kwil-db gateway client. +// Note: This is a custom format, not standard SIWE - it omits the account address line +// and uses "Issue At" instead of "Issued At" to match kgw's expectations. func composeGatewayAuthMessage(param *gateway.AuthnParameterResponse, domain string, uri string, version string, chainID string) string { var msg bytes.Buffer msg.WriteString(domain + " wants you to sign in with your account:\n") From 7cf5c1c50605d56dcb404b64ad5dd20d29143636 Mon Sep 17 00:00:00 2001 From: bulkcade Date: Thu, 15 Jan 2026 13:22:07 +0000 Subject: [PATCH 11/13] reinstate old test comments to reduce dif --- core/tnclient/transport_cre_test.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/core/tnclient/transport_cre_test.go b/core/tnclient/transport_cre_test.go index 00990f8..4ce592a 100644 --- a/core/tnclient/transport_cre_test.go +++ b/core/tnclient/transport_cre_test.go @@ -21,7 +21,15 @@ import ( // See the examples/cre_integration/ directory for complete working examples. func TestNewCRETransport(t *testing.T) { + // Note: We cannot create a real NodeRuntime outside of CRE environment, + // so this test just verifies the function signature and basic structure. + t.Run("constructor_exists", func(t *testing.T) { + // This test just verifies that the NewCRETransport function exists + // and has the expected signature. + // Actual testing requires CRE simulation environment. + + // Verify the function is not nil assert.NotNil(t, NewCRETransport) }) @@ -46,31 +54,42 @@ func TestNewCRETransport(t *testing.T) { } func TestCRETransport_Implements_Transport_Interface(t *testing.T) { + // This compile-time check verifies that CRETransport implements Transport + // The var _ Transport = (*CRETransport)(nil) line in transport_cre.go + // ensures this at compile time, but we include this test for documentation. t.Run("implements_interface", func(t *testing.T) { + // If this compiles, the interface is implemented var _ Transport = (*CRETransport)(nil) }) } func TestWithCRETransport(t *testing.T) { t.Run("option_exists", func(t *testing.T) { + // Verify the WithCRETransport option function exists assert.NotNil(t, WithCRETransport) }) t.Run("option_signature", func(t *testing.T) { + // Verify the function returns an Option + // This test documents the expected signature var _ Option = WithCRETransport(nil, "http://example.com") }) } func TestWithCRETransportAndSigner(t *testing.T) { t.Run("option_exists", func(t *testing.T) { + // Verify the WithCRETransportAndSigner option function exists assert.NotNil(t, WithCRETransportAndSigner) }) t.Run("option_signature", func(t *testing.T) { + // Verify the function returns an Option var _ Option = WithCRETransportAndSigner(nil, "http://example.com", nil) }) } +// Unit tests for error classification + func TestIsTransientTxError(t *testing.T) { tests := []struct { name string @@ -94,7 +113,7 @@ func TestIsTransientTxError(t *testing.T) { name: "Multi-word message with code", err: fmt.Errorf("JSON-RPC error: transaction not found in mempool or ledger (code: -202)"), want: true, - reasoning: "Regex should handle multi-word messages", + reasoning: "Regex should handle multi-word messages (fixed from %*s limitation)", }, { name: "ErrorTimeout code", From f587321b4dd08cab346aa87daf456a03e3ae60c9 Mon Sep 17 00:00:00 2001 From: bulkcade <108339627+bulkcade@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:25:52 +0000 Subject: [PATCH 12/13] Update core/tnclient/transport_cre.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- core/tnclient/transport_cre.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/tnclient/transport_cre.go b/core/tnclient/transport_cre.go index a75deba..b1907a3 100644 --- a/core/tnclient/transport_cre.go +++ b/core/tnclient/transport_cre.go @@ -189,7 +189,7 @@ type CREHTTPCacheConfig struct { func (t *CRETransport) cacheSettingsForJSONRPC(method string, paramsJSON []byte) *http.CacheSettings { // Always attach cache settings so every CRE HTTP request participates. // Note: per CRE docs, if MaxAge is nil/zero it won't read from cache, - // but may still store if Store is true. :contentReference[oaicite:2]{index=2} + // but may still store if Store is true. return &http.CacheSettings{ Store: t.httpCacheStore, MaxAge: durationpb.New(t.httpCacheMaxAge), From 7d57c7d7300ca462efead38231bf1ebaa7eaa262 Mon Sep 17 00:00:00 2001 From: christian harrington Date: Fri, 16 Jan 2026 11:30:19 +0000 Subject: [PATCH 13/13] add comment for option --- core/tnclient/options_cre.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/tnclient/options_cre.go b/core/tnclient/options_cre.go index 0d244f1..42e97c0 100644 --- a/core/tnclient/options_cre.go +++ b/core/tnclient/options_cre.go @@ -108,6 +108,19 @@ func WithCRETransportAndSigner(runtime cre.NodeRuntime, endpoint string, signer } } +// WithCRETransportAndSignerWithHTTPCache is a convenience function that combines +// WithSigner and a CRE transport configured with an HTTP cache in the correct +// order. +// +// This ensures the signer is set before creating the CRE transport, which is +// necessary for write operations. The provided cacheCfg controls the HTTP cache +// behavior used by the underlying CRE transport. +// +// Example: +// +// client, err := tnclient.NewClient(ctx, endpoint, +// tnclient.WithCRETransportAndSignerWithHTTPCache(nodeRuntime, endpoint, signer, cacheCfg), +// ) func WithCRETransportAndSignerWithHTTPCache(runtime cre.NodeRuntime, endpoint string, signer auth.Signer, cacheCfg *CREHTTPCacheConfig) Option { return func(c *Client) { // Set signer first