55 "math"
66 "time"
77
8+ "github.com/block-vision/sui-go-sdk/models"
9+ "github.com/block-vision/sui-go-sdk/transaction"
810 "github.com/smartcontractkit/chainlink-common/pkg/services"
911
1012 "github.com/smartcontractkit/chainlink-sui/relayer/client"
@@ -56,14 +58,26 @@ func checkConfirmations(loopCtx context.Context, txm *SuiTxm) {
5658
5759 for _ , tx := range inFlightTransactions {
5860 txm .lggr .Debugw ("Checking transaction confirmations" , "transactionID" , tx .TransactionID )
59- if tx .State != StateSubmitted {
60- continue
61- }
6261
63- txm .lggr .Debugw ("Transaction is in submitted state" , "transactionID" , tx .TransactionID )
64- resp , err := txm .suiGateway .GetTransactionStatus (loopCtx , tx .Digest )
65- if err != nil {
66- txm .lggr .Errorw ("Error getting transaction status" , "transactionID" , tx .TransactionID , "error" , err )
62+ var resp client.TransactionResult
63+ var err error
64+
65+ if tx .State == StateSubmitted {
66+ txm .lggr .Debugw ("Transaction is in submitted state" , "transactionID" , tx .TransactionID )
67+ resp , err = txm .suiGateway .GetTransactionStatus (loopCtx , tx .Digest )
68+ if err != nil {
69+ txm .lggr .Errorw ("Error getting transaction status" , "transactionID" , tx .TransactionID , "error" , err )
70+ continue
71+ }
72+ } else if tx .State == StateRetriable {
73+ txm .lggr .Debugw ("Transaction is in retriable state" , "transactionID" , tx .TransactionID )
74+ // Check if it's a broadcast error (never made it onchain)
75+ if tx .BroadcastError == "" {
76+ continue
77+ }
78+ resp .Status = failure
79+ resp .Error = tx .BroadcastError
80+ } else {
6781 continue
6882 }
6983
@@ -88,15 +102,53 @@ func handleSuccess(txm *SuiTxm, tx SuiTx) error {
88102 return err
89103 }
90104 txm .lggr .Infow ("Transaction finalized" , "transactionID" , tx .TransactionID )
105+
106+ if err := txm .coinManager .ReleaseCoins (tx .TransactionID ); err != nil {
107+ // This error is not critical, can be safely ignored as the coins will auto-release after the default TTL
108+ txm .lggr .Debugw ("Failed to release coins" , "transactionID" , tx .TransactionID , "error" , err )
109+ }
110+
91111 return nil
92112}
93113
94114func handleTransactionError (ctx context.Context , txm * SuiTxm , tx SuiTx , result * client.TransactionResult ) error {
95115 txm .lggr .Debugw ("Handling transaction error" , "transactionID" , tx .TransactionID , "error" , result .Error )
96116
97117 txError := suierrors .ParseSuiErrorMessage (result .Error )
98- if txError == nil {
99- txError = suierrors .NewSuiError (suierrors .UnknownErrors , result .Error )
118+
119+ // Check if the error is a locked object error, mark the coin as reserved if it is not already
120+ // to avoid other transactions from using it
121+ if objectID , version , ok := suierrors .ExtractLockedObjectRef (result .Error ); ok {
122+ txm .lggr .Infow ("Detected locked coin at confirmation time" ,
123+ "txID" , tx .TransactionID ,
124+ "objectID" , objectID ,
125+ "version" , version ,
126+ )
127+
128+ coinID , err := transaction .ConvertSuiAddressStringToBytes (models .SuiAddress (objectID ))
129+ if err == nil && ! txm .coinManager .IsCoinReserved (* coinID ) {
130+ // Coin lock duration
131+ expiry := DefaultLockedCoinTTL
132+
133+ // The coin is not recorded is not marked as reserved, mark it as reserved
134+ err = txm .coinManager .TryReserveCoins (ctx , tx .TransactionID , []transaction.SuiObjectRef {
135+ {
136+ ObjectId : * coinID ,
137+ Version : 0 ,
138+ Digest : nil ,
139+ },
140+ }, & expiry )
141+
142+ if err != nil {
143+ // This is not a critical error, so we continue
144+ txm .lggr .Debugw (
145+ "Failed to mark locked coin as reserved" ,
146+ "transactionID" , tx .TransactionID ,
147+ "objectID" , objectID ,
148+ "error" , err ,
149+ )
150+ }
151+ }
100152 }
101153
102154 isRetryable , strategy := txm .retryManager .IsRetryable (& tx , result .Error )
@@ -111,6 +163,8 @@ func handleTransactionError(ctx context.Context, txm *SuiTxm, tx SuiTx, result *
111163 return handleExponentialBackoffRetry (txm , tx )
112164 case GasBump :
113165 return handleGasBumpRetry (ctx , txm , tx , txError )
166+ case CoinRefresh :
167+ return handleCoinRefreshRetry (ctx , txm , tx , txError )
114168 case NoRetry :
115169 return markTransactionFailed (txm , tx , txError )
116170 default :
@@ -147,6 +201,40 @@ func handleGasBumpRetry(ctx context.Context, txm *SuiTxm, tx SuiTx, txError *sui
147201 return nil
148202}
149203
204+ func handleCoinRefreshRetry (ctx context.Context , txm * SuiTxm , tx SuiTx , txError * suierrors.SuiError ) error {
205+ txm .lggr .Infow ("Coin refresh strategy - refreshing coins for locked coin error" , "transactionID" , tx .TransactionID )
206+
207+ // Release the old coins that are locked
208+ if err := txm .coinManager .ReleaseCoins (tx .TransactionID ); err != nil {
209+ // This is not critical - coins will auto-release after TTL
210+ txm .lggr .Debugw ("Failed to release old coins" , "transactionID" , tx .TransactionID , "error" , err )
211+ }
212+
213+ // Get the current transaction to ensure we have the latest state
214+ currentTx , err := txm .transactionRepository .GetTransaction (tx .TransactionID )
215+ if err != nil {
216+ txm .lggr .Errorw ("Failed to get current transaction" , "transactionID" , tx .TransactionID , "error" , err )
217+ return err
218+ }
219+
220+ // Calling UpdateTransactionGas will also update the gas coins used as the transaction gets re-built
221+ // with new (unlocked) coins.
222+ // Call chain: UpdateTransactionGas -> UpdateBSCPayload -> preparePTBTransaction (this refreshes the coins).
223+ if err := txm .transactionRepository .UpdateTransactionGas (ctx , txm .keystoreService , txm .suiGateway , tx .TransactionID , currentTx .Metadata .GasLimit ); err != nil {
224+ txm .lggr .Errorw ("Failed to update transaction with refreshed coins" , "transactionID" , tx .TransactionID , "error" , err )
225+ return err
226+ }
227+
228+ if err := txm .transactionRepository .ChangeState (tx .TransactionID , StateRetriable ); err != nil {
229+ txm .lggr .Errorw ("Failed to update transaction state" , "transactionID" , tx .TransactionID , "error" , err )
230+ return err
231+ }
232+
233+ txm .lggr .Infow ("Transaction refreshed with new coins" , "transactionID" , tx .TransactionID )
234+ txm .broadcastChannel <- tx .TransactionID
235+ return nil
236+ }
237+
150238func handleExponentialBackoffRetry (txm * SuiTxm , tx SuiTx ) error {
151239 delaySeconds := float64 (defaultExponentialBackoffDelaySeconds ) * math .Pow (2 , float64 (tx .Attempt ))
152240
@@ -185,5 +273,11 @@ func markTransactionFailed(txm *SuiTxm, tx SuiTx, txError *suierrors.SuiError) e
185273 }
186274
187275 txm .lggr .Infow ("Transaction failed" , "transactionID" , tx .TransactionID )
276+
277+ if err := txm .coinManager .ReleaseCoins (tx .TransactionID ); err != nil {
278+ // This error is not critical, can be safely ignored as the coins will auto-release after the default TTL
279+ txm .lggr .Debugw ("Failed to release coins" , "transactionID" , tx .TransactionID , "error" , err )
280+ }
281+
188282 return nil
189283}
0 commit comments