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 }