package evm import ( "context" "crypto/ecdsa" "math/big" "strings" "sync" "time" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/shopspring/decimal" ) type Client interface { bind.ContractBackend PublicAddress() common.Address PublicAddressAsHexString() string PendingNonce(context.Context) (uint64, error) NewTransactor() (*bind.TransactOpts, error) Execute(context.Context, func(context.Context, *TransactOpts) (*types.Transaction, error)) (Transaction, error) ExecuteAndWait(context.Context, func(context.Context) (Transaction, error)) error NativeTokenBalance(context.Context) (decimal.Decimal, error) CurrentBlockNumber(context.Context) (uint64, error) CurrentBlock(context.Context) (*types.Block, error) SetPendingTransactionCheckPeriod(duration time.Duration) TransactionByHash(context.Context, common.Hash) (*types.Transaction, bool, error) TokenService() TokenService } type ClientOption func(c Client) (Client, error) func WithPendingTransactionWaitResolution(duration time.Duration) ClientOption { return func(c Client) (Client, error) { c.SetPendingTransactionCheckPeriod(duration) return c, nil } } type client struct { *ethclient.Client ts TokenService pendingTransactionCheckPeriod time.Duration fromAddress common.Address transactionlock *sync.Mutex privateKey *ecdsa.PrivateKey chainId *big.Int defaultTimeout time.Duration } func (c *client) TokenService() TokenService { return c.ts } func NewClient(ctx context.Context, rpcurl string, privatekey string, tm TokenMapper, options ...ClientOption) (Client, error) { c, err := ethclient.Dial(rpcurl) if err != nil { return nil, errors.Wrapf(err, "dialing rpc url %s", rpcurl) } ecdsaprivatekey, publicaddress, err := GetPrivateKeyAndPublicAddressFromString(privatekey) if err != nil { return nil, errors.Wrap(err, "reading private key") } res := &client{ Client: c, pendingTransactionCheckPeriod: 2 * time.Second, privateKey: ecdsaprivatekey, fromAddress: publicaddress, transactionlock: &sync.Mutex{}, defaultTimeout: 15 * time.Minute, } res.ts = NewTokenService(res, tm) res.chainId, err = c.ChainID(ctx) if err != nil { return nil, errors.Wrap(err, "retrieving chain id") } ires := Client(res) for _, option := range options { ires, err = option(ires) if err != nil { return nil, errors.Wrap(err, "applying option") } } return ires, nil } func (c *client) PublicAddress() common.Address { return c.fromAddress } func (c *client) SetPendingTransactionCheckPeriod(period time.Duration) { c.pendingTransactionCheckPeriod = period } func (c *client) PendingNonce(ctx context.Context) (uint64, error) { return c.PendingNonceAt(ctx, c.PublicAddress()) } func (c *client) PublicAddressAsHexString() string { return c.PublicAddress().Hex() } func (c *client) NewTransactor() (*bind.TransactOpts, error) { return bind.NewKeyedTransactorWithChainID(c.privateKey, c.chainId) } func (c *client) Execute(ctx context.Context, action func(context.Context, *TransactOpts) (*types.Transaction, error)) (Transaction, error) { c.transactionlock.Lock() defer c.transactionlock.Unlock() localctx, cancel := context.WithTimeout(ctx, c.defaultTimeout) defer cancel() auth, err := NewTransactOpts(localctx, c) if err != nil { return nil, errors.Wrap(err, "init transaction options") } tx, err := action(localctx, auth) if err != nil { return nil, errors.Wrap(err, "executing waitable action") } log.Debug().Msgf("//TX// tx started / hash: %s / gasprice: %s / gaslimit: %d", tx.Hash().Hex(), decimal.NewFromBigInt(tx.GasPrice(), -9), tx.Gas()) return NewTransaction(c, c.pendingTransactionCheckPeriod, tx), nil } func (c *client) ExecuteAndWait(ctx context.Context, action func(context.Context) (Transaction, error)) error { tx, err := action(ctx) if err != nil { return errors.Wrap(err, "executing waitable action") } return tx.Wait(ctx) } func (c *client) GetTransactionsForAddressInBlock(ctx context.Context, a string, b int64) { block, err := c.BlockByNumber(ctx, big.NewInt(b)) if err != nil { panic(err) } txcount, err := c.TransactionCount(ctx, block.Hash()) if err != nil { panic(err) } for i := 0; i < int(txcount); i++ { tx, err := c.TransactionInBlock(ctx, block.Hash(), uint(i)) if err != nil { panic(err) } if strings.ToLower(tx.To().Hex()) == strings.ToLower(a) { log.Debug().Msgf("%d // %s // %s -> %s", b, decimal.NewFromBigInt(tx.Value(), -18), "", tx.To()) } } } func (c *client) SuggestGasPrice(ctx context.Context) (*big.Int, error) { return c.Client.SuggestGasPrice(ctx) } func (c *client) CurrentBlockNumber(ctx context.Context) (uint64, error) { block, err := c.BlockNumber(ctx) if err != nil { return 0, err } return block, nil } func (c *client) CurrentBlock(ctx context.Context) (*types.Block, error) { number, err := c.CurrentBlockNumber(ctx) if err != nil { return nil, errors.Wrap(err, "getting current block number") } block, err := c.BlockByNumber(ctx, big.NewInt(int64(number))) if err != nil { return nil, errors.Wrapf(err, "getting block %d", number) } return block, nil } func (c *client) NativeTokenBalance(ctx context.Context) (decimal.Decimal, error) { return c.BalanceFromAddress(ctx, c.PublicAddress()) } func (c *client) BalanceFromAddress(ctx context.Context, address common.Address) (decimal.Decimal, error) { b, err := c.PendingBalanceAt(ctx, address) if err != nil { return decimal.Zero, errors.Wrap(err, "getting balance") } return decimal.NewFromBigInt(b, -18), nil } func GetPrivateKeyAndPublicAddressFromString(privatekey string) (*ecdsa.PrivateKey, common.Address, error) { privateKeyECDSA, err := crypto.HexToECDSA(privatekey) if err != nil { return nil, common.Address{}, errors.Wrap(err, "converting hex private key to ecdsa") } publicKeyECDSA, ok := privateKeyECDSA.Public().(*ecdsa.PublicKey) if !ok { return nil, common.Address{}, errors.New("cannot assert type: publicKey is not of type *ecdsa.PublicKey") } return privateKeyECDSA, crypto.PubkeyToAddress(*publicKeyECDSA), nil }