synapsecns/sanguine

View on GitHub
ethergo/backends/anvil/options.go

Summary

Maintainability
B
6 hrs
Test Coverage
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]
}