Skip to content

Commit 8c6b0eb

Browse files
authored
feat: Adds an EVM RPC Provider (#186)
This adds a simple EVM RPC provider that can be used to interact with Ethereum-like chains. It supports loading a private key from raw, with KMS integration planned for the future.
1 parent 63f765e commit 8c6b0eb

16 files changed

+1305
-5
lines changed

.changeset/shy-emus-create.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
Adds an EVM RPC Chain Provider

chain/evm/evm_chain.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import (
1313
chain_common "github.com/smartcontractkit/chainlink-deployments-framework/chain/internal/common"
1414
)
1515

16+
// ConfirmFunc is a function that takes a transaction, waits for the transaction to be confirmed,
17+
// and returns the block number and an error.
18+
type ConfirmFunc func(tx *types.Transaction) (uint64, error)
19+
1620
// OnchainClient is an EVM chain client.
1721
// For EVM specifically we can use existing geth interface to abstract chain clients.
1822
type OnchainClient interface {
@@ -30,7 +34,7 @@ type Chain struct {
3034
Client OnchainClient
3135
// Note the Sign function can be abstract supporting a variety of key storage mechanisms (e.g. KMS etc).
3236
DeployerKey *bind.TransactOpts
33-
Confirm func(tx *types.Transaction) (uint64, error)
37+
Confirm ConfirmFunc
3438
// Users are a set of keys that can be used to interact with the chain.
3539
// These are distinct from the deployer key.
3640
Users []*bind.TransactOpts

chain/evm/provider/confirm_functor.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
"time"
8+
9+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/core/types"
12+
13+
chain_selectors "github.com/smartcontractkit/chain-selectors"
14+
15+
"github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
16+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
17+
)
18+
19+
// ConfirmFunctor is an interface for creating a confirmation function for transactions on the
20+
// EVM chain.
21+
type ConfirmFunctor interface {
22+
// Generate returns a function that confirms transactions on the EVM chain.
23+
Generate(
24+
ctx context.Context, selector uint64, client evm.OnchainClient, from common.Address,
25+
) (evm.ConfirmFunc, error)
26+
}
27+
28+
// ConfirmFuncGeth returns a ConfirmFunctor that uses the Geth client to confirm transactions.
29+
func ConfirmFuncGeth(waitMinedTimeout time.Duration) ConfirmFunctor {
30+
return &confirmFuncGeth{
31+
waitMinedTimeout: waitMinedTimeout,
32+
}
33+
}
34+
35+
// confirmFuncGeth implements the ConfirmFunctor interface which generates a confirmation function
36+
// for transactions using the Geth client.
37+
type confirmFuncGeth struct {
38+
waitMinedTimeout time.Duration
39+
}
40+
41+
// Generate returns a function that confirms transactions using the Geth client.
42+
func (g *confirmFuncGeth) Generate(
43+
ctx context.Context, selector uint64, client evm.OnchainClient, from common.Address,
44+
) (evm.ConfirmFunc, error) {
45+
return func(tx *types.Transaction) (uint64, error) {
46+
var blockNum uint64
47+
if tx == nil {
48+
return 0, fmt.Errorf("tx was nil, nothing to confirm for selector: %d", selector)
49+
}
50+
51+
ctxTimeout, cancel := context.WithTimeout(ctx, g.waitMinedTimeout)
52+
defer cancel()
53+
54+
receipt, err := bind.WaitMined(ctxTimeout, client, tx)
55+
if err != nil {
56+
return 0, fmt.Errorf("tx %s failed to confirm for selector %d: %w",
57+
tx.Hash().Hex(), selector, err,
58+
)
59+
}
60+
if receipt == nil {
61+
return blockNum, fmt.Errorf("receipt was nil for tx %s for selector %d",
62+
tx.Hash().Hex(), selector,
63+
)
64+
}
65+
66+
blockNum = receipt.BlockNumber.Uint64()
67+
68+
if receipt.Status == 0 {
69+
reason, err := getErrorReasonFromTx(ctxTimeout, client, from, tx, receipt)
70+
if err == nil && reason != "" {
71+
return 0, fmt.Errorf("tx %s reverted for selector %d: %s",
72+
tx.Hash().Hex(), selector, reason,
73+
)
74+
}
75+
76+
return blockNum, fmt.Errorf("tx %s reverted, could not decode error reason for selector %d",
77+
tx.Hash().Hex(), selector,
78+
)
79+
}
80+
81+
return blockNum, nil
82+
}, nil
83+
}
84+
85+
// ConfirmFuncSeth returns a ConfirmFunctor that uses the Seth client to confirm transactions.
86+
// It requires the RPC URL, a list of directories where Geth wrappers are located, and an optional
87+
// configuration file path for the Seth client. If you do not have a configuration file, you can
88+
// pass an empty string.
89+
func ConfirmFuncSeth(
90+
rpcURL string, waitMinedTimeout time.Duration, gethWrapperDirs []string, configFilePath string,
91+
) ConfirmFunctor {
92+
return &confirmFuncSeth{
93+
rpcURL: rpcURL,
94+
waitMinedTimeout: waitMinedTimeout,
95+
gethWrappersDirs: gethWrapperDirs,
96+
configFilePath: configFilePath,
97+
}
98+
}
99+
100+
// confirmFuncSeth implements the ConfirmFunctor interface which generates a confirmation function
101+
// for transactions using the Seth client.
102+
type confirmFuncSeth struct {
103+
rpcURL string
104+
waitMinedTimeout time.Duration
105+
gethWrappersDirs []string
106+
configFilePath string
107+
}
108+
109+
// Generate returns a function that confirms transactions using the Seth client. The provided
110+
// client must be a MultiClient.
111+
func (g *confirmFuncSeth) Generate(
112+
ctx context.Context, selector uint64, client evm.OnchainClient, from common.Address,
113+
) (evm.ConfirmFunc, error) {
114+
// Convert the client to a MultiClient because we need to use the multi-client's WaitMined
115+
// method.
116+
multiClient, ok := client.(*deployment.MultiClient)
117+
if !ok {
118+
return nil, fmt.Errorf("expected client to be of type *deployment.MultiClient, got %T", client)
119+
}
120+
121+
// Get the ChainID from the selector
122+
chainIDStr, err := chain_selectors.GetChainIDFromSelector(selector)
123+
if err != nil {
124+
return nil, fmt.Errorf("failed to get chain ID from selector %d: %w", selector, err)
125+
}
126+
127+
chainID, err := strconv.ParseUint(chainIDStr, 10, 64)
128+
if err != nil {
129+
return nil, fmt.Errorf("failed to parse chain ID %s: %w", chainIDStr, err)
130+
}
131+
132+
// Setup the seth client
133+
sethClient, err := newSethClient(
134+
g.rpcURL, chainID, g.gethWrappersDirs, g.configFilePath,
135+
)
136+
if err != nil {
137+
return nil, fmt.Errorf("failed to setup seth client: %w", err)
138+
}
139+
140+
return func(tx *types.Transaction) (uint64, error) {
141+
ctxTimeout, cancel := context.WithTimeout(ctx, g.waitMinedTimeout)
142+
defer cancel()
143+
144+
if _, err := multiClient.WaitMined(ctxTimeout, tx); err != nil {
145+
return 0, err
146+
}
147+
148+
decoded, err := sethClient.DecodeTx(tx)
149+
if err != nil {
150+
return 0, err
151+
}
152+
153+
if decoded.Receipt == nil {
154+
return 0, fmt.Errorf("no receipt found for transaction %s even though it wasn't reverted. This should not happen", tx.Hash().String())
155+
}
156+
157+
return decoded.Receipt.BlockNumber.Uint64(), nil
158+
}, nil
159+
}

0 commit comments

Comments
 (0)