synapsecns/sanguine

View on GitHub
ethergo/backends/simulated/simulated.go

Summary

Maintainability
A
1 hr
Test Coverage
package simulated

import (
    "context"
    "errors"
    "math/big"
    "testing"
    "time"

    "github.com/ethereum/go-ethereum/accounts/abi/bind"
    "github.com/ethereum/go-ethereum/accounts/keystore"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/ethereum/go-ethereum/eth/gasprice"
    "github.com/ethereum/go-ethereum/params"
    "github.com/ipfs/go-log"
    "github.com/lmittmann/w3/w3types"
    "github.com/stretchr/testify/assert"
    commonBackend "github.com/synapsecns/sanguine/ethergo/backends"
    "github.com/synapsecns/sanguine/ethergo/backends/base"
    "github.com/synapsecns/sanguine/ethergo/backends/simulated/multibackend"
    "github.com/synapsecns/sanguine/ethergo/chain"
    "github.com/synapsecns/sanguine/ethergo/chain/client"
    "github.com/synapsecns/sanguine/ethergo/signer/nonce"
    "github.com/teivah/onecontext"
)

var logger = log.Logger("simulated-logger")

// Backend is the simulated backend.
type Backend struct {
    t *testing.T
    // base backend is the base backend
    *base.Backend
    // backend handles commits
    simulatedBackend *multibackend.SimulatedBackend
    // faucetAddr is the address funde at genesis
    faucetAddr *keystore.Key
    // gasLimit is the block gas limit
    gasLimit uint64
    // chainConfig is the chainConfig for this chain
    chainConfig *params.ChainConfig
}

func (s *Backend) BatchWithContext(_ context.Context, _ ...w3types.Caller) error {
    return errors.New("rpc calls not supported on simulated backend")
}

// Signer gets the signer for the backend.
func (s *Backend) Signer() types.Signer {
    latestBlock, err := s.BlockByNumber(s.Context(), nil)
    assert.Nil(s.T(), err)

    return types.MakeSigner(s.chainConfig, latestBlock.Number(), latestBlock.Time())
}

// T gets the testing object for the backend.
func (s *Backend) T() *testing.T {
    s.t.Helper()
    return s.t
}

// SetT sets the testing object on the backend.
func (s *Backend) SetT(t *testing.T) {
    t.Helper()
    s.t = t
}

// BackendName is the name of the simulated backend.
const BackendName = "SimulatedBackend"

// BackendName gets the name of SimulatedBackend.
func (s *Backend) BackendName() string {
    return BackendName
}

// Commit commits pending txes to the backend. Does not thing if no txes are pending.
func (s *Backend) Commit() {
    s.simulatedBackend.Commit()
}

// EmptyBlock mines an empty block.
func (s *Backend) EmptyBlock(blockTime time.Time) {
    s.simulatedBackend.EmptyBlock(blockTime)
}

// AdjustTime adjusts the time of the most recent block.
func (s *Backend) AdjustTime(adjustment time.Duration) error {
    //nolint: wrapcheck
    return s.simulatedBackend.AdjustTime(adjustment)
}

// getFaucetTxContext gets a signed transaction from the faucet address.
func (s *Backend) getFaucetTxContext(ctx context.Context) *bind.TransactOpts {
    auth, err := bind.NewKeyedTransactorWithChainID(s.faucetAddr.PrivateKey, s.Chain.GetBigChainID())
    assert.Nil(s.T(), err)

    //nolint: ineffassign
    gasPrice, err := s.SuggestGasPrice(ctx)
    assert.Nil(s.T(), err)

    // standard eth value tx price
    auth.GasLimit = 21000
    auth.GasPrice = gasPrice

    return auth
}

// faucetSignTx will sign a tx with the faucet addr.
func (s *Backend) faucetSignTx(tx *types.Transaction) *types.Transaction {
    tx, err := s.SignTx(tx, s.Signer(), s.faucetAddr.PrivateKey)
    assert.Nil(s.T(), err)
    return tx
}

// ChainConfig gets the chain config for the simulated backend.
func (s *Backend) ChainConfig() *params.ChainConfig {
    return s.chainConfig
}

// GetFundedAccount returns an account with the requested balance. (Note: if genesis acount has an insufficient
// balance, blocks may be mined here).
func (s *Backend) GetFundedAccount(ctx context.Context, requestBalance *big.Int) *keystore.Key {
    key := s.MockAccount()

    s.Store(key)

    s.FundAccount(ctx, key.Address, *requestBalance)

    return key
}

// FundAccount fundsa  new account.
func (s *Backend) FundAccount(ctx context.Context, address common.Address, amount big.Int) {
    rawTx := s.getFaucetTxContext(ctx)

    tx := s.faucetSignTx(types.NewTx(&types.LegacyTx{
        To:       &address,
        Value:    &amount,
        Gas:      rawTx.GasLimit,
        GasPrice: rawTx.GasPrice,
    }))

    assert.Nil(s.T(), s.SendTransaction(ctx, tx))
    s.Commit()

    // wait for tx confirmation
    s.WaitForConfirmation(ctx, tx)
}

// SendTransaction sends a transaction and commits it mining a new block
// in cases where you would not like to commit automatically, you can run
// s.Client().SendTransaction().
func (s *Backend) SendTransaction(ctx context.Context, tx *types.Transaction) error {
    err := s.Client().SendTransaction(ctx, tx)
    s.Commit()
    //nolint: wrapcheck
    return err
}

// GetTxContext gets a signed transaction from full backend.
func (s *Backend) GetTxContext(ctx context.Context, address *common.Address) (res commonBackend.AuthType) {
    ctx, cancel := onecontext.Merge(ctx, s.Context())
    defer cancel()

    var acct *keystore.Key
    // TODO handle storing accounts to conform to get tx context
    if address != nil {
        acct = s.GetAccount(*address)
        if acct == nil {
            s.T().Errorf("could not get account %s", address.String())
            return res
        }
    } else {
        acct = s.GetFundedAccount(ctx, big.NewInt(0).Mul(big.NewInt(params.Ether), big.NewInt(10)))
        s.Store(acct)
    }

    auth, err := s.NewKeyedTransactorFromKey(acct.PrivateKey)
    assert.Nil(s.T(), err)

    gasBlock, err := s.BlockByNumber(ctx, nil)
    assert.Nil(s.T(), err)

    //nolint: ineffassign
    err = s.Chain.GasSetter().SetGasFee(ctx, auth, gasBlock.NumberU64(), gasprice.DefaultMaxPrice)
    assert.Nil(s.T(), err)

    return commonBackend.AuthType{
        TransactOpts: auth,
        PrivateKey:   acct.PrivateKey,
    }
}

// BlockGasLimit is the gas limit used for the block.
const BlockGasLimit = uint64(91712388)

// NewSimulatedBackend gets a simulated backend from geth for testing and creates an account
// with balance. ChainID is 1337.
func NewSimulatedBackend(ctx context.Context, t *testing.T) *Backend {
    t.Helper()
    return NewSimulatedBackendWithChainID(ctx, t, params.AllEthashProtocolChanges.ChainID)
}

// NewSimulatedBackendWithChainID gets a simulated backend from geth for testing and creates an account
// with balance.
func NewSimulatedBackendWithChainID(ctx context.Context, t *testing.T, chainID *big.Int) *Backend {
    t.Helper()
    return NewSimulatedBackendWithConfig(ctx, t, multibackend.NewConfigWithChainID(chainID))
}

// NewSimulatedBackendWithConfig gets a simulated backend from geth for testing and creates an account
// with balance.
func NewSimulatedBackendWithConfig(ctx context.Context, t *testing.T, config *params.ChainConfig) *Backend {
    t.Helper()
    // 100 million ether
    balance := big.NewInt(0).Mul(big.NewInt(params.Ether), big.NewInt(100000000))
    key := base.MockAccount(t)

    genesisAlloc := map[common.Address]core.GenesisAccount{
        key.Address: {
            Balance: balance,
        },
    }

    simulatedBackend := multibackend.NewSimulatedBackendWithConfig(genesisAlloc, BlockGasLimit, config)
    baseClient := Client{simulatedBackend}

    chn, err := chain.NewFromClient(ctx, &client.Config{
        RPCUrl:  []string{""},
        ChainID: int(config.ChainID.Uint64()),
    }, baseClient)
    chn.SetChainConfig(config)
    assert.Nil(t, err)

    baseBackend, err := base.NewBaseBackend(ctx, t, chn)
    assert.Nil(t, err)

    backend := Backend{
        Backend:          baseBackend,
        simulatedBackend: simulatedBackend,
        chainConfig:      config,
    }
    backend.SetT(t)
    backend.Manager = nonce.NewNonceManager(ctx, &backend, backend.GetBigChainID())
    backend.faucetAddr = key
    backend.gasLimit = BlockGasLimit

    return &backend
}

var _ commonBackend.SimulatedTestBackend = &Backend{}