2021-11-08 18:20:31 +00:00
|
|
|
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)
|
2021-11-09 04:11:05 +00:00
|
|
|
Execute(context.Context, func(context.Context, *TransactOpts) (*types.Transaction, error), ...ExecutionOption) (Transaction, error)
|
2021-11-08 18:35:39 +00:00
|
|
|
ExecuteAndWait(context.Context, func(context.Context) (Transaction, error)) error
|
2021-11-08 18:20:31 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-11-09 04:11:05 +00:00
|
|
|
type ClientOption func(c *client) (*client, error)
|
2021-11-08 18:20:31 +00:00
|
|
|
|
|
|
|
func WithPendingTransactionWaitResolution(duration time.Duration) ClientOption {
|
2021-11-09 04:11:05 +00:00
|
|
|
return func(c *client) (*client, error) {
|
|
|
|
c.pendingTransactionCheckPeriod = duration
|
2021-11-08 18:20:31 +00:00
|
|
|
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 {
|
2021-11-09 04:11:05 +00:00
|
|
|
res, err = option(res)
|
2021-11-08 18:20:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "applying option")
|
|
|
|
}
|
|
|
|
}
|
2021-11-09 04:11:05 +00:00
|
|
|
return res, nil
|
2021-11-08 18:20:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2021-11-09 04:11:05 +00:00
|
|
|
func (c *client) Execute(ctx context.Context, action func(context.Context, *TransactOpts) (*types.Transaction, error), opts ...ExecutionOption) (Transaction, error) {
|
2021-11-08 18:20:31 +00:00
|
|
|
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")
|
|
|
|
}
|
2021-11-09 07:51:22 +00:00
|
|
|
for _, opt := range opts {
|
|
|
|
auth = opt(auth)
|
|
|
|
}
|
2021-11-08 18:20:31 +00:00
|
|
|
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())
|
2021-11-08 18:35:39 +00:00
|
|
|
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)
|
2021-11-08 18:20:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|