@@ -23,7 +23,8 @@ import (
2323)
2424
2525const (
26- MaxQueueLen = 1000
26+ MaxQueueLen = 1000
27+ ConfirmationThreshold = 4
2728)
2829
2930type TxManager interface {
@@ -188,7 +189,7 @@ func (txm *starktxm) broadcast(ctx context.Context, publicKey *felt.Felt, accoun
188189 if accountNonceErr != nil {
189190 return txhash , fmt .Errorf ("failed to check account nonce during TxStore creation: %+w" , accountNonceErr )
190191 }
191- newTxStore , createErr := txm .accountStore .CreateTxStore (accountAddress , initialNonce )
192+ newTxStore , createErr := txm .accountStore .CreateTxStore (accountAddress , initialNonce , txm . lggr )
192193 if createErr != nil {
193194 return txhash , fmt .Errorf ("failed to create TxStore: %+w" , createErr )
194195 }
@@ -273,7 +274,7 @@ func (txm *starktxm) broadcast(ctx context.Context, publicKey *felt.Felt, accoun
273274 broadcastTxnV3 .InvokeTxnV3 .ResourceBounds .L1Gas .MaxPricePerUnit = txm .updateMaxPriceUnitBounds (L1GasPrice , 150 )
274275 broadcastTxnV3 .InvokeTxnV3 .ResourceBounds .L2Gas .MaxPricePerUnit = txm .updateMaxPriceUnitBounds (L2GasPrice , 150 )
275276
276- txm .lggr .Infow ("Set resource bounds" , "L1MaxAmount" , tx .ResourceBounds .L1Gas .MaxAmount , "L1MaxPricePerUnit" , tx .ResourceBounds .L1Gas .MaxPricePerUnit )
277+ txm .lggr .Infow ("Set resource bounds" , "L1MaxAmount" , tx .ResourceBounds .L1Gas .MaxAmount , "L1MaxPricePerUnit" , tx .ResourceBounds .L1Gas .MaxPricePerUnit , "FinalNonce" , nonce )
277278
278279 L1DataGasConsumed := friEstimate .L1DataGasConsumed .BigInt (new (big.Int ))
279280 L1DataGasPrice := friEstimate .L1DataGasPrice .BigInt (new (big.Int ))
@@ -293,17 +294,8 @@ func (txm *starktxm) broadcast(ctx context.Context, publicKey *felt.Felt, accoun
293294 // finally, transmit the invoke
294295 res , err := account .Provider .AddInvokeTransaction (execCtx , & broadcastTxnV3 )
295296 if err != nil {
296- // TODO: handle initial broadcast errors - what kind of errors occur?
297- var dataErr * starknetrpc.RPCError
298- var dataStr string
299- if ! errors .As (err , & dataErr ) {
300- return txhash , fmt .Errorf ("failed to read EstimateFee error: %T %+v" , err , err )
301- }
302- data := dataErr .Data
303- dataStr = fmt .Sprintf ("%+v" , data )
304- txm .lggr .Errorw ("failed to invoke tx" , "accountAddress" , accountAddress , "error" , err , "data" , dataStr )
305-
306- if strings .Contains (dataStr , RPCNonceErrMsg ) {
297+ txm .lggr .Errorw ("failed to invoke tx" , "accountAddress" , accountAddress , "error" , err )
298+ if strings .Contains (err .Error (), RPCNonceErrMsg ) {
307299 // if we see an invalid nonce error at the broadcast stage, that means that we are out of sync.
308300 // see the comment at resyncNonce for more details.
309301 if resyncErr := txm .resyncNonce (ctx , client , accountAddress ); resyncErr != nil {
@@ -362,56 +354,34 @@ func (txm *starktxm) confirmLoop() {
362354 break
363355 }
364356
365- allUnconfirmedTxs := txm .accountStore .GetAllUnconfirmed ()
366- for accountAddressStr , unconfirmedTxs := range allUnconfirmedTxs {
357+ for _ , accountAddressStr := range txm .accountStore .Accounts () {
367358 accountAddress , err := new (felt.Felt ).SetString (accountAddressStr )
368359 // this should never occur because the acccount address string key was created from the account address felt.
369360 if err != nil {
370361 txm .lggr .Errorw ("could not recreate account address felt" , "accountAddress" , accountAddressStr )
371362 continue
372363 }
373- for _ , unconfirmedTx := range unconfirmedTxs {
374- hash := unconfirmedTx .Hash
375- f , err := starknetutils .HexToFelt (hash )
376- if err != nil {
377- txm .lggr .Errorw ("invalid felt value" , "hash" , hash )
378- continue
379- }
380- response , err := client .Provider .GetTransactionStatus (ctx , f )
381-
382- // tx can be rejected due to a nonce error. but we cannot know from the Starknet RPC directly so we have to wait for
383- // a broadcasted tx to fail in order to fix the nonce errors
384-
385- if err != nil {
386- txm .lggr .Errorw ("failed to fetch transaction status" , "hash" , hash , "nonce" , unconfirmedTx .Nonce , "error" , err )
387- continue
388- }
389-
390- finalityStatus := response .FinalityStatus
391- executionStatus := response .ExecutionStatus
392-
393- // any finalityStatus other than received
394- if finalityStatus == starknetrpc .TxnStatus_Accepted_On_L1 || finalityStatus == starknetrpc .TxnStatus_Accepted_On_L2 || finalityStatus == starknetrpc .TxnStatus_Rejected {
395- txm .lggr .Debugw (fmt .Sprintf ("tx confirmed: %s" , finalityStatus ), "hash" , hash , "nonce" , unconfirmedTx .Nonce , "finalityStatus" , finalityStatus )
396- if err := txm .accountStore .GetTxStore (accountAddress ).Confirm (unconfirmedTx .Nonce , hash ); err != nil {
397- txm .lggr .Errorw ("failed to confirm tx in TxStore" , "hash" , hash , "accountAddress" , accountAddress , "error" , err )
398- }
399- }
400-
401- // currently, feeder client is only way to get rejected reason
402- if finalityStatus == starknetrpc .TxnStatus_Rejected {
403- // we assume that all rejected transactions results in a unused rejected nonce, so
404- // resync. see the comment at resyncNonce for more details.
405- if resyncErr := txm .resyncNonce (ctx , client , accountAddress ); resyncErr != nil {
406- txm .lggr .Errorw ("resync failed for rejected tx" , "error" , resyncErr )
407- }
408-
409- go txm .logFeederError (ctx , hash , f )
410- }
411-
412- if executionStatus == starknetrpc .TxnExecutionStatusREVERTED {
413- // TODO: get revert reason?
414- txm .lggr .Errorw ("transaction reverted" , "hash" , hash )
364+ nonce , err := client .AccountNonceLatest (ctx , accountAddress )
365+ if err != nil {
366+ txm .lggr .Errorf ("failed to fetch latest nonce for account %v, err: %v" , accountAddress , err )
367+ continue
368+ }
369+ // Confirm all transactions with nonce lower than the latest.
370+ confirmed , highestUnconfirmed := txm .accountStore .GetTxStore (accountAddress ).Confirm (nonce )
371+ txm .lggr .Infow ("Confirmation loop" , "accountAddress" , accountAddress , "latestNonce" , nonce ,
372+ "transactionsConfirmed" , confirmed , "highestUnconfirmed" , highestUnconfirmed )
373+
374+ // We add a maximum threshold between latest nonce and highest unconfirmed. This prevents the TXM from sending a very large
375+ // number of unconfirmed transactions in the mempool and triggers a resync to prevent nonce gaps since the RPC responses are unreliable.
376+ // The nonce stored here won't necessarily be picked up by the next transaction since there is a fast-forward functionality in broadcasting.
377+ // But it ensures that if for whatever reason the diff between mined and uncofirmed transactions starts to grow, the TXM will be able to
378+ // go back on the nonce and fill any nonce gaps.
379+ hu := highestUnconfirmed .BigInt (new (big.Int ))
380+ n := nonce .BigInt (new (big.Int ))
381+ threshold := big .NewInt (ConfirmationThreshold )
382+ if new (big.Int ).Sub (hu , n ).Cmp (threshold ) == 1 {
383+ if resyncErr := txm .resyncNonce (ctx , client , accountAddress ); resyncErr != nil {
384+ txm .lggr .Errorw ("resync failed for rejected tx" , "error" , resyncErr )
415385 }
416386 }
417387 }
@@ -424,22 +394,6 @@ func (txm *starktxm) confirmLoop() {
424394 }
425395}
426396
427- func (txm * starktxm ) logFeederError (ctx context.Context , hash string , f * felt.Felt ) {
428- feederClient , err := txm .feederClient .Get ()
429- if err != nil {
430- txm .lggr .Errorw ("failed to load feeder client" , "error" , err )
431- return
432- }
433-
434- rejectedTx , err := feederClient .TransactionFailure (ctx , f )
435- if err != nil {
436- txm .lggr .Errorw ("failed to fetch reason for transaction failure" , "hash" , hash , "error" , err )
437- return
438- }
439-
440- txm .lggr .Errorw ("feeder rejected reason" , "hash" , hash , "errorMessage" , rejectedTx .ErrorMessage )
441- }
442-
443397func (txm * starktxm ) resyncNonce (ctx context.Context , client * starknet.Client , accountAddress * felt.Felt ) error {
444398 /*
445399 the follow errors indicate that there could be a problem with our locally tracked nonce value:
0 commit comments