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.
263280func (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