package anvil import ( "errors" "fmt" "github.com/ImVexed/fasturl" "github.com/ethereum/go-ethereum/accounts" "github.com/ory/dockertest/v3/docker" "github.com/synapsecns/sanguine/core/processlog" "github.com/tyler-smith/go-bip39" "golang.org/x/exp/slices" "reflect" "runtime" "strconv" "strings" "time" ) func _() { // make sure we don't panic NewAnvilOptionBuilder() } const defaultMnemonic = "tag volcano eight thank tide danger coast health above argue embrace heavy" // NewAnvilOptionBuilder creates a new option builder. func NewAnvilOptionBuilder() *OptionBuilder { optionsBuilder := &OptionBuilder{} // set default non-anvil values optionsBuilder.SetMaxWaitTime(time.Minute * 2) optionsBuilder.SetRestartPolicy(Autoremove) optionsBuilder.SetExpirySeconds(900) optionsBuilder.SetAccounts(10) optionsBuilder.SetBlockTime(0) optionsBuilder.SetBalance(10000) optionsBuilder.SetGasLimit(gasLimit * 5) optionsBuilder.SetDerivationPath(accounts.DefaultBaseDerivationPath) // note: ordinarily we strongly discourage using panics, but the setup should be deterministic. // to prevent unintended behavior, we have a boot time static assertion check above to make sure this will not panic err := optionsBuilder.SetHardfork(Latest) if err != nil { panic(err) } err = optionsBuilder.SetOrder(Fifo) if err != nil { panic(err) } optionsBuilder.SetBaseFee(1) optionsBuilder.SetStepsTracing(true) optionsBuilder.SetTimestamp(0) optionsBuilder.SetAllowOrigin("*") optionsBuilder.SetHost("0.0.0.0") err = optionsBuilder.SetMnemonic(defaultMnemonic) if err != nil { panic(err) } return optionsBuilder } // tagName is the tag name used for anvil options building. const tagName = "anvil" type evmOptions struct { ForkURL string `anvil:"fork-url"` BlockNumber uint64 `anvil:"fork-block-number"` ForkRetryBackoff uint `anvil:"fork-retry-backoff"` Retries uint16 `anvil:"retries"` Timeout uint64 `anvil:"timeout"` ComputeUnitsPerSecond uint64 `anvil:"compute-units-per-second"` NoRateLimitUnitsPerSecond bool `anvil:"no-rate-limit-units-per-second"` NoStorageCaching bool `anvil:"no-storage-caching"` } type generalOptions struct { Accounts uint8 `anvil:"accounts"` BlockTime uint64 `anvil:"block-time"` Balance uint64 `anvil:"balance"` DerivationPath accounts.DerivationPath `anvil:"derivation-path"` Hardfork Hardfork `anvil:"hardfork"` Order Order `anvil:"order"` StepsTracing bool `anvil:"steps-tracing"` Silent bool `anvil:"silent"` Timestamp uint64 `anvil:"timestamp"` Mnemonic string `anvil:"mnemonic"` NoMining bool `anvil:"no-mining"` } type executorOptions struct { BaseFee uint64 `anvil:"base-fee"` BlockBaseFeePerGas uint64 `anvil:"block-base-fee-per-gas"` ChainID uint64 `anvil:"chain-id"` CodeSizeLimit uint64 `anvil:"code-size-limit"` GasLimit uint64 `anvil:"gas-limit"` GasPrice uint64 `anvil:"gas-price"` } type serverOptions struct { AllowOrigin string `anvil:"allow-origin"` disableCors bool `anvil:"disable-cors"` PruneHistory bool `anvil:"prune-history"` Host string `anvil:"host"` } // nonAnvilOptions options are options that are not part of the anvil spec, but are used by the anvil backend. // it's important that these do not include the anvil annotation. type nonAnvilOptions struct { // enableOtterscan specifies wether or not to start a container for otterscan enableOtterscan bool // maxWait is the maximum time to wait for the server to start maxWait time.Duration // expirySeconds is the number of seconds to wait before expiring a request // set to 0 to disable expirySeconds uint // autoremove is wether the container should be deleted after the run autoremove bool // restartPolicy restarts a policy restartPolicy *docker.RestartPolicy // processOptions are options for the process logger processOptions []processlog.StdStreamLogArgsOption } // OptionBuilder is a builder for anvil options. type OptionBuilder struct { generalOptions evmOptions executorOptions serverOptions nonAnvilOptions } // ========= General Options ========= // OtterscanEnabled sets whether or not to enable otterscan. // Note: for this to work correctly, the underlying node we fork from must also support the ots namespace. func (o *OptionBuilder) OtterscanEnabled(enabled bool) { o.enableOtterscan = enabled } // SetAccounts sets the number of Accounts to create (defaults to 10). func (o *OptionBuilder) SetAccounts(accountCount uint8) { o.Accounts = accountCount } // GetAccounts gets the number of accounts to use. func (o *OptionBuilder) GetAccounts() uint8 { return o.Accounts } // SetBlockTime sets the block time (defaults to 1 second). // if block time is 0 or less than a second, no mining is used. func (o *OptionBuilder) SetBlockTime(blockTime time.Duration) { o.BlockTime = uint64(blockTime / time.Second) } // SetBalance sets the Balance of each account in ether (defaults to 1000 ether). func (o *OptionBuilder) SetBalance(balance uint64) { o.Balance = balance } // SetNoMining sets whether or not to mine blocks. // this is automatically set and must be disabled for other options to be sent. // if disabled, this can cause concurrency issues since only one account can be impersonated at a time. func (o *OptionBuilder) SetNoMining(noMining bool) { o.NoMining = noMining } // SetDerivationPath sets the derivation path to use for the Accounts. // defaults to m/44'/60'/0'/0. func (o *OptionBuilder) SetDerivationPath(derivationPath accounts.DerivationPath) { o.DerivationPath = derivationPath } // GetDerivationPath returns the derivation path to use for the Accounts. func (o *OptionBuilder) GetDerivationPath() accounts.DerivationPath { return o.DerivationPath } // SetHardfork sets the Hardfork to use for the chain. func (o *OptionBuilder) SetHardfork(hardfork Hardfork) error { if !slices.Contains(allHardforks, hardfork) { return fmt.Errorf("invalid Hardfork: %s", hardfork.String()) } o.Hardfork = hardfork return nil } // GetHardfork returns the Hardfork to use for the chain. func (o *OptionBuilder) GetHardfork() Hardfork { return o.Hardfork } // GetMnemonic returns the mnemonic to use for the chain. func (o *OptionBuilder) GetMnemonic() string { return o.Mnemonic } // SetInit sets the genesis file to use for the chain. // Warning: this option is not currently supported. func (o *OptionBuilder) SetInit(_ string) error { return fmt.Errorf("unsupported option (not yet implemented): %s", getFunctionName(o.SetInit)) } // SetMnemonic sets the mnemonic to use for the chain. func (o *OptionBuilder) SetMnemonic(mnemonic string) error { o.Mnemonic = mnemonic if !bip39.IsMnemonicValid(mnemonic) { return fmt.Errorf("invalid mnemonic: %s", mnemonic) } return nil } // SetOrder sets the transaction Order to use for the chain. // Defaults to Fees. func (o *OptionBuilder) SetOrder(order Order) error { if !slices.Contains(allOrders, order) { return fmt.Errorf("invalid Order: %s", order.String()) } o.Order = order return nil } // SetStepsTracing enables or disables steps tracing. // Defaults to enabled. func (o *OptionBuilder) SetStepsTracing(enabled bool) { o.StepsTracing = enabled } // SetIPC sets the IPC path to use for the chain. // This option is not currently supported. func (o *OptionBuilder) SetIPC(path string) error { return fmt.Errorf("unsupported option (not yet implemented): %s", getFunctionName(o.SetIPC)) } // SetSilent enables or disables Silent boot. func (o *OptionBuilder) SetSilent(silent bool) { o.Silent = silent } // SetTimestamp sets the Timestamp of the genesis block. This option is ignored if set to 0. func (o *OptionBuilder) SetTimestamp(timestamp uint64) { o.Timestamp = timestamp } // ========= EVM Options ========= // SetForkURL sets the fork URL to use for the chain. func (o *OptionBuilder) SetForkURL(url string) error { _, err := fasturl.ParseURL(url) if err != nil { return fmt.Errorf("invalid fork url: %s", url) } o.ForkURL = url return nil } // SetForkBlockNumber sets the fork block number to use for the chain. // only valid if SetForkURL is also called. func (o *OptionBuilder) SetForkBlockNumber(block uint64) { o.BlockNumber = block } // SetForkRetryBackoff sets the fork retry backoff to use for the chain. func (o *OptionBuilder) SetForkRetryBackoff(backoff uint) { o.ForkRetryBackoff = backoff } // SetRetries sets the number of retries to use for forked rpc requests. func (o *OptionBuilder) SetRetries(retries uint16) { o.Retries = retries } // SetTimeout sets the timeout to use for forked rpc requests in milliseconds. func (o *OptionBuilder) SetTimeout(timeout time.Duration) { o.Timeout = uint64(timeout / time.Millisecond) } // SetComputeUnitsPerSecond sets the CUPS per second, see: https://docs.alchemy.com/reference/compute-units func (o *OptionBuilder) SetComputeUnitsPerSecond(units uint64) { o.ComputeUnitsPerSecond = units } // SetRateLimitUnitsPerSecondEnabled enables or disables rate limiting of CUPS. func (o *OptionBuilder) SetRateLimitUnitsPerSecondEnabled(enabled bool) { o.NoRateLimitUnitsPerSecond = enabled } // SetStorageCachingEnabled enables or disables storage caching. func (o *OptionBuilder) SetStorageCachingEnabled(enabled bool) { o.NoStorageCaching = enabled } // SetBaseFee sets the base fee for the chain. func (o *OptionBuilder) SetBaseFee(baseFee uint64) { o.BaseFee = baseFee } // SetBlockBaseFeePerGas sets the block base fee per gas for the chain. func (o *OptionBuilder) SetBlockBaseFeePerGas(baseFee uint64) { o.BlockBaseFeePerGas = baseFee } // SetChainID sets the chain id to use in foundry. func (o *OptionBuilder) SetChainID(chainID uint64) { o.ChainID = chainID } // SetCodeSizeLimit sets the code size limit for the chain. func (o *OptionBuilder) SetCodeSizeLimit(codeSizeLimit uint64) { o.CodeSizeLimit = codeSizeLimit } // SetGasLimit sets the gas limit per block for the chain. func (o *OptionBuilder) SetGasLimit(gasLimit uint64) { o.GasLimit = gasLimit } // SetGasPrice sets the gas price for the chain. func (o *OptionBuilder) SetGasPrice(gasPrice uint64) { o.GasPrice = gasPrice } // SetAllowOrigin sets the allow origin for the chain. // defaults to "*". func (o *OptionBuilder) SetAllowOrigin(allowOrigin string) { o.AllowOrigin = allowOrigin } // SetDisableCors disables cors for the chain. func (o *OptionBuilder) SetDisableCors(disableCors bool) { o.disableCors = disableCors } // SetPruneHistory enables or disables pruning of history. // defaults to false. func (o *OptionBuilder) SetPruneHistory(pruneHistory bool) { o.PruneHistory = pruneHistory } // SetHost sets the host to listen on. func (o *OptionBuilder) SetHost(host string) { o.Host = host } // SetMaxWaitTime sets the max wait time for the docker container(s) to start. func (o *OptionBuilder) SetMaxWaitTime(maxWait time.Duration) { o.maxWait = maxWait } // SetExpirySeconds sets the expiry seconds for the docker containers(s) to be removed. func (o *OptionBuilder) SetExpirySeconds(expirySeconds uint) { o.expirySeconds = expirySeconds } // SetProcessLogOptions sets the process log options for the docker container(s). func (o *OptionBuilder) SetProcessLogOptions(customOpts ...processlog.StdStreamLogArgsOption) { o.processOptions = customOpts } // SetRestartPolicy sets the restart policy for the docker container(s). func (o *OptionBuilder) SetRestartPolicy(restartPolicy RestartPolicy) { alwaysRestart := docker.AlwaysRestart() failure := docker.RestartOnFailure(10) unlessStopped := docker.RestartUnlessStopped() neverRestart := docker.NeverRestart() o.autoremove = false switch restartPolicy { case Restart: o.restartPolicy = &alwaysRestart case Failure: o.restartPolicy = &failure case UnlessStopped: o.restartPolicy = &unlessStopped case No: o.restartPolicy = &neverRestart case Autoremove: o.restartPolicy = nil o.autoremove = true } } // RestartPolicy defines the restart policy for the docker container. type RestartPolicy string const ( // Restart defines the restart policy for the docker container. Restart RestartPolicy = "always" // Failure defines the restart policy for the docker container. Failure = "on-failure" // UnlessStopped defines the restart policy for the docker container. UnlessStopped = "unless-stopped" // No defines the restart policy for the docker container. No = "no" // Autoremove removes the container when it's finished. Autoremove = "autoremove" ) // Build converts the option builder into a list of command line parameters. // for use in anvil. func (o *OptionBuilder) Build() (args []string, err error) { v := reflect.ValueOf(*o) fields, err := getFields(v, make(map[string]string)) if err != nil { return []string{}, fmt.Errorf("error getting fields: %w", err) } for key, value := range fields { args = append(args, fmt.Sprintf("--%s %s", key, value)) } return args, nil } func getFields(v reflect.Value, options map[string]string) (_ map[string]string, err error) { t := v.Type() for i := 0; i < t.NumField(); i++ { // Get the field, returns https://golang.org/pkg/reflect/#StructField field := t.Field(i) value := v.Field(i) // iterate over all available tags in the sub field, we'll ignore parent fields if field.Type.Kind() == reflect.Struct { options, err = getFields(value, options) if err != nil { return map[string]string{}, fmt.Errorf("error getting fields: %w", err) } } tag := field.Tag.Get(tagName) if tag == "" { continue } // check if the field has the default value if fieldIsEmpty(value) { continue } castField, err := valueToString(value) if err != nil { return map[string]string{}, fmt.Errorf("error casting field of type %v: %w", value.Kind(), err) } options[tag] = castField } return options, nil } // nolint: cyclop func fieldIsEmpty(v reflect.Value) bool { //nolint: exhaustive switch v.Kind() { case reflect.Bool: if !v.Bool() { return true } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if v.Int() == 0 { return true } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: if v.Uint() == 0 { return true } case reflect.Float32, reflect.Float64: if v.Float() == 0.0 { return true } case reflect.String: if v.String() == "" { return true } case reflect.Slice, reflect.Map: if v.IsNil() || v.Len() == 0 { return true } case reflect.Pointer: return v.IsNil() case reflect.Struct: if reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) { return true } } return false } // valueToString converts a reflect.Value to a string. // bools are cast to empty. // nolint: cyclop func valueToString(v reflect.Value) (string, error) { switch v.Type().Name() { case reflect.TypeOf(Frontier).Name(): //nolint: forcetypeassert return v.Interface().(Hardfork).String(), nil case reflect.TypeOf(Fees).Name(): //nolint: forcetypeassert return v.Interface().(Order).String(), nil } //nolint: exhaustive switch v.Kind() { // custom types first case reflect.TypeOf(accounts.DerivationPath{}).Kind(): //nolint: forcetypeassert derivationPath := v.Interface().(accounts.DerivationPath) if len(derivationPath) == 0 { return "", errors.New("derivation path is empty") } // Remove the last item from the slice (since this is used as a base derivation path rather than a fully qualified account name) derivationPath = derivationPath[:len(derivationPath)-1] return fmt.Sprintf("\"%s\"", derivationPath.String()), nil case reflect.Bool: return "", nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(v.Int(), 10), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return strconv.FormatUint(v.Uint(), 10), nil case reflect.Float32, reflect.Float64: return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil case reflect.String: return fmt.Sprintf("\"%s\"", v.String()), nil default: return "", fmt.Errorf("could not convert %v to string", v.Kind()) } } func getFunctionName(temp interface{}) string { strs := strings.Split(runtime.FuncForPC(reflect.ValueOf(temp).Pointer()).Name(), ".") return strs[len(strs)-1] }