github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/epochs/base_client.go (about)

     1  package epochs
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/sethvargo/go-retry"
    10  
    11  	"github.com/rs/zerolog"
    12  
    13  	sdk "github.com/onflow/flow-go-sdk"
    14  	sdkcrypto "github.com/onflow/flow-go-sdk/crypto"
    15  	"github.com/onflow/flow-go/network"
    16  
    17  	"github.com/onflow/flow-go/module"
    18  )
    19  
    20  const (
    21  	waitForSealedRetryInterval = 3 * time.Second
    22  	waitForSealedMaxDuration   = 5 * time.Minute
    23  )
    24  
    25  var (
    26  	// errTransactionExpired is returned when a transaction expires before it is incorporated into a block.
    27  	errTransactionExpired = errors.New("transaction expired")
    28  	// errTransactionReverted is returned when a transaction is executed, but the execution reverts.
    29  	errTransactionReverted = errors.New("transaction execution reverted")
    30  )
    31  
    32  // BaseClient represents the core fields and methods needed to create
    33  // a client to a contract on the Flow Network.
    34  type BaseClient struct {
    35  	Log zerolog.Logger // default logger
    36  
    37  	FlowClient module.SDKClientWrapper // flow access node client
    38  
    39  	AccountAddress  sdk.Address      // account belonging to node interacting with the contract
    40  	AccountKeyIndex uint             // account key index
    41  	Signer          sdkcrypto.Signer // signer used to sign transactions
    42  }
    43  
    44  // NewBaseClient creates a instance of BaseClient
    45  func NewBaseClient(
    46  	log zerolog.Logger,
    47  	flowClient module.SDKClientWrapper,
    48  	accountAddress string,
    49  	accountKeyIndex uint,
    50  	signer sdkcrypto.Signer,
    51  ) *BaseClient {
    52  
    53  	return &BaseClient{
    54  		Log:             log,
    55  		FlowClient:      flowClient,
    56  		AccountKeyIndex: accountKeyIndex,
    57  		Signer:          signer,
    58  		AccountAddress:  sdk.HexToAddress(accountAddress),
    59  	}
    60  }
    61  
    62  // GetAccount returns the current state for the account associated with the BaseClient.
    63  // Error returns:
    64  //   - network.TransientError for any errors from the underlying client
    65  //   - generic error in case of unexpected critical failure
    66  func (c *BaseClient) GetAccount(ctx context.Context) (*sdk.Account, error) {
    67  
    68  	// get account from access node for given address
    69  	account, err := c.FlowClient.GetAccount(ctx, c.AccountAddress)
    70  	if err != nil {
    71  		// we consider all errors from client network calls to be transient and non-critical
    72  		return nil, network.NewTransientErrorf("could not get account: %w", err)
    73  	}
    74  
    75  	// check if account key index within range of keys
    76  	if int(c.AccountKeyIndex) >= len(account.Keys) {
    77  		return nil, fmt.Errorf("given account key index exceeds the number of keys for this account (%d>=%d)",
    78  			c.AccountKeyIndex, len(account.Keys))
    79  	}
    80  
    81  	return account, nil
    82  }
    83  
    84  // SendTransaction submits a transaction to Flow. Requires transaction to be signed.
    85  // Error returns:
    86  //   - network.TransientError for any errors from the underlying client
    87  //   - generic error in case of unexpected critical failure
    88  func (c *BaseClient) SendTransaction(ctx context.Context, tx *sdk.Transaction) (sdk.Identifier, error) {
    89  
    90  	// check if the transaction has a signature
    91  	if len(tx.EnvelopeSignatures) == 0 {
    92  		return sdk.EmptyID, fmt.Errorf("can not submit an unsigned transaction")
    93  	}
    94  
    95  	// submit transaction to client
    96  	err := c.FlowClient.SendTransaction(ctx, *tx)
    97  	if err != nil {
    98  		// we consider all errors from client network calls to be transient and non-critical
    99  		return sdk.EmptyID, network.NewTransientErrorf("failed to send transaction: %w", err)
   100  	}
   101  
   102  	return tx.ID(), nil
   103  }
   104  
   105  // WaitForSealed waits for a transaction to be sealed
   106  // Error returns:
   107  //   - network.TransientError for any errors from the underlying client, if the retry period has been exceeded
   108  //   - errTransactionExpired if the transaction has expired
   109  //   - errTransactionReverted if the transaction execution reverted
   110  //   - generic error in case of unexpected critical failure
   111  func (c *BaseClient) WaitForSealed(ctx context.Context, txID sdk.Identifier, started time.Time) error {
   112  
   113  	log := c.Log.With().Str("tx_id", txID.Hex()).Logger()
   114  
   115  	backoff := retry.NewConstant(waitForSealedRetryInterval)
   116  	backoff = retry.WithMaxDuration(waitForSealedMaxDuration, backoff)
   117  
   118  	attempts := 0
   119  	err := retry.Do(ctx, backoff, func(ctx context.Context) error {
   120  		attempts++
   121  		log = c.Log.With().Int("attempt", attempts).Float64("time_elapsed_s", time.Since(started).Seconds()).Logger()
   122  
   123  		result, err := c.FlowClient.GetTransactionResult(ctx, txID)
   124  		if err != nil {
   125  			// we consider all errors from client network calls to be transient and non-critical
   126  			err = network.NewTransientErrorf("could not get transaction result: %w", err)
   127  			log.Err(err).Msg("retrying getting transaction result...")
   128  			return retry.RetryableError(err)
   129  		}
   130  
   131  		if result.Error != nil {
   132  			return fmt.Errorf("transaction reverted with error=[%s]: %w", result.Error.Error(), errTransactionReverted)
   133  		}
   134  
   135  		log.Info().Str("status", result.Status.String()).Msg("got transaction result")
   136  
   137  		// if the transaction has expired we skip waiting for seal
   138  		if result.Status == sdk.TransactionStatusExpired {
   139  			return errTransactionExpired
   140  		}
   141  
   142  		if result.Status == sdk.TransactionStatusSealed {
   143  			return nil
   144  		}
   145  
   146  		return retry.RetryableError(network.NewTransientErrorf("transaction not sealed yet (status=%s)", result.Status))
   147  	})
   148  	if err != nil {
   149  		return fmt.Errorf("transaction (id=%s) failed to be sealed successfully after %s: %w", txID.String(), time.Since(started), err)
   150  	}
   151  
   152  	return nil
   153  }