Skip to content

Commit 7bf90e6

Browse files
committed
add pre/post decode hooks, remove default post-deployment hook and always execute wait/retry logic
1 parent 9dac0f9 commit 7bf90e6

File tree

6 files changed

+122
-103
lines changed

6 files changed

+122
-103
lines changed

seth/client.go

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,10 +1002,10 @@ func (cl *ContractLoader[T]) LoadContract(name string, address common.Address, a
10021002
return wrapperInitFn(address, cl.Client.Client)
10031003
}
10041004

1005-
// DeployContractWithHooks deploys a smart contract with optional pre- and post-deployment hooks.
1006-
// It allows for custom logic to be executed before and after the contract deployment process,
1007-
// enhancing flexibility and control over the deployment workflow.
1008-
func (m *Client) DeployContractWithHooks(hooks ContractDeploymentHooks, auth *bind.TransactOpts, name string, abi abi.ABI, bytecode []byte, params ...interface{}) (DeploymentData, error) {
1005+
// DeployContract deploys contract using ABI and bytecode passed to it, waits for transaction to be minted and contract really
1006+
// available at the address, so that when the method returns it's safe to interact with it. It also saves the contract address and ABI name
1007+
// to the contract map, so that we can use that, when tracing transactions. It is suggested to use name identical to the name of the contract Solidity file.
1008+
func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.ABI, bytecode []byte, params ...interface{}) (DeploymentData, error) {
10091009
L.Info().
10101010
Msgf("Started deploying %s contract", name)
10111011

@@ -1015,10 +1015,12 @@ func (m *Client) DeployContractWithHooks(hooks ContractDeploymentHooks, auth *bi
10151015
}
10161016
}
10171017

1018-
if hooks.Pre != nil {
1019-
if err := hooks.Pre(auth, name, abi, bytecode, params...); err != nil {
1018+
if m.Cfg.Hooks != nil && m.Cfg.Hooks.ContractDeployment.Pre != nil {
1019+
if err := m.Cfg.Hooks.ContractDeployment.Pre(auth, name, abi, bytecode, params...); err != nil {
10201020
return DeploymentData{}, errors.Wrap(err, "pre-hook failed")
10211021
}
1022+
} else {
1023+
L.Trace().Msg("No pre-contract deployment hook defined. Skipping")
10221024
}
10231025

10241026
address, tx, contract, err := bind.DeployContract(auth, abi, bytecode, m.Client, params...)
@@ -1037,10 +1039,70 @@ func (m *Client) DeployContractWithHooks(hooks ContractDeploymentHooks, auth *bi
10371039
m.ContractStore.AddABI(name, abi)
10381040
}
10391041

1040-
if hooks.Post != nil {
1041-
if err := hooks.Post(m, tx); err != nil {
1042+
if m.Cfg.Hooks != nil && m.Cfg.Hooks.ContractDeployment.Post != nil {
1043+
if err := m.Cfg.Hooks.ContractDeployment.Post(m, tx); err != nil {
10421044
return DeploymentData{}, errors.Wrap(err, "post-hook failed")
10431045
}
1046+
} else {
1047+
L.Trace().Msg("No post-contract deployment hook defined. Skipping")
1048+
}
1049+
1050+
if err := retry.Do(
1051+
func() error {
1052+
ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
1053+
_, err := bind.WaitDeployed(ctx, m.Client, tx)
1054+
cancel()
1055+
1056+
// let's make sure that deployment transaction was successful, before retrying
1057+
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
1058+
ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
1059+
receipt, mineErr := bind.WaitMined(ctx, m.Client, tx)
1060+
if mineErr != nil {
1061+
cancel()
1062+
return mineErr
1063+
}
1064+
cancel()
1065+
1066+
if receipt.Status == 0 {
1067+
return errors.New("deployment transaction was reverted")
1068+
}
1069+
}
1070+
1071+
return err
1072+
}, retry.OnRetry(func(i uint, retryErr error) {
1073+
switch {
1074+
case errors.Is(retryErr, context.DeadlineExceeded):
1075+
replacementTx, replacementErr := prepareReplacementTransaction(m, tx)
1076+
if replacementErr != nil {
1077+
L.Debug().Str("Current error", retryErr.Error()).Str("Replacement error", replacementErr.Error()).Uint("Attempt", i+1).Msg("Failed to prepare replacement transaction for contract deployment. Retrying with the original one")
1078+
return
1079+
}
1080+
tx = replacementTx
1081+
default:
1082+
// do nothing, just wait again until it's mined
1083+
}
1084+
L.Debug().Str("Current error", retryErr.Error()).Uint("Attempt", i+1).Msg("Waiting for contract to be deployed")
1085+
}),
1086+
retry.DelayType(retry.FixedDelay),
1087+
// if gas bump retries are set to 0, we still want to retry 10 times, because what we will be retrying will be other errors (no code at address, etc.)
1088+
// downside is that if retries are enabled and their number is low other retry errors will be retried only that number of times
1089+
// (we could have custom logic for different retry count per error, but that seemed like an overkill, so it wasn't implemented)
1090+
retry.Attempts(func() uint {
1091+
if m.Cfg.GasBumpRetries() != 0 {
1092+
return m.Cfg.GasBumpRetries()
1093+
}
1094+
return 10
1095+
}()),
1096+
retry.RetryIf(func(err error) bool {
1097+
return strings.Contains(strings.ToLower(err.Error()), "no contract code at given address") ||
1098+
strings.Contains(strings.ToLower(err.Error()), "no contract code after deployment") ||
1099+
(m.Cfg.GasBumpRetries() != 0 && errors.Is(err, context.DeadlineExceeded))
1100+
}),
1101+
); err != nil {
1102+
// pass this specific error, so that Decode knows that it's not the actual revert reason
1103+
_, _ = m.Decode(tx, errors.New(ErrContractDeploymentFailed))
1104+
1105+
return DeploymentData{}, wrapErrInMessageWithASuggestion(m.rewriteDeploymentError(err))
10441106
}
10451107

10461108
L.Info().
@@ -1061,17 +1123,6 @@ func (m *Client) DeployContractWithHooks(hooks ContractDeploymentHooks, auth *bi
10611123
return DeploymentData{Address: address, Transaction: tx, BoundContract: contract}, nil
10621124
}
10631125

1064-
// DeployContract deploys contract using ABI and bytecode passed to it, waits for transaction to be minted and contract really
1065-
// available at the address, so that when the method returns it's safe to interact with it. It also saves the contract address and ABI name
1066-
// to the contract map, so that we can use that, when tracing transactions. It is suggested to use name identical to the name of the contract Solidity file.
1067-
func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.ABI, bytecode []byte, params ...interface{}) (DeploymentData, error) {
1068-
hooks := ContractDeploymentHooks{
1069-
Post: RetryingPostDeployHook,
1070-
}
1071-
1072-
return m.DeployContractWithHooks(hooks, auth, name, abi, bytecode, params...)
1073-
}
1074-
10751126
// rewriteDeploymentError makes some known errors more human friendly
10761127
func (m *Client) rewriteDeploymentError(err error) error {
10771128
var maybeRetryErr retry.Error

seth/client_builder.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,11 @@ func (c *ClientBuilder) WithEthClient(ethclient simulated.Client) *ClientBuilder
373373
return c
374374
}
375375

376+
func (c *ClientBuilder) WithHooks(hooks Hooks) *ClientBuilder {
377+
c.config.Hooks = &hooks
378+
return c
379+
}
380+
376381
// WithReadOnlyMode sets the client to read-only mode. It removes all private keys from all Networks and disables nonce protection and ephemeral addresses.
377382
func (c *ClientBuilder) WithReadOnlyMode() *ClientBuilder {
378383
c.readonly = true

seth/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type Config struct {
4747
ephemeral bool
4848
RPCHeaders http.Header
4949
ethclient simulated.Client
50+
Hooks *Hooks
5051

5152
// external fields
5253
// ArtifactDir is the directory where all artifacts generated by seth are stored (e.g. transaction traces)

seth/contracts.go

Lines changed: 0 additions & 84 deletions
This file was deleted.

seth/decode.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ func (m *Client) DecodeTx(tx *types.Transaction) (*DecodedTransaction, error) {
148148

149149
l := L.With().Str("Transaction", tx.Hash().Hex()).Logger()
150150

151+
if m.Cfg.Hooks != nil && m.Cfg.Hooks.Decode.Pre != nil {
152+
if err := m.Cfg.Hooks.Decode.Pre(m); err != nil {
153+
return nil, err
154+
}
155+
} else {
156+
l.Trace().
157+
Msg("No pre-decode hook found. Skipping")
158+
}
159+
151160
var receipt *types.Receipt
152161
var err error
153162
tx, receipt, err = m.waitUntilMined(l, tx)
@@ -162,6 +171,15 @@ func (m *Client) DecodeTx(tx *types.Transaction) (*DecodedTransaction, error) {
162171

163172
decoded, decodeErr := m.decodeTransaction(l, tx, receipt)
164173

174+
if m.Cfg.Hooks != nil && m.Cfg.Hooks.Decode.Post != nil {
175+
if err := m.Cfg.Hooks.Decode.Post(m, decoded, decodeErr); err != nil {
176+
return nil, err
177+
}
178+
} else {
179+
l.Trace().
180+
Msg("No post-decode hook found. Skipping")
181+
}
182+
165183
if decodeErr != nil && errors.Is(decodeErr, errors.New(ErrNoABIMethod)) {
166184
m.handleTxDecodingError(l, *decoded, decodeErr)
167185
return decoded, revertErr

seth/hooks.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package seth
2+
3+
import (
4+
"github.com/ethereum/go-ethereum/accounts/abi"
5+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
6+
"github.com/ethereum/go-ethereum/core/types"
7+
)
8+
9+
type Hooks struct {
10+
ContractDeployment ContractDeploymentHooks
11+
Decode DecodeHooks
12+
}
13+
14+
type PreDeployHook func(auth *bind.TransactOpts, name string, abi abi.ABI, bytecode []byte, params ...interface{}) error
15+
type PostDeployHook func(client *Client, tx *types.Transaction) error
16+
17+
type ContractDeploymentHooks struct {
18+
Pre PreDeployHook
19+
Post PostDeployHook
20+
}
21+
22+
type PreDecodeHook func(client *Client) error
23+
type PostDecodeHook func(client *Client, decodedTx *DecodedTransaction, decodedErr error) error
24+
25+
type DecodeHooks struct {
26+
Pre PreDecodeHook
27+
Post PostDecodeHook
28+
}

0 commit comments

Comments
 (0)