2023-03-02 01:05:53 +00:00
|
|
|
package util
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/ecdsa"
|
2023-04-03 16:58:45 +00:00
|
|
|
"errors"
|
2023-03-02 01:05:53 +00:00
|
|
|
"fmt"
|
|
|
|
"math/big"
|
2023-04-03 16:58:45 +00:00
|
|
|
"time"
|
2023-03-02 01:05:53 +00:00
|
|
|
|
2023-04-03 16:58:45 +00:00
|
|
|
"github.com/ethereum/go-ethereum"
|
2023-03-02 01:05:53 +00:00
|
|
|
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
|
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
|
|
"github.com/ethereum/go-ethereum/ethclient"
|
|
|
|
)
|
|
|
|
|
2023-04-03 16:58:45 +00:00
|
|
|
var (
|
|
|
|
ErrEvmBroadcastTimeout = errors.New("timed out waiting for tx to be committed to block")
|
|
|
|
// ErrEvmTxFailed is returned when a tx is committed to a block, but the receipt status is 0.
|
|
|
|
// this means the tx failed. we don't have debug_traceTransaction RPC command so the best way
|
|
|
|
// to determine the problem is to attempt to make the tx manually.
|
|
|
|
ErrEvmTxFailed = errors.New("transaction was committed but failed. likely an execution revert by contract code")
|
|
|
|
)
|
|
|
|
|
2023-03-02 01:05:53 +00:00
|
|
|
type EvmTxRequest struct {
|
|
|
|
Tx *ethtypes.Transaction
|
|
|
|
Data interface{}
|
|
|
|
}
|
|
|
|
type EvmTxResponse struct {
|
|
|
|
Request EvmTxRequest
|
|
|
|
TxHash common.Hash
|
|
|
|
Err error
|
|
|
|
}
|
|
|
|
|
2023-03-31 17:30:37 +00:00
|
|
|
type ErrEvmFailedToSign struct{ Err error }
|
2023-03-02 01:05:53 +00:00
|
|
|
|
2023-03-31 17:30:37 +00:00
|
|
|
func (e ErrEvmFailedToSign) Error() string {
|
2023-03-02 01:05:53 +00:00
|
|
|
return fmt.Sprintf("failed to sign tx: %s", e.Err)
|
|
|
|
}
|
|
|
|
|
2023-03-31 17:30:37 +00:00
|
|
|
type ErrEvmFailedToBroadcast struct{ Err error }
|
2023-03-02 01:05:53 +00:00
|
|
|
|
2023-03-31 17:30:37 +00:00
|
|
|
func (e ErrEvmFailedToBroadcast) Error() string {
|
2023-03-02 01:05:53 +00:00
|
|
|
return fmt.Sprintf("failed to broadcast tx: %s", e.Err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// EvmSigner manages signing and broadcasting requests to transfer Erc20 tokens
|
|
|
|
// Will work for calling all contracts that have func signature `transfer(address,uint256)`
|
|
|
|
type EvmSigner struct {
|
|
|
|
signerAddress common.Address
|
2023-04-03 16:58:45 +00:00
|
|
|
Auth *bind.TransactOpts
|
2023-03-02 01:05:53 +00:00
|
|
|
EvmClient *ethclient.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewEvmSigner(
|
|
|
|
evmClient *ethclient.Client,
|
|
|
|
privKey *ecdsa.PrivateKey,
|
|
|
|
chainId *big.Int,
|
|
|
|
) (*EvmSigner, error) {
|
|
|
|
auth, err := bind.NewKeyedTransactorWithChainID(privKey, chainId)
|
|
|
|
if err != nil {
|
|
|
|
return &EvmSigner{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
publicKey := privKey.Public()
|
|
|
|
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
|
|
|
|
if !ok {
|
|
|
|
return &EvmSigner{}, fmt.Errorf("cannot assert type: publicKey is not of type *ecdsa.PublicKey")
|
|
|
|
}
|
|
|
|
|
|
|
|
return &EvmSigner{
|
2023-04-03 16:58:45 +00:00
|
|
|
Auth: auth,
|
2023-03-02 01:05:53 +00:00
|
|
|
signerAddress: crypto.PubkeyToAddress(*publicKeyECDSA),
|
|
|
|
EvmClient: evmClient,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *EvmSigner) Run(requests <-chan EvmTxRequest) <-chan EvmTxResponse {
|
|
|
|
responses := make(chan EvmTxResponse)
|
|
|
|
|
|
|
|
// receive tx requests, sign & broadcast them.
|
|
|
|
// Responses are sent once the tx is added to the pending tx pool.
|
|
|
|
// To see result, use TransactionReceipt after tx has been included in a block.
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
// wait for incoming request
|
|
|
|
req := <-requests
|
|
|
|
|
2023-04-03 16:58:45 +00:00
|
|
|
signedTx, err := s.Auth.Signer(s.signerAddress, req.Tx)
|
2023-03-02 01:05:53 +00:00
|
|
|
if err != nil {
|
2023-03-31 17:30:37 +00:00
|
|
|
err = ErrEvmFailedToSign{Err: err}
|
2023-03-02 01:05:53 +00:00
|
|
|
} else {
|
|
|
|
err = s.EvmClient.SendTransaction(context.Background(), signedTx)
|
|
|
|
if err != nil {
|
2023-03-31 17:30:37 +00:00
|
|
|
err = ErrEvmFailedToBroadcast{Err: err}
|
2023-03-02 01:05:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
responses <- EvmTxResponse{
|
|
|
|
Request: req,
|
|
|
|
TxHash: signedTx.Hash(),
|
|
|
|
Err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return responses
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *EvmSigner) Address() common.Address {
|
|
|
|
return s.signerAddress
|
|
|
|
}
|
2023-04-03 16:58:45 +00:00
|
|
|
|
|
|
|
// WaitForEvmTxReceipt polls for a tx receipt and errors on timeout.
|
|
|
|
// If the receipt comes back, but with status 0 (failed), an error is returned.
|
|
|
|
func WaitForEvmTxReceipt(client *ethclient.Client, txHash common.Hash, timeout time.Duration) (*ethtypes.Receipt, error) {
|
|
|
|
var receipt *ethtypes.Receipt
|
|
|
|
var err error
|
|
|
|
outOfTime := time.After(timeout)
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-outOfTime:
|
|
|
|
err = ErrEvmBroadcastTimeout
|
|
|
|
default:
|
|
|
|
receipt, err = client.TransactionReceipt(context.Background(), txHash)
|
|
|
|
if errors.Is(err, ethereum.NotFound) {
|
|
|
|
// tx still not committed to a block. retry!
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// a response status of 0 means the tx was successfully committed but failed to execute
|
|
|
|
if receipt.Status == 0 {
|
|
|
|
err = ErrEvmTxFailed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
return receipt, err
|
|
|
|
}
|