Skip to content

Commit dbcd003

Browse files
committed
allow to pass pre/post contract deployment hooks
1 parent 3514fc5 commit dbcd003

File tree

1 file changed

+190
-61
lines changed

1 file changed

+190
-61
lines changed

seth/client.go

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

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) {
1005+
type ContractDeploymentHooks struct {
1006+
Pre func(auth *bind.TransactOpts, name string, abi abi.ABI, bytecode []byte, params ...interface{}) error
1007+
Post func(client *Client, tx *types.Transaction) error
1008+
}
1009+
1010+
func (m *Client) DeployContractWithHooks(hooks ContractDeploymentHooks, auth *bind.TransactOpts, name string, abi abi.ABI, bytecode []byte, params ...interface{}) (DeploymentData, error) {
10091011
L.Info().
10101012
Msgf("Started deploying %s contract", name)
10111013

@@ -1015,6 +1017,12 @@ func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.AB
10151017
}
10161018
}
10171019

1020+
if hooks.Pre != nil {
1021+
if err := hooks.Pre(auth, name, abi, bytecode, params...); err != nil {
1022+
return DeploymentData{}, errors.Wrap(err, "pre-hook failed")
1023+
}
1024+
}
1025+
10181026
address, tx, contract, err := bind.DeployContract(auth, abi, bytecode, m.Client, params...)
10191027
if err != nil {
10201028
return DeploymentData{}, wrapErrInMessageWithASuggestion(err)
@@ -1031,63 +1039,10 @@ func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.AB
10311039
m.ContractStore.AddABI(name, abi)
10321040
}
10331041

1034-
// retry is needed both for gas bumping and for waiting for deployment to finish (sometimes there's no code at address the first time we check)
1035-
if err := retry.Do(
1036-
func() error {
1037-
ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
1038-
_, err := bind.WaitDeployed(ctx, m.Client, tx)
1039-
cancel()
1040-
1041-
// let's make sure that deployment transaction was successful, before retrying
1042-
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
1043-
ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
1044-
receipt, mineErr := bind.WaitMined(ctx, m.Client, tx)
1045-
if mineErr != nil {
1046-
cancel()
1047-
return mineErr
1048-
}
1049-
cancel()
1050-
1051-
if receipt.Status == 0 {
1052-
return errors.New("deployment transaction was reverted")
1053-
}
1054-
}
1055-
1056-
return err
1057-
}, retry.OnRetry(func(i uint, retryErr error) {
1058-
switch {
1059-
case errors.Is(retryErr, context.DeadlineExceeded):
1060-
replacementTx, replacementErr := prepareReplacementTransaction(m, tx)
1061-
if replacementErr != nil {
1062-
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")
1063-
return
1064-
}
1065-
tx = replacementTx
1066-
default:
1067-
// do nothing, just wait again until it's mined
1068-
}
1069-
L.Debug().Str("Current error", retryErr.Error()).Uint("Attempt", i+1).Msg("Waiting for contract to be deployed")
1070-
}),
1071-
retry.DelayType(retry.FixedDelay),
1072-
// 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.)
1073-
// downside is that if retries are enabled and their number is low other retry errors will be retried only that number of times
1074-
// (we could have custom logic for different retry count per error, but that seemed like an overkill, so it wasn't implemented)
1075-
retry.Attempts(func() uint {
1076-
if m.Cfg.GasBumpRetries() != 0 {
1077-
return m.Cfg.GasBumpRetries()
1078-
}
1079-
return 10
1080-
}()),
1081-
retry.RetryIf(func(err error) bool {
1082-
return strings.Contains(strings.ToLower(err.Error()), "no contract code at given address") ||
1083-
strings.Contains(strings.ToLower(err.Error()), "no contract code after deployment") ||
1084-
(m.Cfg.GasBumpRetries() != 0 && errors.Is(err, context.DeadlineExceeded))
1085-
}),
1086-
); err != nil {
1087-
// pass this specific error, so that Decode knows that it's not the actual revert reason
1088-
_, _ = m.Decode(tx, errors.New(ErrContractDeploymentFailed))
1089-
1090-
return DeploymentData{}, wrapErrInMessageWithASuggestion(m.rewriteDeploymentError(err))
1042+
if hooks.Post != nil {
1043+
if err := hooks.Post(m, tx); err != nil {
1044+
return DeploymentData{}, errors.Wrap(err, "post-hook failed")
1045+
}
10911046
}
10921047

10931048
L.Info().
@@ -1108,6 +1063,180 @@ func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.AB
11081063
return DeploymentData{Address: address, Transaction: tx, BoundContract: contract}, nil
11091064
}
11101065

1066+
// DeployContract deploys contract using ABI and bytecode passed to it, waits for transaction to be minted and contract really
1067+
// 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
1068+
// 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.
1069+
func (m *Client) DeployContract(auth *bind.TransactOpts, name string, abi abi.ABI, bytecode []byte, params ...interface{}) (DeploymentData, error) {
1070+
hooks := ContractDeploymentHooks{
1071+
Post: func(client *Client, tx *types.Transaction) error {
1072+
err := retry.Do(
1073+
func() error {
1074+
ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
1075+
_, err := bind.WaitDeployed(ctx, m.Client, tx)
1076+
cancel()
1077+
1078+
// let's make sure that deployment transaction was successful, before retrying
1079+
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
1080+
ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
1081+
receipt, mineErr := bind.WaitMined(ctx, m.Client, tx)
1082+
if mineErr != nil {
1083+
cancel()
1084+
return mineErr
1085+
}
1086+
cancel()
1087+
1088+
if receipt.Status == 0 {
1089+
return errors.New("deployment transaction was reverted")
1090+
}
1091+
}
1092+
1093+
return err
1094+
}, retry.OnRetry(func(i uint, retryErr error) {
1095+
switch {
1096+
case errors.Is(retryErr, context.DeadlineExceeded):
1097+
replacementTx, replacementErr := prepareReplacementTransaction(m, tx)
1098+
if replacementErr != nil {
1099+
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")
1100+
return
1101+
}
1102+
tx = replacementTx
1103+
default:
1104+
// do nothing, just wait again until it's mined
1105+
}
1106+
L.Debug().Str("Current error", retryErr.Error()).Uint("Attempt", i+1).Msg("Waiting for contract to be deployed")
1107+
}),
1108+
retry.DelayType(retry.FixedDelay),
1109+
// 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.)
1110+
// downside is that if retries are enabled and their number is low other retry errors will be retried only that number of times
1111+
// (we could have custom logic for different retry count per error, but that seemed like an overkill, so it wasn't implemented)
1112+
retry.Attempts(func() uint {
1113+
if m.Cfg.GasBumpRetries() != 0 {
1114+
return m.Cfg.GasBumpRetries()
1115+
}
1116+
return 10
1117+
}()),
1118+
retry.RetryIf(func(err error) bool {
1119+
return strings.Contains(strings.ToLower(err.Error()), "no contract code at given address") ||
1120+
strings.Contains(strings.ToLower(err.Error()), "no contract code after deployment") ||
1121+
(m.Cfg.GasBumpRetries() != 0 && errors.Is(err, context.DeadlineExceeded))
1122+
}),
1123+
)
1124+
1125+
if err != nil {
1126+
// pass this specific error, so that Decode knows that it's not the actual revert reason
1127+
_, _ = m.Decode(tx, errors.New(ErrContractDeploymentFailed))
1128+
1129+
return wrapErrInMessageWithASuggestion(m.rewriteDeploymentError(err))
1130+
}
1131+
1132+
return nil
1133+
},
1134+
}
1135+
1136+
return m.DeployContractWithHooks(hooks, auth, name, abi, bytecode, params...)
1137+
1138+
// L.Info().
1139+
// Msgf("Started deploying %s contract", name)
1140+
1141+
// if auth.Context != nil {
1142+
// if err, ok := auth.Context.Value(ContextErrorKey{}).(error); ok {
1143+
// return DeploymentData{}, errors.Wrapf(err, "aborted contract deployment for %s, because context passed in transaction options had an error set", name)
1144+
// }
1145+
// }
1146+
1147+
// address, tx, contract, err := bind.DeployContract(auth, abi, bytecode, m.Client, params...)
1148+
// if err != nil {
1149+
// return DeploymentData{}, wrapErrInMessageWithASuggestion(err)
1150+
// }
1151+
1152+
// L.Info().
1153+
// Str("Address", address.Hex()).
1154+
// Str("TXHash", tx.Hash().Hex()).
1155+
// Msgf("Waiting for %s contract deployment to finish", name)
1156+
1157+
// m.ContractAddressToNameMap.AddContract(address.Hex(), name)
1158+
1159+
// if _, ok := m.ContractStore.GetABI(name); !ok {
1160+
// m.ContractStore.AddABI(name, abi)
1161+
// }
1162+
1163+
// // retry is needed both for gas bumping and for waiting for deployment to finish (sometimes there's no code at address the first time we check)
1164+
// if err := retry.Do(
1165+
// func() error {
1166+
// ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
1167+
// _, err := bind.WaitDeployed(ctx, m.Client, tx)
1168+
// cancel()
1169+
1170+
// // let's make sure that deployment transaction was successful, before retrying
1171+
// if err != nil && !errors.Is(err, context.DeadlineExceeded) {
1172+
// ctx, cancel := context.WithTimeout(context.Background(), m.Cfg.Network.TxnTimeout.Duration())
1173+
// receipt, mineErr := bind.WaitMined(ctx, m.Client, tx)
1174+
// if mineErr != nil {
1175+
// cancel()
1176+
// return mineErr
1177+
// }
1178+
// cancel()
1179+
1180+
// if receipt.Status == 0 {
1181+
// return errors.New("deployment transaction was reverted")
1182+
// }
1183+
// }
1184+
1185+
// return err
1186+
// }, retry.OnRetry(func(i uint, retryErr error) {
1187+
// switch {
1188+
// case errors.Is(retryErr, context.DeadlineExceeded):
1189+
// replacementTx, replacementErr := prepareReplacementTransaction(m, tx)
1190+
// if replacementErr != nil {
1191+
// 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")
1192+
// return
1193+
// }
1194+
// tx = replacementTx
1195+
// default:
1196+
// // do nothing, just wait again until it's mined
1197+
// }
1198+
// L.Debug().Str("Current error", retryErr.Error()).Uint("Attempt", i+1).Msg("Waiting for contract to be deployed")
1199+
// }),
1200+
// retry.DelayType(retry.FixedDelay),
1201+
// // 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.)
1202+
// // downside is that if retries are enabled and their number is low other retry errors will be retried only that number of times
1203+
// // (we could have custom logic for different retry count per error, but that seemed like an overkill, so it wasn't implemented)
1204+
// retry.Attempts(func() uint {
1205+
// if m.Cfg.GasBumpRetries() != 0 {
1206+
// return m.Cfg.GasBumpRetries()
1207+
// }
1208+
// return 10
1209+
// }()),
1210+
// retry.RetryIf(func(err error) bool {
1211+
// return strings.Contains(strings.ToLower(err.Error()), "no contract code at given address") ||
1212+
// strings.Contains(strings.ToLower(err.Error()), "no contract code after deployment") ||
1213+
// (m.Cfg.GasBumpRetries() != 0 && errors.Is(err, context.DeadlineExceeded))
1214+
// }),
1215+
// ); err != nil {
1216+
// // pass this specific error, so that Decode knows that it's not the actual revert reason
1217+
// _, _ = m.Decode(tx, errors.New(ErrContractDeploymentFailed))
1218+
1219+
// return DeploymentData{}, wrapErrInMessageWithASuggestion(m.rewriteDeploymentError(err))
1220+
// }
1221+
1222+
// L.Info().
1223+
// Str("Address", address.Hex()).
1224+
// Str("TXHash", tx.Hash().Hex()).
1225+
// Msgf("Deployed %s contract", name)
1226+
1227+
// if !m.Cfg.ShouldSaveDeployedContractMap() {
1228+
// return DeploymentData{Address: address, Transaction: tx, BoundContract: contract}, nil
1229+
// }
1230+
1231+
// if err := SaveDeployedContract(m.Cfg.ContractMapFile, name, address.Hex()); err != nil {
1232+
// L.Warn().
1233+
// Err(err).
1234+
// Msg("Failed to save deployed contract address to file")
1235+
// }
1236+
1237+
// return DeploymentData{Address: address, Transaction: tx, BoundContract: contract}, nil
1238+
}
1239+
11111240
// rewriteDeploymentError makes some known errors more human friendly
11121241
func (m *Client) rewriteDeploymentError(err error) error {
11131242
var maybeRetryErr retry.Error

0 commit comments

Comments
 (0)