Files

528 lines
15 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package relayercmd
import (
"fmt"
"math/big"
"os"
"strings"
"github.com/luxfi/cli/v2/pkg/cobrautils"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/contract"
"github.com/luxfi/cli/v2/pkg/interchain/relayer"
"github.com/luxfi/cli/v2/pkg/localnet"
"github.com/luxfi/cli/v2/pkg/models"
"github.com/luxfi/cli/v2/pkg/networkoptions"
"github.com/luxfi/cli/v2/pkg/prompts"
"github.com/luxfi/cli/v2/pkg/utils"
"github.com/luxfi/cli/v2/pkg/ux"
"github.com/luxfi/cli/v2/pkg/vm"
"github.com/luxfi/cli/v2/sdk/evm"
"github.com/luxfi/node/v2/v2/utils/logging"
"github.com/spf13/cobra"
)
type DeployFlags struct {
Network networkoptions.NetworkFlags
Version string
LogLevel string
RelayCChain bool
BlockchainsToRelay []string
Key string
Amount float64
CChainAmount float64
BlockchainFundingKey string
CChainFundingKey string
BinPath string
AllowPrivateIPs bool
}
var deployFlags DeployFlags
const (
disableDeployToRemotePrompt = true
aproxFundingFee = 0.01
)
// lux interchain relayer deploy
func newDeployCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploys an Warp Relayer for the given Network",
Long: `Deploys an Warp Relayer for the given Network.`,
RunE: deploy,
Args: cobrautils.ExactArgs(0),
}
networkoptions.AddNetworkFlagsToCmd(cmd, &deployFlags.Network, true, networkoptions.NonMainnetSupportedNetworkOptions)
cmd.Flags().StringVar(&deployFlags.BinPath, "bin-path", "", "use the given relayer binary")
cmd.Flags().StringVar(
&deployFlags.Version,
"version",
constants.DefaultRelayerVersion,
"version to deploy",
)
cmd.Flags().StringVar(&deployFlags.LogLevel, "log-level", "", "log level to use for relayer logs")
cmd.Flags().StringSliceVar(&deployFlags.BlockchainsToRelay, "blockchains", nil, "blockchains to relay as source and destination")
cmd.Flags().BoolVar(&deployFlags.RelayCChain, "cchain", false, "relay C-Chain as source and destination")
cmd.Flags().StringVar(&deployFlags.Key, "key", "", "key to be used by default both for rewards and to pay fees")
cmd.Flags().Float64Var(&deployFlags.Amount, "amount", 0, "automatically fund l1s fee payments with the given amount")
cmd.Flags().Float64Var(&deployFlags.CChainAmount, "cchain-amount", 0, "automatically fund cchain fee payments with the given amount")
cmd.Flags().StringVar(&deployFlags.BlockchainFundingKey, "blockchain-funding-key", "", "key to be used to fund relayer account on all l1s")
cmd.Flags().StringVar(&deployFlags.CChainFundingKey, "cchain-funding-key", "", "key to be used to fund relayer account on cchain")
cmd.Flags().BoolVar(&deployFlags.AllowPrivateIPs, "allow-private-ips", true, "allow relayer to connect to private ips")
return cmd
}
func deploy(_ *cobra.Command, args []string) error {
return CallDeploy(args, deployFlags, models.UndefinedNetwork)
}
func CallDeploy(_ []string, flags DeployFlags, network models.Network) error {
var err error
if network == models.UndefinedNetwork {
network, err = networkoptions.GetNetworkFromCmdLineFlags(
app,
"In which Network will operate the Relayer?",
flags.Network,
true,
false,
networkoptions.NonMainnetSupportedNetworkOptions,
"",
)
if err != nil {
return err
}
}
deployToRemote := false
if !disableDeployToRemotePrompt && network.Kind != models.Local {
prompt := "Do you want to deploy the relayer to a remote or a local host?"
remoteHostOption := "I want to deploy the relayer into a remote node in the cloud"
localHostOption := "I prefer to deploy into a localhost process"
options := []string{remoteHostOption, localHostOption, explainOption}
for {
option, err := app.Prompt.CaptureList(
prompt,
options,
)
if err != nil {
return err
}
switch option {
case remoteHostOption:
deployToRemote = true
case localHostOption:
case explainOption:
ux.Logger.PrintToUser("A local host relayer is for temporary networks, won't survive a host restart")
ux.Logger.PrintToUser("or a relayer transient failure (but anyway can be manually restarted by cmd)")
ux.Logger.PrintToUser("A remote relayer is deployed into a new cloud node, and will recover from")
ux.Logger.PrintToUser("temporary relayer failures and from host restarts.")
continue
}
break
}
}
if !deployToRemote {
if isUP, _, _, err := relayer.RelayerIsUp(app.GetLocalRelayerRunPath(network.Kind)); err != nil {
return err
} else if isUP {
return fmt.Errorf("there is already a local relayer deployed for %s", network.Kind.String())
}
}
logLevelOptions := []string{
logging.Info.LowerString(),
logging.Warn.LowerString(),
logging.Error.LowerString(),
logging.Off.LowerString(),
logging.Fatal.LowerString(),
logging.Debug.LowerString(),
logging.Trace.LowerString(),
logging.Verbo.LowerString(),
}
if flags.LogLevel == "" {
prompt := "Which log level do you prefer for your relayer?"
flags.LogLevel, err = app.Prompt.CaptureList(
prompt,
logLevelOptions,
)
if err != nil {
return err
}
} else {
if _, err := logging.ToLevel(flags.LogLevel); err != nil {
return fmt.Errorf("invalid log level %s: %w", flags.LogLevel, err)
}
}
networkUP := true
_, err = utils.GetChainID(network.Endpoint, "C")
if err != nil {
if !strings.Contains(err.Error(), "connection refused") {
return err
}
networkUP = false
}
configureBlockchains := false
if networkUP {
if flags.BlockchainsToRelay != nil || flags.RelayCChain {
configureBlockchains = true
} else {
prompt := "Do you want to add blockchain information to your relayer?"
yesOption := "Yes, I want to configure source and destination blockchains"
noOption := "No, I prefer to configure the relayer later on"
options := []string{yesOption, noOption, explainOption}
for {
option, err := app.Prompt.CaptureList(
prompt,
options,
)
if err != nil {
return err
}
switch option {
case yesOption:
configureBlockchains = true
case noOption:
case explainOption:
ux.Logger.PrintToUser("You can configure a list of source and destination blockchains, so that the")
ux.Logger.PrintToUser("relayer will listen for new messages on each source, and deliver them to the")
ux.Logger.PrintToUser("destinations.")
ux.Logger.PrintToUser("Or you can not configure those later on, by using the 'relayer config' cmd.")
continue
}
break
}
}
}
var configSpec ConfigSpec
if configureBlockchains {
// TODO: this is the base for a 'relayer config' cmd
// that should load the current config, generate a configSpec for that,
// and use this to change the config, before saving it
// most probably, also, relayer config should restart the relayer
var cancel bool
configSpec, cancel, err = GenerateConfigSpec(
network,
flags.RelayCChain,
flags.BlockchainsToRelay,
flags.Key,
)
if cancel {
return nil
}
if err != nil {
return err
}
}
fundBlockchains := false
if networkUP && len(configSpec.destinations) > 0 {
if flags.Amount != 0 {
fundBlockchains = true
} else {
// TODO: this (and the next section) are the base for a 'relayer fund' cmd
// it must be based on relayer conf, and try to gather a nice blockchain desc
// from the blockchain id (as relayer logs cmd)
ux.Logger.PrintToUser("")
for _, destination := range configSpec.destinations {
addr, err := evm.PrivateKeyToAddress(destination.privateKey)
if err != nil {
return err
}
client, err := evm.GetClient(destination.rpcEndpoint)
if err != nil {
return err
}
balance, err := client.GetAddressBalance(addr.Hex())
if err != nil {
return err
}
balanceFlt := new(big.Float).SetInt(balance)
balanceFlt = balanceFlt.Quo(balanceFlt, new(big.Float).SetInt(vm.OneLux))
ux.Logger.PrintToUser("Relayer private key on destination %s has a balance of %.9f", destination.blockchainDesc, balanceFlt)
}
ux.Logger.PrintToUser("")
prompt := "Do you want to fund relayer destinations?"
yesOption := "Yes, I want to fund destination blockchains"
noOption := "No, I prefer to fund the relayer later on"
options := []string{yesOption, noOption, explainOption}
for {
option, err := app.Prompt.CaptureList(
prompt,
options,
)
if err != nil {
return err
}
switch option {
case yesOption:
fundBlockchains = true
case noOption:
case explainOption:
ux.Logger.PrintToUser("You need to set some balance on the destination addresses")
ux.Logger.PrintToUser("so the relayer can pay for fees when delivering messages.")
continue
}
break
}
}
}
if fundBlockchains {
for _, destination := range configSpec.destinations {
addr, err := evm.PrivateKeyToAddress(destination.privateKey)
if err != nil {
return err
}
client, err := evm.GetClient(destination.rpcEndpoint)
if err != nil {
return err
}
cchainBlockchainID, err := contract.GetBlockchainID(
app,
network,
contract.ChainSpec{
CChain: true,
},
)
if err != nil {
return err
}
isCChainDestination := cchainBlockchainID.String() == destination.blockchainID
doPay := false
switch {
case !isCChainDestination && flags.Amount != 0:
doPay = true
case isCChainDestination && flags.CChainAmount != 0:
doPay = true
default:
balance, err := client.GetAddressBalance(addr.Hex())
if err != nil {
return err
}
balanceFlt := new(big.Float).SetInt(balance)
balanceFlt = balanceFlt.Quo(balanceFlt, new(big.Float).SetInt(vm.OneLux))
prompt := fmt.Sprintf("Do you want to fund relayer for destination %s (current C-Chain LUX balance: %.9f)?", destination.blockchainDesc, balanceFlt)
yesOption := "Yes, I will send funds to it"
noOption := "Not now"
options := []string{yesOption, noOption}
option, err := app.Prompt.CaptureList(
prompt,
options,
)
if err != nil {
return err
}
switch option {
case yesOption:
doPay = true
case noOption:
}
}
if doPay {
genesisAddress, genesisPrivateKey, err := contract.GetEVMSubnetPrefundedKey(
app,
network,
contract.ChainSpec{
BlockchainID: destination.blockchainID,
},
)
if err != nil {
return err
}
privateKey := ""
if flags.Amount != 0 {
privateKey = genesisPrivateKey
}
if flags.BlockchainFundingKey != "" || flags.CChainFundingKey != "" {
if isCChainDestination {
if flags.CChainFundingKey != "" {
k, err := app.GetKey(flags.CChainFundingKey, network, false)
if err != nil {
return err
}
privateKey = k.PrivKeyHex()
}
} else {
if flags.BlockchainFundingKey != "" {
k, err := app.GetKey(flags.BlockchainFundingKey, network, false)
if err != nil {
return err
}
privateKey = k.PrivKeyHex()
}
}
}
if privateKey == "" {
privateKey, err = prompts.PromptPrivateKey(
app.Prompt,
fmt.Sprintf("fund the relayer destination %s", destination.blockchainDesc),
app.GetKeyDir(),
app.GetKey,
genesisAddress,
genesisPrivateKey,
)
if err != nil {
return err
}
}
balance, err := client.GetPrivateKeyBalance(privateKey)
if err != nil {
return err
}
if balance.Cmp(big.NewInt(0)) == 0 {
return fmt.Errorf("destination %s funding key has no balance", destination.blockchainDesc)
}
balanceBigFlt := new(big.Float).SetInt(balance)
balanceBigFlt = balanceBigFlt.Quo(balanceBigFlt, new(big.Float).SetInt(vm.OneLux))
balanceFlt, _ := balanceBigFlt.Float64()
balanceFlt -= aproxFundingFee
var amountFlt float64
switch {
case !isCChainDestination && flags.Amount != 0:
amountFlt = flags.Amount
case isCChainDestination && flags.CChainAmount != 0:
amountFlt = flags.CChainAmount
default:
amountFlt, err = app.Prompt.CaptureFloat(
fmt.Sprintf("Amount to transfer (available: %f)", balanceFlt),
func(f float64) error {
if f <= 0 {
return fmt.Errorf("%f is not positive", f)
}
if f > balanceFlt {
return fmt.Errorf("%f exceeds available funding balance of %f", f, balanceFlt)
}
return nil
},
)
if err != nil {
return err
}
}
if amountFlt > balanceFlt {
return fmt.Errorf(
"desired balance %f for destination %s exceeds available funding balance of %f",
amountFlt,
destination.blockchainDesc,
balanceFlt,
)
}
amountBigFlt := new(big.Float).SetFloat64(amountFlt)
amountBigFlt = amountBigFlt.Mul(amountBigFlt, new(big.Float).SetInt(vm.OneLux))
amount, _ := amountBigFlt.Int(nil)
receipt, err := client.FundAddress(privateKey, addr.Hex(), amount)
if err != nil {
return err
}
ux.Logger.PrintToUser("%s Paid fee: %.9f LUX",
destination.blockchainDesc,
evm.CalculateEvmFeeInLux(receipt.GasUsed, receipt.EffectiveGasPrice))
}
}
}
if deployToRemote {
return nil
}
runFilePath := app.GetLocalRelayerRunPath(network.Kind)
storageDir := app.GetLocalRelayerStorageDir(network.Kind)
localNetworkRootDir := ""
if network.Kind == models.Local {
localNetworkRootDir, err = localnet.GetLocalNetworkDir(app)
if err != nil {
return err
}
}
configPath := app.GetLocalRelayerConfigPath(network.Kind, localNetworkRootDir)
logPath := app.GetLocalRelayerLogPath(network.Kind)
metricsPort := constants.RemoteWarpRelayerMetricsPort
if !deployToRemote {
switch network.Kind {
case models.Local:
metricsPort = constants.LocalNetworkLocalWarpRelayerMetricsPort
case models.Devnet:
metricsPort = constants.DevnetLocalWarpRelayerMetricsPort
case models.Testnet:
metricsPort = constants.TestnetLocalWarpRelayerMetricsPort
}
}
// create config
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Generating relayer config file at %s", configPath)
if err := relayer.CreateBaseRelayerConfig(
configPath,
flags.LogLevel,
storageDir,
uint16(metricsPort),
network,
flags.AllowPrivateIPs,
); err != nil {
return err
}
for _, source := range configSpec.sources {
if err := relayer.AddSourceToRelayerConfig(
configPath,
source.rpcEndpoint,
source.wsEndpoint,
source.subnetID,
source.blockchainID,
source.warpRegistryAddress,
source.warpMessengerAddress,
source.rewardAddress,
); err != nil {
return err
}
}
for _, destination := range configSpec.destinations {
if err := relayer.AddDestinationToRelayerConfig(
configPath,
destination.rpcEndpoint,
destination.subnetID,
destination.blockchainID,
destination.privateKey,
); err != nil {
return err
}
}
if len(configSpec.sources) > 0 && len(configSpec.destinations) > 0 {
// relayer fails for empty configs
binPath, err := relayer.DeployRelayer(
flags.Version,
flags.BinPath,
app.GetWarpRelayerBinDir(),
configPath,
logPath,
runFilePath,
storageDir,
)
if err != nil {
if bs, err := os.ReadFile(logPath); err == nil {
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser(string(bs))
}
return err
}
if network.Kind == models.Local {
if err := localnet.WriteExtraLocalNetworkData(app, "", binPath, "", ""); err != nil {
return err
}
}
}
return nil
}