-
Notifications
You must be signed in to change notification settings - Fork 3
docs: view Chainlink CRE usage examples and documentation #168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,3 +30,6 @@ go.work | |
| gitignore | ||
|
|
||
| .env | ||
|
|
||
| # CRE workflow build artifacts | ||
| *.wasm | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import ( | |
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "math/big" | ||
| "net/url" | ||
| "regexp" | ||
| "strconv" | ||
|
|
@@ -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 | ||
|
|
@@ -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) | ||
|
|
@@ -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) | ||
| } | ||
|
|
||
|
|
@@ -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") | ||
| } | ||
|
|
||
| // 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 { | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 -20Repository: 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.goRepository: 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 -50Repository: 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 ( 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) | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: trufnetwork/sdk-go
Length of output: 1076
🏁 Script executed:
Repository: trufnetwork/sdk-go
Length of output: 857
🏁 Script executed:
Repository: trufnetwork/sdk-go
Length of output: 6712
🏁 Script executed:
Repository: trufnetwork/sdk-go
Length of output: 231
🏁 Script executed:
Repository: trufnetwork/sdk-go
Length of output: 872
🏁 Script executed:
Repository: trufnetwork/sdk-go
Length of output: 94
🏁 Script executed:
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-202and-32001codes handled inisTransientTxErrorat lines 586–591). If available, use it instead of string matching.🤖 Prompt for AI Agents