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.
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+ 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