evm/client.go

209 lines
6.2 KiB
Go

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), ...ExecutionOption) (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)
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.pendingTransactionCheckPeriod = 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")
}
for _, option := range options {
res, err = option(res)
if err != nil {
return nil, errors.Wrap(err, "applying option")
}
}
return res, nil
}
func (c *client) PublicAddress() common.Address {
return c.fromAddress
}
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), opts ...ExecutionOption) (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")
}
for _, opt := range opts {
auth = opt(auth)
}
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
}