Skip to content

Commit fcf776f

Browse files
committed
docs: view Chainlink CRE usage examples and documentation
1 parent dc627bd commit fcf776f

File tree

24 files changed

+1800
-10
lines changed

24 files changed

+1800
-10
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ go.work
3030
gitignore
3131

3232
.env
33+
34+
# CRE workflow build artifacts
35+
*.wasm

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ if gwClient := client.GetKwilClient(); gwClient != nil {
6060

6161
> **Note**: For most use cases, prefer the high-level Client methods (`ListStreams`, `DeployStream`, etc.) which are transport-agnostic and work with any transport implementation.
6262
63+
## Using with Chainlink Runtime Environment (CRE)
64+
65+
The TRUF.NETWORK SDK supports [Chainlink Runtime Environment (CRE)](https://docs.chain.link/cre) for building decentralized workflows with consensus-backed data retrieval.
66+
67+
**Key features:**
68+
- Decentralized data access with DON consensus
69+
- Full CRUD operations (read/write)
70+
- WASM-based secure execution
71+
72+
**For complete documentation:**
73+
- 📖 [CRE Integration Guide](docs/CRE_INTEGRATION.md) - Setup, API reference, and examples
74+
- 🎯 [Working Demo](examples/truf-cre-demo/) - Complete CRUD lifecycle example
75+
6376
## Local Node Testing
6477

6578
### Setting Up a Local Node

core/tnclient/transport_cre.go

Lines changed: 176 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"encoding/json"
99
"fmt"
10+
"math/big"
1011
"net/url"
1112
"regexp"
1213
"strconv"
@@ -63,6 +64,9 @@ type CRETransport struct {
6364
reqID atomic.Uint64
6465
authCookie string // Cookie value for gateway authentication
6566
authCookieMu sync.RWMutex
67+
currentNonce int64 // Track nonce for sequential transactions
68+
nonceMu sync.Mutex
69+
nonceFetched bool
6670
}
6771

6872
// Verify CRETransport implements Transport interface at compile time
@@ -192,6 +196,19 @@ func (t *CRETransport) doJSONRPC(ctx context.Context, method string, params any,
192196

193197
// Check for JSON-RPC errors
194198
if rpcResp.Error != nil {
199+
// For broadcast errors (-201), decode the BroadcastError details
200+
if rpcResp.Error.Code == -201 && len(rpcResp.Error.Data) > 0 {
201+
var broadcastErr struct {
202+
Code uint32 `json:"code"`
203+
Hash string `json:"hash"`
204+
Message string `json:"message"`
205+
}
206+
if err := json.Unmarshal(rpcResp.Error.Data, &broadcastErr); err == nil {
207+
return fmt.Errorf("JSON-RPC error: %s (code: %d) [Broadcast: code=%d, hash=%s, msg=%s]",
208+
rpcResp.Error.Message, rpcResp.Error.Code,
209+
broadcastErr.Code, broadcastErr.Hash, broadcastErr.Message)
210+
}
211+
}
195212
return fmt.Errorf("JSON-RPC error: %s (code: %d)", rpcResp.Error.Message, rpcResp.Error.Code)
196213
}
197214

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

285+
// Retry loop for nonce errors
286+
const maxRetries = 3
287+
for attempt := 0; attempt < maxRetries; attempt++ {
288+
fmt.Printf("[DEBUG] Execute attempt %d/%d for action=%s\n", attempt+1, maxRetries, action)
289+
txHash, err := t.executeOnce(ctx, namespace, action, inputs, opts...)
290+
if err != nil {
291+
fmt.Printf("[DEBUG] Execute error on attempt %d: %v\n", attempt+1, err)
292+
// Check if it's a nonce error
293+
if strings.Contains(err.Error(), "invalid nonce") && attempt < maxRetries-1 {
294+
fmt.Printf("[DEBUG] Nonce error detected, resetting nonceFetched and retrying\n")
295+
// Reset nonce tracking to refetch on next attempt
296+
t.nonceMu.Lock()
297+
t.nonceFetched = false
298+
t.nonceMu.Unlock()
299+
continue // Retry
300+
}
301+
return types.Hash{}, err
302+
}
303+
return txHash, nil
304+
}
305+
306+
return types.Hash{}, fmt.Errorf("max retries exceeded")
307+
}
308+
309+
// executeOnce performs a single execute attempt (internal helper)
310+
func (t *CRETransport) executeOnce(ctx context.Context, namespace string, action string, inputs [][]any, opts ...clientType.TxOpt) (types.Hash, error) {
311+
fmt.Printf("[DEBUG] executeOnce called: action=%s\n", action)
312+
268313
// Convert inputs to EncodedValue arrays
269314
var encodedInputs [][]*types.EncodedValue
270315
for _, inputRow := range inputs {
@@ -298,6 +343,54 @@ func (t *CRETransport) Execute(ctx context.Context, namespace string, action str
298343
opt(txOpts)
299344
}
300345

346+
// Auto-manage nonce if not explicitly provided
347+
if txOpts.Nonce == 0 {
348+
t.nonceMu.Lock()
349+
350+
// Fetch nonce from gateway on first transaction only
351+
if !t.nonceFetched {
352+
// Create AccountID from signer
353+
acctID := &types.AccountID{
354+
Identifier: t.signer.CompactID(),
355+
KeyType: t.signer.PubKey().Type(),
356+
}
357+
358+
// Fetch account info via user.account RPC call
359+
params := map[string]any{
360+
"id": acctID,
361+
}
362+
363+
var accountResp struct {
364+
ID *types.AccountID `json:"id"`
365+
Balance string `json:"balance"`
366+
Nonce int64 `json:"nonce"`
367+
}
368+
369+
err := t.callJSONRPC(ctx, "user.account", params, &accountResp)
370+
if err != nil {
371+
// If account doesn't exist yet, start with nonce 0
372+
if !strings.Contains(err.Error(), "not found") && !strings.Contains(err.Error(), "does not exist") {
373+
t.nonceMu.Unlock()
374+
return types.Hash{}, fmt.Errorf("failed to fetch account nonce: %w", err)
375+
}
376+
t.currentNonce = 0
377+
fmt.Printf("[DEBUG] Account not found, starting with nonce=0\n")
378+
} else {
379+
// Account nonce is the LAST used nonce, so NEXT nonce is nonce+1
380+
t.currentNonce = accountResp.Nonce + 1
381+
fmt.Printf("[DEBUG] Fetched account nonce=%d, using next nonce=%d\n", accountResp.Nonce, t.currentNonce)
382+
}
383+
t.nonceFetched = true
384+
}
385+
386+
// Use current nonce and increment
387+
txOpts.Nonce = t.currentNonce
388+
fmt.Printf("[DEBUG] Using nonce=%d for transaction\n", t.currentNonce)
389+
t.currentNonce++
390+
391+
t.nonceMu.Unlock()
392+
}
393+
301394
// Ensure chain ID is fetched before building transaction
302395
// This prevents transactions with empty chain IDs
303396
// Check if already initialized (read lock)
@@ -322,33 +415,108 @@ func (t *CRETransport) Execute(ctx context.Context, namespace string, action str
322415
t.chainIDMu.Unlock()
323416
}
324417

418+
// Ensure Fee is not nil to prevent signature verification mismatch
419+
// When Fee is nil, SerializeMsg produces "Fee: <nil>" but after JSON
420+
// marshaling/unmarshaling it becomes "Fee: 0", causing signature mismatch
421+
fee := txOpts.Fee
422+
if fee == nil {
423+
fee = big.NewInt(0)
424+
}
425+
325426
// Build unsigned transaction
326427
tx := &types.Transaction{
327428
Body: &types.TransactionBody{
328429
Payload: payloadBytes,
329430
PayloadType: payload.Type(),
330-
Fee: txOpts.Fee,
431+
Fee: fee,
331432
Nonce: uint64(txOpts.Nonce),
332433
ChainID: chainID,
333434
},
435+
Serialization: types.DefaultSignedMsgSerType, // Required for EthPersonalSigner
334436
}
335437

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

341-
// Broadcast transaction
342-
params := map[string]any{
343-
"tx": tx,
443+
// Pre-serialize transaction to avoid WASM pointer corruption
444+
// Go WASM uses 64-bit pointers but WASM runtime uses 32-bit pointers.
445+
// Transaction struct contains pointer fields (Signature, Body) which get
446+
// corrupted when crossing the WASM boundary (golang/go#59156, golang/go#66984).
447+
// Solution: Manually construct JSON-RPC request to avoid struct traversal in WASM.
448+
txJSON, err := json.Marshal(tx)
449+
if err != nil {
450+
return types.Hash{}, fmt.Errorf("failed to marshal transaction: %w", err)
451+
}
452+
453+
// Manually construct JSON-RPC request to bypass params map
454+
reqID := t.nextReqID()
455+
rpcReqJSON := fmt.Sprintf(
456+
`{"jsonrpc":"2.0","id":"%s","method":"user.broadcast","params":{"tx":%s}}`,
457+
reqID, string(txJSON))
458+
459+
// Create headers
460+
headers := map[string]string{
461+
"Content-Type": "application/json",
462+
}
463+
464+
// Add auth cookie if we have one
465+
t.authCookieMu.RLock()
466+
if t.authCookie != "" {
467+
headers["Cookie"] = t.authCookie
468+
}
469+
t.authCookieMu.RUnlock()
470+
471+
// Create CRE HTTP request
472+
httpReq := &http.Request{
473+
Url: t.endpoint,
474+
Method: "POST",
475+
Body: []byte(rpcReqJSON),
476+
Headers: headers,
477+
}
478+
479+
// Execute via CRE client
480+
httpResp, err := t.client.SendRequest(t.runtime, httpReq).Await()
481+
if err != nil {
482+
return types.Hash{}, fmt.Errorf("CRE HTTP request failed: %w", err)
483+
}
484+
485+
// Check HTTP status
486+
if httpResp.StatusCode != 200 {
487+
return types.Hash{}, fmt.Errorf("unexpected HTTP status code: %d", httpResp.StatusCode)
488+
}
489+
490+
// Parse JSON-RPC response
491+
var rpcResp jsonrpc.Response
492+
if err := json.Unmarshal(httpResp.Body, &rpcResp); err != nil {
493+
return types.Hash{}, fmt.Errorf("failed to unmarshal JSON-RPC response: %w", err)
494+
}
495+
496+
// Check for JSON-RPC errors
497+
if rpcResp.Error != nil {
498+
// For broadcast errors (-201), decode the BroadcastError details
499+
if rpcResp.Error.Code == -201 && len(rpcResp.Error.Data) > 0 {
500+
var broadcastErr struct {
501+
Code uint32 `json:"code"`
502+
Hash string `json:"hash"`
503+
Message string `json:"message"`
504+
}
505+
if err := json.Unmarshal(rpcResp.Error.Data, &broadcastErr); err == nil {
506+
return types.Hash{}, fmt.Errorf("JSON-RPC error: %s (code: %d) [Broadcast: code=%d, hash=%s, msg=%s]",
507+
rpcResp.Error.Message, rpcResp.Error.Code,
508+
broadcastErr.Code, broadcastErr.Hash, broadcastErr.Message)
509+
}
510+
}
511+
return types.Hash{}, fmt.Errorf("JSON-RPC error: %s (code: %d)", rpcResp.Error.Message, rpcResp.Error.Code)
344512
}
345513

514+
// Unmarshal result
346515
var result struct {
347516
TxHash types.Hash `json:"tx_hash"`
348517
}
349-
350-
if err := t.callJSONRPC(ctx, "user.broadcast", params, &result); err != nil {
351-
return types.Hash{}, err
518+
if err := json.Unmarshal(rpcResp.Result, &result); err != nil {
519+
return types.Hash{}, fmt.Errorf("failed to unmarshal result: %w", err)
352520
}
353521

354522
return result.TxHash, nil

0 commit comments

Comments
 (0)