services/scribe/config/generate.go
package config
import (
"context"
"fmt"
"github.com/synapsecns/sanguine/ethergo/parser/hardhat"
"os"
"strconv"
"time"
"bitbucket.org/tentontrain/math"
awsTime "github.com/aws/smithy-go/time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"golang.org/x/exp/slices"
)
// ClientGenerator generates an ethclient from a context and a url, this is used so we can override
// ethclient.DialContext for testing.
type ClientGenerator func(ctx context.Context, rawURL string) (ReceiptClient, error)
// ReceiptClient is an client that implements receipt fetching.
type ReceiptClient interface {
// TransactionReceipt returns the receipt of a mined transaction. Note that the
// transaction may not be included in the current canonical chain even if a receipt
// exists.
// TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
}
// DefaultClientGenerator generates the default ethclient.
func DefaultClientGenerator(ctx context.Context, rawURL string) (ReceiptClient, error) {
//nolint: wrapcheck
return ethclient.DialContext(ctx, rawURL)
}
type configList map[int]map[string]ContractConfig
// ContractsForChain gets all contraacts for a given chain.
func (c configList) ContractsForChain(chainID int) (configs []ContractConfig) {
chainConfigs, ok := c[chainID]
if !ok {
return configs
}
for _, contractConfigs := range chainConfigs {
configs = append(configs, contractConfigs)
}
return configs
}
// GenerateConfig generates a config using a hardhat deployment and scribe.
// this requires scribe to be live.
//
//nolint:cyclop
func GenerateConfig(ctx context.Context, omniRPCUrl, deployPath string, outputPath string, skippedChainIDS []int, cg ClientGenerator) error {
contracts, err := hardhat.GetDeployments(deployPath)
if err != nil {
return fmt.Errorf("could not get deployments: %w", err)
}
configList := make(configList)
for _, contract := range contracts {
for chainIDStr, network := range contract.Networks {
chainID, err := strconv.Atoi(chainIDStr)
if err != nil {
return fmt.Errorf("could not parse chainid from string %s: %w", chainIDStr, err)
}
// skip chainids
if slices.Contains(skippedChainIDS, chainID) {
continue
}
deployBlock, err := getDeployBlock(ctx, cg, omniRPCUrl, common.HexToHash(network.TransactionHash), uint32(chainID))
if err != nil {
return fmt.Errorf("could not get deploy block for contract %s on network %d", contract.Name, chainID)
}
// initialize the chain map
_, hasChain := configList[chainID]
if !hasChain {
configList[chainID] = make(map[string]ContractConfig)
}
chainContract, hasContract := configList[chainID][network.Address]
// if the contract already exist, just use lesser of the two start blocks
if hasContract {
chainContract.StartBlock = math.Min[uint64](deployBlock, chainContract.StartBlock)
configList[chainID][network.Address] = chainContract
continue
}
configList[chainID][network.Address] = ContractConfig{
Address: network.Address,
StartBlock: deployBlock,
}
}
}
config := Config{}
for chainID := range configList {
config.Chains = append(config.Chains, ChainConfig{
ChainID: uint32(chainID),
Contracts: configList.ContractsForChain(chainID),
})
}
encodedConfig, err := config.Encode()
if err != nil {
return fmt.Errorf("could not create encoded config: %w", err)
}
err = os.WriteFile(outputPath, encodedConfig, 0600)
if err != nil {
return fmt.Errorf("could not write file: %w", err)
}
return nil
}
func getDeployBlock(ctx context.Context, cg ClientGenerator, omniRPCUrl string, txhash common.Hash, chainID uint32) (block uint64, err error) {
rpcClient, err := cg(ctx, fmt.Sprintf("%s/rpc/%d", omniRPCUrl, chainID))
if err != nil {
return block, fmt.Errorf("could not get client: %w", err)
}
var txReceipt *types.Receipt
OUTER:
for attempt := 0; attempt < 20; attempt++ {
txReceipt, err = rpcClient.TransactionReceipt(ctx, txhash)
if err != nil {
if attempt < 20 {
_ = awsTime.SleepWithContext(ctx, time.Second*2)
continue
}
return block, fmt.Errorf("could not get tx receipt: %w", err)
}
break OUTER
}
return txReceipt.BlockNumber.Uint64(), nil
}