ethergo/backends/simulated/simulated.go
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{}