|
| 1 | +package deployment |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "errors" |
| 6 | + "fmt" |
| 7 | + "math/big" |
| 8 | + "time" |
| 9 | + |
| 10 | + "github.com/avast/retry-go/v4" |
| 11 | + "github.com/ethereum/go-ethereum" |
| 12 | + "github.com/ethereum/go-ethereum/accounts/abi/bind" |
| 13 | + "github.com/ethereum/go-ethereum/common" |
| 14 | + "github.com/ethereum/go-ethereum/core/types" |
| 15 | + "github.com/ethereum/go-ethereum/ethclient" |
| 16 | + |
| 17 | + "github.com/smartcontractkit/chainlink-common/pkg/logger" |
| 18 | + |
| 19 | + chainsel "github.com/smartcontractkit/chain-selectors" |
| 20 | +) |
| 21 | + |
| 22 | +const ( |
| 23 | + // Default retry configuration for RPC calls |
| 24 | + RPCDefaultRetryAttempts = 10 |
| 25 | + RPCDefaultRetryDelay = 1000 * time.Millisecond |
| 26 | + |
| 27 | + // Default retry configuration for dialing RPC endpoints |
| 28 | + RPCDefaultDialRetryAttempts = 10 |
| 29 | + RPCDefaultDialRetryDelay = 1000 * time.Millisecond |
| 30 | +) |
| 31 | + |
| 32 | +type RetryConfig struct { |
| 33 | + Attempts uint |
| 34 | + Delay time.Duration |
| 35 | +} |
| 36 | + |
| 37 | +func defaultRetryConfig() RetryConfig { |
| 38 | + return RetryConfig{ |
| 39 | + Attempts: RPCDefaultRetryAttempts, |
| 40 | + Delay: RPCDefaultRetryDelay, |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +// MultiClient should comply with the OnchainClient interface |
| 45 | +var _ OnchainClient = &MultiClient{} |
| 46 | + |
| 47 | +type MultiClient struct { |
| 48 | + *ethclient.Client |
| 49 | + Backups []*ethclient.Client |
| 50 | + RetryConfig RetryConfig |
| 51 | + lggr logger.Logger |
| 52 | + chainName string |
| 53 | +} |
| 54 | + |
| 55 | +func NewMultiClient(lggr logger.Logger, rpcsCfg RPCConfig, opts ...func(client *MultiClient)) (*MultiClient, error) { |
| 56 | + if len(rpcsCfg.RPCs) == 0 { |
| 57 | + return nil, errors.New("no RPCs provided, need at least one") |
| 58 | + } |
| 59 | + // Set the chain name |
| 60 | + chain, exists := chainsel.ChainBySelector(rpcsCfg.ChainSelector) |
| 61 | + if !exists { |
| 62 | + return nil, fmt.Errorf("chain with selector %d not found", rpcsCfg.ChainSelector) |
| 63 | + } |
| 64 | + mc := MultiClient{lggr: lggr, chainName: chain.Name} |
| 65 | + |
| 66 | + clients := make([]*ethclient.Client, 0, len(rpcsCfg.RPCs)) |
| 67 | + for i, rpc := range rpcsCfg.RPCs { |
| 68 | + client, err := mc.dialWithRetry(rpc, lggr) |
| 69 | + if err != nil { |
| 70 | + lggr.Warnf("failed to dial client %d for RPC '%s' trying with the next one: %v", i, rpc.Name, err) |
| 71 | + continue |
| 72 | + } |
| 73 | + clients = append(clients, client) |
| 74 | + } |
| 75 | + |
| 76 | + if len(clients) == 0 { |
| 77 | + return nil, errors.New("no valid RPC clients created") |
| 78 | + } |
| 79 | + |
| 80 | + mc.Client = clients[0] |
| 81 | + mc.Backups = clients[1:] |
| 82 | + mc.RetryConfig = defaultRetryConfig() |
| 83 | + |
| 84 | + for _, opt := range opts { |
| 85 | + opt(&mc) |
| 86 | + } |
| 87 | + return &mc, nil |
| 88 | +} |
| 89 | + |
| 90 | +func (mc *MultiClient) SendTransaction(ctx context.Context, tx *types.Transaction) error { |
| 91 | + return mc.retryWithBackups("SendTransaction", func(client *ethclient.Client) error { |
| 92 | + return client.SendTransaction(ctx, tx) |
| 93 | + }) |
| 94 | +} |
| 95 | + |
| 96 | +func (mc *MultiClient) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { |
| 97 | + var result []byte |
| 98 | + err := mc.retryWithBackups("CallContract", func(client *ethclient.Client) error { |
| 99 | + var err error |
| 100 | + result, err = client.CallContract(ctx, msg, blockNumber) |
| 101 | + return err |
| 102 | + }) |
| 103 | + return result, err |
| 104 | +} |
| 105 | + |
| 106 | +func (mc *MultiClient) CallContractAtHash(ctx context.Context, msg ethereum.CallMsg, blockHash common.Hash) ([]byte, error) { |
| 107 | + var result []byte |
| 108 | + err := mc.retryWithBackups("CallContractAtHash", func(client *ethclient.Client) error { |
| 109 | + var err error |
| 110 | + result, err = client.CallContractAtHash(ctx, msg, blockHash) |
| 111 | + return err |
| 112 | + }) |
| 113 | + return result, err |
| 114 | +} |
| 115 | + |
| 116 | +func (mc *MultiClient) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { |
| 117 | + var code []byte |
| 118 | + err := mc.retryWithBackups("CodeAt", func(client *ethclient.Client) error { |
| 119 | + var err error |
| 120 | + code, err = client.CodeAt(ctx, account, blockNumber) |
| 121 | + return err |
| 122 | + }) |
| 123 | + return code, err |
| 124 | +} |
| 125 | + |
| 126 | +func (mc *MultiClient) CodeAtHash(ctx context.Context, account common.Address, blockHash common.Hash) ([]byte, error) { |
| 127 | + var code []byte |
| 128 | + err := mc.retryWithBackups("CodeAtHash", func(client *ethclient.Client) error { |
| 129 | + var err error |
| 130 | + code, err = client.CodeAtHash(ctx, account, blockHash) |
| 131 | + return err |
| 132 | + }) |
| 133 | + return code, err |
| 134 | +} |
| 135 | + |
| 136 | +func (mc *MultiClient) NonceAt(ctx context.Context, account common.Address, block *big.Int) (uint64, error) { |
| 137 | + var count uint64 |
| 138 | + err := mc.retryWithBackups("NonceAt", func(client *ethclient.Client) error { |
| 139 | + var err error |
| 140 | + count, err = client.NonceAt(ctx, account, block) |
| 141 | + return err |
| 142 | + }) |
| 143 | + return count, err |
| 144 | +} |
| 145 | + |
| 146 | +func (mc *MultiClient) NonceAtHash(ctx context.Context, account common.Address, blockHash common.Hash) (uint64, error) { |
| 147 | + var count uint64 |
| 148 | + err := mc.retryWithBackups("NonceAtHash", func(client *ethclient.Client) error { |
| 149 | + var err error |
| 150 | + count, err = client.NonceAtHash(ctx, account, blockHash) |
| 151 | + return err |
| 152 | + }) |
| 153 | + return count, err |
| 154 | +} |
| 155 | + |
| 156 | +func (mc *MultiClient) WaitMined(ctx context.Context, tx *types.Transaction) (*types.Receipt, error) { |
| 157 | + mc.lggr.Debugf("Waiting for tx %s to be mined for chain %s", tx.Hash().Hex(), mc.chainName) |
| 158 | + // no retries here because we want to wait for the tx to be mined |
| 159 | + resultCh := make(chan *types.Receipt) |
| 160 | + doneCh := make(chan struct{}) |
| 161 | + |
| 162 | + waitMined := func(client *ethclient.Client, tx *types.Transaction) { |
| 163 | + mc.lggr.Debugf("Waiting for tx %s to be mined with chain %s", tx.Hash().Hex(), mc.chainName) |
| 164 | + receipt, err := bind.WaitMined(ctx, client, tx) |
| 165 | + if err != nil { |
| 166 | + mc.lggr.Warnf("WaitMined error %v with chain %s", err, mc.chainName) |
| 167 | + return |
| 168 | + } |
| 169 | + select { |
| 170 | + case resultCh <- receipt: |
| 171 | + case <-doneCh: |
| 172 | + return |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + for _, client := range append([]*ethclient.Client{mc.Client}, mc.Backups...) { |
| 177 | + go waitMined(client, tx) |
| 178 | + } |
| 179 | + var receipt *types.Receipt |
| 180 | + select { |
| 181 | + case receipt = <-resultCh: |
| 182 | + close(doneCh) |
| 183 | + mc.lggr.Debugf("Tx %s mined with chain %s", tx.Hash().Hex(), mc.chainName) |
| 184 | + return receipt, nil |
| 185 | + case <-ctx.Done(): |
| 186 | + mc.lggr.Warnf("WaitMined context done %v", ctx.Err()) |
| 187 | + close(doneCh) |
| 188 | + return nil, ctx.Err() |
| 189 | + } |
| 190 | +} |
| 191 | + |
| 192 | +func (mc *MultiClient) retryWithBackups(opName string, op func(*ethclient.Client) error) error { |
| 193 | + var err error |
| 194 | + for i, client := range append([]*ethclient.Client{mc.Client}, mc.Backups...) { |
| 195 | + err2 := retry.Do(func() error { |
| 196 | + mc.lggr.Debugf("Trying op %s with chain %s client index %d", opName, mc.chainName, i) |
| 197 | + err = op(client) |
| 198 | + if err != nil { |
| 199 | + mc.lggr.Warnf("retryable error '%s' for op %s with chain %s client index %d", MaybeDataErr(err), opName, mc.chainName, i) |
| 200 | + return err |
| 201 | + } |
| 202 | + return nil |
| 203 | + }, retry.Attempts(mc.RetryConfig.Attempts), retry.Delay(mc.RetryConfig.Delay)) |
| 204 | + if err2 == nil { |
| 205 | + return nil |
| 206 | + } |
| 207 | + mc.lggr.Infof("Client at index %d failed, trying next client chain %s", i, mc.chainName) |
| 208 | + } |
| 209 | + return errors.Join(err, fmt.Errorf("All backup clients %v failed for chain %s", mc.Backups, mc.chainName)) |
| 210 | +} |
| 211 | + |
| 212 | +func (mc *MultiClient) dialWithRetry(rpc RPC, lggr logger.Logger) (*ethclient.Client, error) { |
| 213 | + endpoint, err := rpc.ToEndpoint() |
| 214 | + if err != nil { |
| 215 | + return nil, err |
| 216 | + } |
| 217 | + |
| 218 | + var client *ethclient.Client |
| 219 | + err = retry.Do(func() error { |
| 220 | + var err2 error |
| 221 | + mc.lggr.Debugf("dialing endpoint '%s' for RPC %s for chain %s", endpoint, rpc.Name, mc.chainName) |
| 222 | + client, err2 = ethclient.Dial(endpoint) |
| 223 | + if err2 != nil { |
| 224 | + lggr.Warnf("retryable error for RPC %s:%s for chain %s %v", rpc.Name, endpoint, mc.chainName, err2) |
| 225 | + return err2 |
| 226 | + } |
| 227 | + return nil |
| 228 | + }, retry.Attempts(RPCDefaultDialRetryAttempts), retry.Delay(RPCDefaultDialRetryDelay)) |
| 229 | + |
| 230 | + if err != nil { |
| 231 | + return nil, errors.Join(err, fmt.Errorf("failed to dial endpoint '%s' for RPC %s for chain %s after retries", endpoint, rpc.Name, mc.chainName)) |
| 232 | + } |
| 233 | + return client, nil |
| 234 | +} |
0 commit comments