Skip to content

Commit c848ac7

Browse files
authored
docs: view Chainlink CRE usage examples and documentation (#168)
* docs: view Chainlink CRE usage examples and documentation * chore: apply suggestion * chore: remove debug print
1 parent dc627bd commit c848ac7

File tree

22 files changed

+1803
-14
lines changed

22 files changed

+1803
-14
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: 171 additions & 12 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
@@ -117,7 +121,7 @@ func (t *CRETransport) callJSONRPC(ctx context.Context, method string, params an
117121
// If we get a 401, try authenticating and retry once
118122
if err != nil && strings.Contains(err.Error(), "401") {
119123
if t.signer == nil {
120-
return fmt.Errorf("%w [DEBUG: signer is nil, cannot authenticate]", err)
124+
return fmt.Errorf("%w: signer is nil, cannot authenticate", err)
121125
}
122126
// Authenticate with gateway
123127
authErr := t.authenticate(ctx)
@@ -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,35 @@ 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+
txHash, err := t.executeOnce(ctx, namespace, action, inputs, opts...)
289+
if err != nil {
290+
// Check if it's a nonce error
291+
if strings.Contains(err.Error(), "invalid nonce") && attempt < maxRetries-1 {
292+
// Reset nonce tracking to refetch on next attempt
293+
t.nonceMu.Lock()
294+
t.nonceFetched = false
295+
t.nonceMu.Unlock()
296+
continue // Retry
297+
}
298+
return types.Hash{}, err
299+
}
300+
return txHash, nil
301+
}
302+
303+
return types.Hash{}, fmt.Errorf("max retries exceeded")
304+
}
305+
306+
// executeOnce performs a single execute attempt (internal helper)
307+
func (t *CRETransport) executeOnce(ctx context.Context, namespace string, action string, inputs [][]any, opts ...clientType.TxOpt) (types.Hash, error) {
268308
// Convert inputs to EncodedValue arrays
269309
var encodedInputs [][]*types.EncodedValue
270310
for _, inputRow := range inputs {
@@ -298,6 +338,51 @@ func (t *CRETransport) Execute(ctx context.Context, namespace string, action str
298338
opt(txOpts)
299339
}
300340

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

410+
// Ensure Fee is not nil to prevent signature verification mismatch
411+
// When Fee is nil, SerializeMsg produces "Fee: <nil>" but after JSON
412+
// marshaling/unmarshaling it becomes "Fee: 0", causing signature mismatch
413+
fee := txOpts.Fee
414+
if fee == nil {
415+
fee = big.NewInt(0)
416+
}
417+
325418
// Build unsigned transaction
326419
tx := &types.Transaction{
327420
Body: &types.TransactionBody{
328421
Payload: payloadBytes,
329422
PayloadType: payload.Type(),
330-
Fee: txOpts.Fee,
423+
Fee: fee,
331424
Nonce: uint64(txOpts.Nonce),
332425
ChainID: chainID,
333426
},
427+
Serialization: types.DefaultSignedMsgSerType, // Required for EthPersonalSigner
334428
}
335429

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

341-
// Broadcast transaction
342-
params := map[string]any{
343-
"tx": tx,
435+
// Pre-serialize transaction to avoid WASM pointer corruption
436+
// Go WASM uses 64-bit pointers but WASM runtime uses 32-bit pointers.
437+
// Transaction struct contains pointer fields (Signature, Body) which get
438+
// corrupted when crossing the WASM boundary (golang/go#59156, golang/go#66984).
439+
// Solution: Manually construct JSON-RPC request to avoid struct traversal in WASM.
440+
txJSON, err := json.Marshal(tx)
441+
if err != nil {
442+
return types.Hash{}, fmt.Errorf("failed to marshal transaction: %w", err)
443+
}
444+
445+
// Manually construct JSON-RPC request to bypass params map
446+
reqID := t.nextReqID()
447+
rpcReqJSON := fmt.Sprintf(
448+
`{"jsonrpc":"2.0","id":"%s","method":"user.broadcast","params":{"tx":%s}}`,
449+
reqID, string(txJSON))
450+
451+
// Create headers
452+
headers := map[string]string{
453+
"Content-Type": "application/json",
344454
}
345455

456+
// Add auth cookie if we have one
457+
t.authCookieMu.RLock()
458+
if t.authCookie != "" {
459+
headers["Cookie"] = t.authCookie
460+
}
461+
t.authCookieMu.RUnlock()
462+
463+
// Create CRE HTTP request
464+
httpReq := &http.Request{
465+
Url: t.endpoint,
466+
Method: "POST",
467+
Body: []byte(rpcReqJSON),
468+
Headers: headers,
469+
}
470+
471+
// Execute via CRE client
472+
httpResp, err := t.client.SendRequest(t.runtime, httpReq).Await()
473+
if err != nil {
474+
return types.Hash{}, fmt.Errorf("CRE HTTP request failed: %w", err)
475+
}
476+
477+
// Check HTTP status
478+
if httpResp.StatusCode != 200 {
479+
return types.Hash{}, fmt.Errorf("unexpected HTTP status code: %d", httpResp.StatusCode)
480+
}
481+
482+
// Parse JSON-RPC response
483+
var rpcResp jsonrpc.Response
484+
if err := json.Unmarshal(httpResp.Body, &rpcResp); err != nil {
485+
return types.Hash{}, fmt.Errorf("failed to unmarshal JSON-RPC response: %w", err)
486+
}
487+
488+
// Check for JSON-RPC errors
489+
if rpcResp.Error != nil {
490+
// For broadcast errors (-201), decode the BroadcastError details
491+
if rpcResp.Error.Code == -201 && len(rpcResp.Error.Data) > 0 {
492+
var broadcastErr struct {
493+
Code uint32 `json:"code"`
494+
Hash string `json:"hash"`
495+
Message string `json:"message"`
496+
}
497+
if err := json.Unmarshal(rpcResp.Error.Data, &broadcastErr); err == nil {
498+
return types.Hash{}, fmt.Errorf("JSON-RPC error: %s (code: %d) [Broadcast: code=%d, hash=%s, msg=%s]",
499+
rpcResp.Error.Message, rpcResp.Error.Code,
500+
broadcastErr.Code, broadcastErr.Hash, broadcastErr.Message)
501+
}
502+
}
503+
return types.Hash{}, fmt.Errorf("JSON-RPC error: %s (code: %d)", rpcResp.Error.Message, rpcResp.Error.Code)
504+
}
505+
506+
// Unmarshal result
346507
var result struct {
347508
TxHash types.Hash `json:"tx_hash"`
348509
}
349-
350-
if err := t.callJSONRPC(ctx, "user.broadcast", params, &result); err != nil {
351-
return types.Hash{}, err
510+
if err := json.Unmarshal(rpcResp.Result, &result); err != nil {
511+
return types.Hash{}, fmt.Errorf("failed to unmarshal result: %w", err)
352512
}
353513

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

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

599758
return nil

0 commit comments

Comments
 (0)