Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ go.work
gitignore

.env

# CRE workflow build artifacts
*.wasm
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ if gwClient := client.GetKwilClient(); gwClient != nil {

> **Note**: For most use cases, prefer the high-level Client methods (`ListStreams`, `DeployStream`, etc.) which are transport-agnostic and work with any transport implementation.

## Using with Chainlink Runtime Environment (CRE)

The TRUF.NETWORK SDK supports [Chainlink Runtime Environment (CRE)](https://docs.chain.link/cre) for building decentralized workflows with consensus-backed data retrieval.

**Key features:**
- Decentralized data access with DON consensus
- Full CRUD operations (read/write)
- WASM-based secure execution

**For complete documentation:**
- 📖 [CRE Integration Guide](docs/CRE_INTEGRATION.md) - Setup, API reference, and examples
- 🎯 [Working Demo](examples/truf-cre-demo/) - Complete CRUD lifecycle example

## Local Node Testing

### Setting Up a Local Node
Expand Down
183 changes: 171 additions & 12 deletions core/tnclient/transport_cre.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"fmt"
"math/big"
"net/url"
"regexp"
"strconv"
Expand Down Expand Up @@ -63,6 +64,9 @@ type CRETransport struct {
reqID atomic.Uint64
authCookie string // Cookie value for gateway authentication
authCookieMu sync.RWMutex
currentNonce int64 // Track nonce for sequential transactions
nonceMu sync.Mutex
nonceFetched bool
}

// Verify CRETransport implements Transport interface at compile time
Expand Down Expand Up @@ -117,7 +121,7 @@ func (t *CRETransport) callJSONRPC(ctx context.Context, method string, params an
// If we get a 401, try authenticating and retry once
if err != nil && strings.Contains(err.Error(), "401") {
if t.signer == nil {
return fmt.Errorf("%w [DEBUG: signer is nil, cannot authenticate]", err)
return fmt.Errorf("%w: signer is nil, cannot authenticate", err)
}
// Authenticate with gateway
authErr := t.authenticate(ctx)
Expand Down Expand Up @@ -192,6 +196,19 @@ func (t *CRETransport) doJSONRPC(ctx context.Context, method string, params any,

// Check for JSON-RPC errors
if rpcResp.Error != nil {
// For broadcast errors (-201), decode the BroadcastError details
if rpcResp.Error.Code == -201 && len(rpcResp.Error.Data) > 0 {
var broadcastErr struct {
Code uint32 `json:"code"`
Hash string `json:"hash"`
Message string `json:"message"`
}
if err := json.Unmarshal(rpcResp.Error.Data, &broadcastErr); err == nil {
return fmt.Errorf("JSON-RPC error: %s (code: %d) [Broadcast: code=%d, hash=%s, msg=%s]",
rpcResp.Error.Message, rpcResp.Error.Code,
broadcastErr.Code, broadcastErr.Hash, broadcastErr.Message)
}
}
return fmt.Errorf("JSON-RPC error: %s (code: %d)", rpcResp.Error.Message, rpcResp.Error.Code)
}

Expand Down Expand Up @@ -259,12 +276,35 @@ func (t *CRETransport) Call(ctx context.Context, namespace string, action string
//
// This method builds a signed transaction and broadcasts it to the TRUF.NETWORK.
// The transaction is signed using the configured signer and executed within CRE's
// consensus mechanism.
// consensus mechanism. Automatically retries on nonce errors.
func (t *CRETransport) Execute(ctx context.Context, namespace string, action string, inputs [][]any, opts ...clientType.TxOpt) (types.Hash, error) {
if t.signer == nil {
return types.Hash{}, fmt.Errorf("signer required for Execute operations")
}

// Retry loop for nonce errors
const maxRetries = 3
for attempt := 0; attempt < maxRetries; attempt++ {
txHash, err := t.executeOnce(ctx, namespace, action, inputs, opts...)
if err != nil {
// Check if it's a nonce error
if strings.Contains(err.Error(), "invalid nonce") && attempt < maxRetries-1 {
// Reset nonce tracking to refetch on next attempt
t.nonceMu.Lock()
t.nonceFetched = false
t.nonceMu.Unlock()
continue // Retry
}
return types.Hash{}, err
}
return txHash, nil
}

return types.Hash{}, fmt.Errorf("max retries exceeded")
Comment on lines +285 to +303
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the file exists and check the code at the specified lines
cat -n core/tnclient/transport_cre.go | sed -n '280,305p'

Repository: trufnetwork/sdk-go

Length of output: 1076


🏁 Script executed:

# Also check the isTransientTxError function mentioned at lines 586-591
cat -n core/tnclient/transport_cre.go | sed -n '580,600p'

Repository: trufnetwork/sdk-go

Length of output: 857


🏁 Script executed:

# Search for nonce error handling patterns and structured error codes in the codebase
rg -n -C3 --type go 'nonce|transient.*error|error.*code' core/tnclient/ | head -100

Repository: trufnetwork/sdk-go

Length of output: 6712


🏁 Script executed:

# Look for error type definitions or error handling patterns related to gateway API
fd --type go -x grep -l 'error\|Error' core/tnclient/ | head -20

Repository: trufnetwork/sdk-go

Length of output: 231


🏁 Script executed:

# Find jsonrpc error code definitions
rg -n --type go 'ErrorCode|ErrorTx|ErrorTimeout|ErrorNonce|error.*nonce' --max-count 50 core/tnclient/

Repository: trufnetwork/sdk-go

Length of output: 872


🏁 Script executed:

# Search for gateway API documentation or error type definitions
find . -name '*.go' -type f | xargs grep -l 'jsonrpc\|ErrorCode' | head -10

Repository: trufnetwork/sdk-go

Length of output: 94


🏁 Script executed:

# Look for comments or error message patterns related to nonce
rg -n 'invalid nonce|nonce.*error|error.*nonce' --type go

Repository: trufnetwork/sdk-go

Length of output: 411


Fragile error detection for nonce errors.

The retry logic uses substring matching strings.Contains(err.Error(), "invalid nonce") to detect nonce errors. This is fragile—if the gateway changes the error message format, retries will fail silently. Consider checking if the gateway API provides a structured error code for nonce errors (similar to the -202 and -32001 codes handled in isTransientTxError at lines 586–591). If available, use it instead of string matching.

🤖 Prompt for AI Agents
In core/tnclient/transport_cre.go around lines 285 to 303, the retry loop
currently detects nonce errors by substring-matching the error message ("invalid
nonce"); replace this fragile check with structured error-code detection:
inspect the returned error for the gateway/json-rpc error type (or unwrap to the
underlying error) and compare its numeric error code against the known
nonce-related codes used elsewhere (reuse the same checks as isTransientTxError
or add a helper like isNonceError that checks for the specific codes), and only
when that code is present perform the nonceMu reset and retry; otherwise return
the error. Ensure you handle nil unwraps and preserve existing behavior of
limiting retries to maxRetries.

}

// executeOnce performs a single execute attempt (internal helper)
func (t *CRETransport) executeOnce(ctx context.Context, namespace string, action string, inputs [][]any, opts ...clientType.TxOpt) (types.Hash, error) {
// Convert inputs to EncodedValue arrays
var encodedInputs [][]*types.EncodedValue
for _, inputRow := range inputs {
Expand Down Expand Up @@ -298,6 +338,51 @@ func (t *CRETransport) Execute(ctx context.Context, namespace string, action str
opt(txOpts)
}

// Auto-manage nonce if not explicitly provided
if txOpts.Nonce == 0 {
t.nonceMu.Lock()

// Fetch nonce from gateway on first transaction only
if !t.nonceFetched {
// Create AccountID from signer
acctID := &types.AccountID{
Identifier: t.signer.CompactID(),
KeyType: t.signer.PubKey().Type(),
}

// Fetch account info via user.account RPC call
params := map[string]any{
"id": acctID,
}

var accountResp struct {
ID *types.AccountID `json:"id"`
Balance string `json:"balance"`
Nonce int64 `json:"nonce"`
}

err := t.callJSONRPC(ctx, "user.account", params, &accountResp)
if err != nil {
// If account doesn't exist yet, start with nonce 0
if !strings.Contains(err.Error(), "not found") && !strings.Contains(err.Error(), "does not exist") {
t.nonceMu.Unlock()
return types.Hash{}, fmt.Errorf("failed to fetch account nonce: %w", err)
}
t.currentNonce = 0
} else {
// Account nonce is the LAST used nonce, so NEXT nonce is nonce+1
t.currentNonce = accountResp.Nonce + 1
}
t.nonceFetched = true
}

// Use current nonce and increment
txOpts.Nonce = t.currentNonce
t.currentNonce++

t.nonceMu.Unlock()
}
Comment on lines +341 to +384
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Search for other error handling patterns with callJSONRPC
echo "=== Error handling patterns with callJSONRPC ==="
rg -n -B2 -A5 'callJSONRPC|err\.Error' core/tnclient/transport_cre.go | head -100

# Look for error type definitions
echo -e "\n=== Error type definitions ==="
rg -n 'type.*Error|error.*struct|ErrAccountNotFound|ErrNotFound' --type go core/tnclient/ | head -50

# Check for similar error string matching patterns
echo -e "\n=== String matching for errors in RPC context ==="
rg -n 'strings\.Contains.*Error|err.*not found|err.*does not exist' --type go core/tnclient/ | head -50

# Look at gateway client implementation
echo -e "\n=== Gateway interaction code ==="
find core/tnclient -name '*.go' -type f | xargs wc -l | sort -n | tail -20

Repository: trufnetwork/sdk-go

Length of output: 6893


🏁 Script executed:

#!/bin/bash

# Get more context around the error handling in transport_cre.go
echo "=== Full context of user.account error handling ==="
sed -n '365,375p' core/tnclient/transport_cre.go

# Search for how errors are returned from gateway RPC
echo -e "\n=== Other RPC calls and their error handling ==="
rg -n -B3 -A8 'callJSONRPC\(ctx' core/tnclient/transport_cre.go | head -150

# Check if there's a gateway error response structure
echo -e "\n=== Response structures in transport ==="
ast-grep --pattern 'type $_ struct {
  $$$
  Error $_ `$_`
  $$$
}'

Repository: trufnetwork/sdk-go

Length of output: 6184


🏁 Script executed:

#!/bin/bash

# Search for account error handling in related test files or docs
echo "=== Test files for user.account handling ==="
find . -name '*test*.go' -type f | xargs grep -l 'user\.account' 2>/dev/null || echo "No test files found"

# Look for any gateway documentation or error constants
echo -e "\n=== Error constants or patterns ==="
rg -n 'account.*not.*found|does.*not.*exist' --type go --type md | head -30

# Check the overall structure of the transport client
echo -e "\n=== Transport client structure ==="
head -100 core/tnclient/transport_cre.go

Repository: trufnetwork/sdk-go

Length of output: 3750


🏁 Script executed:

#!/bin/bash

# Search for error code constants or definitions
echo "=== Looking for error code definitions ==="
rg -n 'code.*int|ErrorCode|error.*code' --type go core/tnclient/ | head -40

# Check if there's a types file defining error codes
echo -e "\n=== Checking types package ==="
find . -name 'types.go' -o -name 'error*.go' | head -10

# Look for account error handling in gateway package or types
echo -e "\n=== Account-related types ==="
rg -n 'type.*Account|account.*error' --type go | head -30

# Search for JSON-RPC error code patterns in gateway responses
echo -e "\n=== JSON-RPC error response structures ==="
rg -n 'type.*Error|Error.*struct' --type go core/tnclient/transport_cre.go -A 5 | head -50

Repository: trufnetwork/sdk-go

Length of output: 4508


🏁 Script executed:

#!/bin/bash

# Check the doJSONRPC function to see full error structure
echo "=== doJSONRPC error handling ==="
sed -n '460,520p' core/tnclient/transport_cre.go

# Look for JSON-RPC response structure definition
echo -e "\n=== JSON-RPC response structure ==="
ast-grep --pattern 'type $_ struct {
  Error struct {
    $$$
  }
  $$$
}'

Repository: trufnetwork/sdk-go

Length of output: 2097


Use error codes instead of string matching for account lookup failures.

Line 367 uses fragile string matching ("not found", "does not exist") to detect non-existent accounts. JSON-RPC error responses include structured error codes, which are reliably extracted elsewhere in the codebase (line 583). The codebase comment at line 565 explicitly acknowledges this: "Consider adding structured error codes to the gateway API for better reliability."

The mutex locking correctly prevents race conditions during nonce fetching and incrementing.


// Ensure chain ID is fetched before building transaction
// This prevents transactions with empty chain IDs
// Check if already initialized (read lock)
Expand All @@ -322,33 +407,108 @@ func (t *CRETransport) Execute(ctx context.Context, namespace string, action str
t.chainIDMu.Unlock()
}

// Ensure Fee is not nil to prevent signature verification mismatch
// When Fee is nil, SerializeMsg produces "Fee: <nil>" but after JSON
// marshaling/unmarshaling it becomes "Fee: 0", causing signature mismatch
fee := txOpts.Fee
if fee == nil {
fee = big.NewInt(0)
}

// Build unsigned transaction
tx := &types.Transaction{
Body: &types.TransactionBody{
Payload: payloadBytes,
PayloadType: payload.Type(),
Fee: txOpts.Fee,
Fee: fee,
Nonce: uint64(txOpts.Nonce),
ChainID: chainID,
},
Serialization: types.DefaultSignedMsgSerType, // Required for EthPersonalSigner
}

// Sign transaction
if err := tx.Sign(t.signer); err != nil {
return types.Hash{}, fmt.Errorf("failed to sign transaction: %w", err)
}

// Broadcast transaction
params := map[string]any{
"tx": tx,
// Pre-serialize transaction to avoid WASM pointer corruption
// Go WASM uses 64-bit pointers but WASM runtime uses 32-bit pointers.
// Transaction struct contains pointer fields (Signature, Body) which get
// corrupted when crossing the WASM boundary (golang/go#59156, golang/go#66984).
// Solution: Manually construct JSON-RPC request to avoid struct traversal in WASM.
txJSON, err := json.Marshal(tx)
if err != nil {
return types.Hash{}, fmt.Errorf("failed to marshal transaction: %w", err)
}

// Manually construct JSON-RPC request to bypass params map
reqID := t.nextReqID()
rpcReqJSON := fmt.Sprintf(
`{"jsonrpc":"2.0","id":"%s","method":"user.broadcast","params":{"tx":%s}}`,
reqID, string(txJSON))

// Create headers
headers := map[string]string{
"Content-Type": "application/json",
}

// Add auth cookie if we have one
t.authCookieMu.RLock()
if t.authCookie != "" {
headers["Cookie"] = t.authCookie
}
t.authCookieMu.RUnlock()

// Create CRE HTTP request
httpReq := &http.Request{
Url: t.endpoint,
Method: "POST",
Body: []byte(rpcReqJSON),
Headers: headers,
}

// Execute via CRE client
httpResp, err := t.client.SendRequest(t.runtime, httpReq).Await()
if err != nil {
return types.Hash{}, fmt.Errorf("CRE HTTP request failed: %w", err)
}

// Check HTTP status
if httpResp.StatusCode != 200 {
return types.Hash{}, fmt.Errorf("unexpected HTTP status code: %d", httpResp.StatusCode)
}

// Parse JSON-RPC response
var rpcResp jsonrpc.Response
if err := json.Unmarshal(httpResp.Body, &rpcResp); err != nil {
return types.Hash{}, fmt.Errorf("failed to unmarshal JSON-RPC response: %w", err)
}

// Check for JSON-RPC errors
if rpcResp.Error != nil {
// For broadcast errors (-201), decode the BroadcastError details
if rpcResp.Error.Code == -201 && len(rpcResp.Error.Data) > 0 {
var broadcastErr struct {
Code uint32 `json:"code"`
Hash string `json:"hash"`
Message string `json:"message"`
}
if err := json.Unmarshal(rpcResp.Error.Data, &broadcastErr); err == nil {
return types.Hash{}, fmt.Errorf("JSON-RPC error: %s (code: %d) [Broadcast: code=%d, hash=%s, msg=%s]",
rpcResp.Error.Message, rpcResp.Error.Code,
broadcastErr.Code, broadcastErr.Hash, broadcastErr.Message)
}
}
return types.Hash{}, fmt.Errorf("JSON-RPC error: %s (code: %d)", rpcResp.Error.Message, rpcResp.Error.Code)
}

// Unmarshal result
var result struct {
TxHash types.Hash `json:"tx_hash"`
}

if err := t.callJSONRPC(ctx, "user.broadcast", params, &result); err != nil {
return types.Hash{}, err
if err := json.Unmarshal(rpcResp.Result, &result); err != nil {
return types.Hash{}, fmt.Errorf("failed to unmarshal result: %w", err)
}

return result.TxHash, nil
Expand Down Expand Up @@ -570,8 +730,7 @@ func (t *CRETransport) authenticate(ctx context.Context) error {
// Make the auth request and capture the response headers
authResp, err := t.doJSONRPCWithResponse(ctx, string(gateway.MethodAuthn), authReq)
if err != nil {
return fmt.Errorf("kgw.authn request failed: %w [DEBUG: sender=%x, nonce=%s]",
err, authReq.Sender, authReq.Nonce)
return fmt.Errorf("kgw.authn request failed: %w", err)
}

// Extract Set-Cookie header from response
Expand All @@ -593,7 +752,7 @@ func (t *CRETransport) authenticate(ctx context.Context) error {
t.authCookieMu.Unlock()
}
} else {
return fmt.Errorf("no Set-Cookie header in kgw.authn response [DEBUG: headers=%+v]", authResp)
return fmt.Errorf("no Set-Cookie header in kgw.authn response")
}

return nil
Expand Down
Loading
Loading