@@ -4,13 +4,13 @@ import (
44 "context"
55 "encoding/binary"
66 "fmt"
7+ "time"
78
89 "github.com/ethereum/go-ethereum/common"
910 "github.com/gagliardetto/solana-go"
1011 "github.com/gagliardetto/solana-go/rpc"
1112 "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/mcm"
1213 "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/timelock"
13- solanaCommon "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common"
1414)
1515
1616const (
@@ -73,7 +73,8 @@ func FindTimelockOperationPDA(
7373}
7474
7575func FindTimelockBypasserOperationPDA (
76- programID solana.PublicKey , timelockID PDASeed , opID [32 ]byte ) (solana.PublicKey , error ) {
76+ programID solana.PublicKey , timelockID PDASeed , opID [32 ]byte ,
77+ ) (solana.PublicKey , error ) {
7778 seeds := [][]byte {[]byte ("timelock_bypasser_operation" ), timelockID [:], opID [:]}
7879 return findPDA (programID , seeds )
7980}
@@ -138,6 +139,16 @@ type SendAndConfirmFn func(
138139 auth solana.PrivateKey ,
139140 builder any ,
140141 commitmentType rpc.CommitmentType ,
142+ opts ... sendTransactionOption ,
143+ ) (string , * rpc.GetTransactionResult , error )
144+
145+ type SendAndConfirmInstructionsFn func (
146+ ctx context.Context ,
147+ client * rpc.Client ,
148+ auth solana.PrivateKey ,
149+ instructions []solana.Instruction ,
150+ commitmentType rpc.CommitmentType ,
151+ opts ... sendTransactionOption ,
141152) (string , * rpc.GetTransactionResult , error )
142153
143154// sendAndConfirm contains the default logic for sending and confirming instructions.
@@ -147,13 +158,14 @@ func sendAndConfirm(
147158 auth solana.PrivateKey ,
148159 instructionBuilder any ,
149160 commitmentType rpc.CommitmentType ,
161+ opts ... sendTransactionOption ,
150162) (string , * rpc.GetTransactionResult , error ) {
151163 instruction , err := validateAndBuildSolanaInstruction (instructionBuilder )
152164 if err != nil {
153165 return "" , nil , fmt .Errorf ("unable to validate and build instruction: %w" , err )
154166 }
155167
156- return sendAndConfirmInstructions (ctx , client , auth , []solana.Instruction {instruction }, commitmentType )
168+ return sendAndConfirmInstructions (ctx , client , auth , []solana.Instruction {instruction }, commitmentType , opts ... )
157169}
158170
159171// sendAndConfirm contains the default logic for sending and confirming instructions.
@@ -163,8 +175,9 @@ func sendAndConfirmInstructions(
163175 auth solana.PrivateKey ,
164176 instructions []solana.Instruction ,
165177 commitmentType rpc.CommitmentType ,
178+ opts ... sendTransactionOption ,
166179) (string , * rpc.GetTransactionResult , error ) {
167- result , err := solanaCommon . SendAndConfirm (ctx , client , instructions , auth , commitmentType )
180+ result , err := sendTransaction (ctx , client , instructions , auth , commitmentType , opts ... )
168181 if err != nil {
169182 return "" , nil , fmt .Errorf ("unable to send instruction: %w" , err )
170183 }
@@ -193,3 +206,144 @@ func chunkIndexes(numItems int, chunkSize int) [][2]int {
193206
194207 return indexes
195208}
209+
210+ type sendTransactionOptions struct {
211+ retries int
212+ delay time.Duration
213+ skipPreflight bool
214+ }
215+
216+ var defaultSendTransactionOptions = func () * sendTransactionOptions {
217+ return & sendTransactionOptions {
218+ retries : 500 , //nolint:mnd
219+ delay : 50 * time .Millisecond , //nolint:mnd
220+ skipPreflight : false ,
221+ }
222+ }
223+
224+ type sendTransactionOption func (* sendTransactionOptions )
225+
226+ func WithRetries (retries int ) sendTransactionOption {
227+ return func (opts * sendTransactionOptions ) { opts .retries = retries }
228+ }
229+
230+ func WithDelay (delay time.Duration ) sendTransactionOption {
231+ return func (opts * sendTransactionOptions ) {
232+ opts .delay = delay
233+ }
234+ }
235+
236+ func WithSkipPreflight (skipPreflight bool ) sendTransactionOption {
237+ return func (opts * sendTransactionOptions ) {
238+ opts .skipPreflight = skipPreflight
239+ }
240+ }
241+
242+ func sendTransaction (
243+ ctx context.Context ,
244+ rpcClient * rpc.Client ,
245+ instructions []solana.Instruction ,
246+ signerAndPayer solana.PrivateKey ,
247+ commitment rpc.CommitmentType ,
248+ opts ... sendTransactionOption ,
249+ ) (* rpc.GetTransactionResult , error ) {
250+ var errBlockHash error
251+ var hashRes * rpc.GetLatestBlockhashResult
252+
253+ sendTransactionOptions := defaultSendTransactionOptions ()
254+ for _ , opt := range opts {
255+ opt (sendTransactionOptions )
256+ }
257+
258+ for range sendTransactionOptions .retries {
259+ hashRes , errBlockHash = rpcClient .GetLatestBlockhash (ctx , rpc .CommitmentConfirmed )
260+ if errBlockHash != nil {
261+ fmt .Println ("GetLatestBlockhash error:" , errBlockHash ) //nolint:forbidigo
262+ time .Sleep (sendTransactionOptions .delay )
263+
264+ continue
265+ }
266+
267+ break
268+ }
269+ if errBlockHash != nil {
270+ fmt .Println ("GetLatestBlockhash error after retries:" , errBlockHash ) //nolint:forbidigo
271+ return nil , errBlockHash
272+ }
273+
274+ tx , err := solana .NewTransaction (instructions , hashRes .Value .Blockhash , solana .TransactionPayer (signerAndPayer .PublicKey ()))
275+ if err != nil {
276+ return nil , err
277+ }
278+
279+ // build signers map
280+ signers := map [solana.PublicKey ]solana.PrivateKey {signerAndPayer .PublicKey (): signerAndPayer }
281+ _ , err = tx .Sign (func (pub solana.PublicKey ) * solana.PrivateKey {
282+ priv , ok := signers [pub ]
283+ if ! ok {
284+ fmt .Printf ("ERROR: Missing signer private key for %s\n " , pub ) //nolint:forbidigo
285+ }
286+
287+ return & priv
288+ })
289+ if err != nil {
290+ return nil , err
291+ }
292+
293+ var txsig solana.Signature
294+ for range sendTransactionOptions .retries {
295+ txOpts := rpc.TransactionOpts {SkipPreflight : sendTransactionOptions .skipPreflight , PreflightCommitment : commitment }
296+ txsig , err = rpcClient .SendTransactionWithOpts (ctx , tx , txOpts )
297+ if err != nil {
298+ fmt .Println ("Error sending transaction:" , err ) //nolint:forbidigo
299+ time .Sleep (sendTransactionOptions .delay )
300+
301+ continue
302+ }
303+
304+ break
305+ }
306+ // If tx failed with rpc error, we should not retry as confirmation will never happen
307+ if err != nil {
308+ fmt .Println ("Error sending transaction after retries:" , err ) //nolint:forbidigo
309+ return nil , err
310+ }
311+
312+ var txStatus rpc.ConfirmationStatusType
313+ count := 0
314+ for txStatus != rpc .ConfirmationStatusConfirmed && txStatus != rpc .ConfirmationStatusFinalized {
315+ if count > sendTransactionOptions .retries {
316+ return nil , fmt .Errorf ("unable to find transaction within timeout (sig: %v)" , txsig )
317+ }
318+ count ++
319+ statusRes , sigErr := rpcClient .GetSignatureStatuses (ctx , true , txsig )
320+ if sigErr != nil {
321+ fmt .Println (sigErr ) //nolint:forbidigo // debugging if tx errors; mainnet can be flakey
322+ time .Sleep (sendTransactionOptions .delay )
323+
324+ continue
325+ }
326+ if statusRes != nil && len (statusRes .Value ) > 0 && statusRes .Value [0 ] != nil {
327+ txStatus = statusRes .Value [0 ].ConfirmationStatus
328+ }
329+ time .Sleep (sendTransactionOptions .delay )
330+ }
331+
332+ v := uint64 (0 )
333+ var errGetTx error
334+ var transactionRes * rpc.GetTransactionResult
335+ txOpts := & rpc.GetTransactionOpts {Commitment : commitment , MaxSupportedTransactionVersion : & v }
336+ for range sendTransactionOptions .retries {
337+ transactionRes , errGetTx = rpcClient .GetTransaction (ctx , txsig , txOpts )
338+ if errGetTx != nil {
339+ fmt .Println ("Error getting transaction:" , errGetTx ) //nolint:forbidigo
340+ time .Sleep (sendTransactionOptions .delay )
341+
342+ continue
343+ }
344+
345+ break
346+ }
347+
348+ return transactionRes , errGetTx
349+ }
0 commit comments