Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ZkSync methods #714

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions blockchain/known_networks.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
FantomClientImplementation ClientImplementation = "Fantom"
WeMixClientImplementation ClientImplementation = "WeMix"
KromaClientImplementation ClientImplementation = "Kroma"
ZKSyncClientImplementation ClientImplementation = "ZKSync"
)

// wrapSingleClient Wraps a single EVM client in its appropriate implementation, based on the chain ID
Expand Down Expand Up @@ -63,6 +64,8 @@ func wrapSingleClient(networkSettings EVMNetwork, client *EthereumClient) EVMCli
wrappedEc = &WeMixClient{client}
case KromaClientImplementation:
wrappedEc = &KromaClient{client}
case ZKSyncClientImplementation:
wrappedEc = &ZKSyncClient{client}
default:
wrappedEc = client
}
Expand Down
219 changes: 219 additions & 0 deletions blockchain/zksync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package blockchain

import (
"context"
"crypto/ecdsa"
"fmt"
"github.com/smartcontractkit/chainlink-testing-framework/utils/conversions"
"math/big"
"strconv"
"time"

"github.com/ethereum/go-ethereum"
"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/rs/zerolog/log"
"github.com/zksync-sdk/zksync2-go/accounts"
"github.com/zksync-sdk/zksync2-go/clients"
zkutils "github.com/zksync-sdk/zksync2-go/utils"
)

// ZKSyncClient represents a single node, EVM compatible client for the ZKSync network
type ZKSyncClient struct {
*EthereumClient
}

// ZKSyncMultiNodeClient represents a multi-node, EVM compatible client for the ZKSync network
type ZKSyncMultiNodeClient struct {
*EthereumMultinodeClient
}

// TransactionOpts returns the base Tx options for 'transactions' that interact with a smart contract. Since most
// contract interactions in this framework are designed to happen through abigen calls, it's intentionally quite bare.
func (z *ZKSyncClient) TransactionOpts(from *EthereumWallet) (*bind.TransactOpts, error) {
privateKey, err := crypto.HexToECDSA(from.PrivateKey())
if err != nil {
return nil, fmt.Errorf("invalid private key: %v", err)
}
opts, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(z.NetworkConfig.ChainID))
if err != nil {
return nil, err
}
price, err := z.Client.SuggestGasPrice(opts.Context)
opts.GasPrice = price
if err != nil {
return nil, err
}
opts.From = common.HexToAddress(from.Address())
opts.Context = context.Background()

nonce, err := z.GetNonce(context.Background(), common.HexToAddress(from.Address()))
if err != nil {
return nil, err
}
opts.Nonce = big.NewInt(int64(nonce))

if z.NetworkConfig.MinimumConfirmations <= 0 { // Wait for your turn to send on an L2 chain
<-z.NonceSettings.registerInstantTransaction(from.Address(), nonce)
}

return opts, nil
}

// ProcessTransaction will queue or wait on a transaction depending on whether parallel transactions are enabled
func (z *ZKSyncClient) ProcessTransaction(tx *types.Transaction) error {
var txConfirmer HeaderEventSubscription
if z.GetNetworkConfig().MinimumConfirmations <= 0 {
fromAddr, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx)
if err != nil {
return err
}
z.NonceSettings.sentInstantTransaction(fromAddr.Hex()) // On an L2 chain, indicate the tx has been sent
txConfirmer = NewInstantConfirmer(z, tx.Hash(), nil, nil, z.l)
} else {
txConfirmer = NewTransactionConfirmer(z, tx, z.GetNetworkConfig().MinimumConfirmations, z.l)
}

z.AddHeaderEventSubscription(tx.Hash().String(), txConfirmer)

if !z.queueTransactions { // For sequential transactions
log.Debug().Str("Hash", tx.Hash().String()).Msg("Waiting for TX to confirm before moving on")
defer z.DeleteHeaderEventSubscription(tx.Hash().String())
return txConfirmer.Wait()
}
return nil
}

// IsTxConfirmed checks if the transaction is confirmed on chain or not
// Temp changes until the TransactionByHash method is fixed
func (z *ZKSyncClient) IsTxConfirmed(txHash common.Hash) (bool, error) {
isPending := false
receipt, err := z.Client.TransactionReceipt(context.Background(), txHash)
if err != nil {
return !isPending, err
}
z.gasStats.AddClientTXData(TXGasData{
TXHash: txHash.String(),
GasUsed: receipt.GasUsed,
CumulativeGasUsed: receipt.CumulativeGasUsed,
})
if receipt.Status == 0 { // 0 indicates failure, 1 indicates success
if err != nil {
log.Warn().Str("TX Hash", txHash.Hex()).
Msg("Transaction failed and was reverted! Unable to retrieve reason!")
return false, err
}
log.Warn().Str("TX Hash", txHash.Hex()).
Msg("Transaction failed and was reverted!")
}
return !isPending, err
}

func (z *ZKSyncClient) Fund(
toAddress string,
amount *big.Float,
gasEstimations GasEstimations,
) error {
// Connect to zkSync network
client, err := clients.Dial(z.GetNetworkConfig().HTTPURLs[0])
if err != nil {
return err
}
defer client.Close()

// Connect to Ethereum network
ethClient, err := ethclient.Dial(z.GetNetworkConfig().HTTPURLs[1])
if err != nil {
return err
}
defer ethClient.Close()

// Create wallet
w, err := accounts.NewWallet(common.Hex2Bytes(z.DefaultWallet.PrivateKey()), &client, ethClient)
if err != nil {
return err
}

opts := &accounts.TransactOpts{}

transferAmt, _ := amount.Int64()
log.Info().
Str("From", z.DefaultWallet.Address()).
Str("To", toAddress).
Str("Amount", strconv.FormatInt(transferAmt, 10)).
Msg("Transferring ETH")

// Create a transfer transaction
tx, err := w.Transfer(opts, accounts.TransferTransaction{
To: common.HexToAddress(toAddress),
Amount: big.NewInt(transferAmt),
Token: zkutils.EthAddress,
})
if err != nil {
return err
}
log.Info().Str("ZKSync", fmt.Sprintf("TXHash %s", tx.Hash())).Msg("Executing ZKSync transaction")
return z.ProcessTransaction(tx)
}

// ReturnFunds overrides the EthereumClient.ReturnFunds method.
// This is needed to call the ZKSyncClient.ProcessTransaction method instead of the EthereumClient.ProcessTransaction method.
func (z *ZKSyncClient) ReturnFunds(fromKey *ecdsa.PrivateKey) error {
var tx *types.Transaction
var err error
for attempt := 0; attempt < 20; attempt++ {
tx, err = attemptZKSyncReturn(z, fromKey, attempt)
if err == nil {
return z.ProcessTransaction(tx)
}
z.l.Debug().Err(err).Int("Attempt", attempt+1).Msg("Error returning funds from Chainlink node, trying again")
time.Sleep(time.Millisecond * 500)
}
return err
}

// This is just a 1:1 copy of attemptReturn, which can't be reused as-is for ZKSync as it doesn't
// accept an interface.
func attemptZKSyncReturn(z *ZKSyncClient, fromKey *ecdsa.PrivateKey, _ int) (*types.Transaction, error) {
to := common.HexToAddress(z.DefaultWallet.Address())
fromAddress, err := conversions.PrivateKeyToAddress(fromKey)
if err != nil {
return nil, err
}
nonce, err := z.GetNonce(context.Background(), fromAddress)
if err != nil {
return nil, err
}
balance, err := z.BalanceAt(context.Background(), fromAddress)
if err != nil {
return nil, err
}
gasEstimations, err := z.EstimateGas(ethereum.CallMsg{
From: fromAddress,
To: &to,
})
if err != nil {
return nil, err
}
totalGasCost := gasEstimations.TotalGasCost
balanceGasDelta := big.NewInt(0).Sub(balance, totalGasCost)

if balanceGasDelta.Sign() < 0 { // Try with 0.5 gwei if we have no or negative margin. Might as well
balanceGasDelta = big.NewInt(500_000_000)
}

tx, err := z.NewTx(fromKey, nonce, to, balanceGasDelta, gasEstimations)
if err != nil {
return nil, err
}
z.l.Info().
Str("Amount", balance.String()).
Str("From", fromAddress.Hex()).
Str("To", to.Hex()).
Str("Total Gas Cost", totalGasCost.String()).
Msg("Returning Funds to Default Wallet")
return tx, z.SendTransaction(context.Background(), tx)
}
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,29 @@ require (
github.com/smartcontractkit/wasp v0.3.6
github.com/stretchr/testify v1.8.4
github.com/testcontainers/testcontainers-go v0.23.0
github.com/zksync-sdk/zksync2-go v0.3.1
go.uber.org/atomic v1.11.0
go.uber.org/zap v1.26.0
golang.org/x/sync v0.4.0
)

require (
github.com/allegro/bigcache v1.2.1 // indirect
github.com/btcsuite/btcd v0.23.4 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect
github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 // indirect
github.com/cespare/cp v1.1.1 // indirect
github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect
github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/miguelmota/go-ethereum-hdwallet v0.1.1 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pyroscope-io/client v0.7.1 // indirect
github.com/rs/cors v1.8.3 // indirect
github.com/sercand/kuberesolver/v4 v4.0.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
)

Expand Down