mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
452 lines
13 KiB
Go
452 lines
13 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package relayercmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/contract"
|
|
"github.com/luxfi/cli/v2/pkg/models"
|
|
"github.com/luxfi/cli/v2/pkg/prompts"
|
|
"github.com/luxfi/cli/v2/pkg/utils"
|
|
"github.com/luxfi/cli/v2/pkg/ux"
|
|
sdkutils "github.com/luxfi/cli/v2/sdk/utils"
|
|
"github.com/luxfi/node/v2/v2/utils/logging"
|
|
"github.com/olekukonko/tablewriter"
|
|
)
|
|
|
|
type SourceSpec struct {
|
|
blockchainDesc string
|
|
rpcEndpoint string
|
|
wsEndpoint string
|
|
blockchainID string
|
|
subnetID string
|
|
rewardAddress string
|
|
warpMessengerAddress string
|
|
warpRegistryAddress string
|
|
}
|
|
|
|
type DestinationSpec struct {
|
|
blockchainDesc string
|
|
rpcEndpoint string
|
|
blockchainID string
|
|
subnetID string
|
|
privateKey string
|
|
}
|
|
|
|
type ConfigSpec struct {
|
|
sources []SourceSpec
|
|
destinations []DestinationSpec
|
|
}
|
|
|
|
const (
|
|
explainOption = "Explain the difference"
|
|
cancelOption = "Cancel"
|
|
)
|
|
|
|
func preview(configSpec ConfigSpec) {
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
table.SetRowLine(true)
|
|
table.SetAutoMergeCellsByColumnIndex([]int{0})
|
|
if len(configSpec.sources) > 0 {
|
|
for _, source := range configSpec.sources {
|
|
table.Append([]string{"Source", source.blockchainDesc})
|
|
}
|
|
}
|
|
if len(configSpec.destinations) > 0 {
|
|
for _, destination := range configSpec.destinations {
|
|
table.Append([]string{"Destination", destination.blockchainDesc})
|
|
}
|
|
}
|
|
table.Render()
|
|
fmt.Println()
|
|
}
|
|
|
|
func addBoth(network models.Network, configSpec ConfigSpec, chainSpec contract.ChainSpec, defaultKey string) (ConfigSpec, error) {
|
|
prompt := "Which blockchain do you want to set both as source and destination?"
|
|
var err error
|
|
if !chainSpec.Defined() {
|
|
chainSpec, err = getBlockchain(network, prompt)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
}
|
|
rpcEndpoint, wsEndpoint, err := contract.GetBlockchainEndpoints(app, network, chainSpec, true, true)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
configSpec, err = addSource(network, configSpec, chainSpec, rpcEndpoint, wsEndpoint, defaultKey)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
configSpec, err = addDestination(network, configSpec, chainSpec, rpcEndpoint, defaultKey)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
return configSpec, nil
|
|
}
|
|
|
|
func getBlockchain(network models.Network, prompt string) (contract.ChainSpec, error) {
|
|
chainSpec := contract.ChainSpec{}
|
|
chainSpec.SetEnabled(true, true, false, false, true)
|
|
if cancel, err := contract.PromptChain(
|
|
app,
|
|
network,
|
|
prompt,
|
|
"",
|
|
&chainSpec,
|
|
); err != nil {
|
|
return chainSpec, err
|
|
} else if cancel {
|
|
return chainSpec, fmt.Errorf("cancelled by user")
|
|
}
|
|
return chainSpec, nil
|
|
}
|
|
|
|
func addSource(
|
|
network models.Network,
|
|
configSpec ConfigSpec,
|
|
chainSpec contract.ChainSpec,
|
|
rpcEndpoint string,
|
|
wsEndpoint string,
|
|
defaultKey string,
|
|
) (ConfigSpec, error) {
|
|
if !chainSpec.Defined() {
|
|
prompt := "Which blockchain do you want to set as source?"
|
|
var err error
|
|
chainSpec, err = getBlockchain(network, prompt)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
rpcEndpoint, wsEndpoint, err = contract.GetBlockchainEndpoints(app, network, chainSpec, true, true)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
}
|
|
blockchainID, err := contract.GetBlockchainID(app, network, chainSpec)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
if foundSource := utils.Find(configSpec.sources, func(s SourceSpec) bool { return s.blockchainID == blockchainID.String() }); foundSource != nil {
|
|
ux.Logger.PrintToUser("blockchain is already a source")
|
|
return configSpec, nil
|
|
}
|
|
blockchainDesc, err := contract.GetBlockchainDesc(chainSpec)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
subnetID, err := contract.GetSubnetID(app, network, chainSpec)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
warpRegistryAddress, warpMessengerAddress, err := contract.GetWarpInfo(app, network, chainSpec, true, true, false)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
rewardAddress := ""
|
|
if defaultKey != "" {
|
|
k, err := app.GetKey(defaultKey, network, false)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
rewardAddress = k.C()
|
|
} else {
|
|
genesisAddress, _, err := contract.GetEVMSubnetPrefundedKey(
|
|
app,
|
|
network,
|
|
chainSpec,
|
|
)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
rewardAddress, err = prompts.PromptAddress(
|
|
app.Prompt,
|
|
fmt.Sprintf("receive relayer rewards on %s", blockchainDesc),
|
|
app.GetKeyDir(),
|
|
app.GetKey,
|
|
genesisAddress,
|
|
network,
|
|
prompts.EVMFormat,
|
|
"Address",
|
|
)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
}
|
|
configSpec.sources = append(configSpec.sources, SourceSpec{
|
|
blockchainDesc: blockchainDesc,
|
|
blockchainID: blockchainID.String(),
|
|
subnetID: subnetID.String(),
|
|
rewardAddress: rewardAddress,
|
|
warpRegistryAddress: warpRegistryAddress,
|
|
warpMessengerAddress: warpMessengerAddress,
|
|
rpcEndpoint: rpcEndpoint,
|
|
wsEndpoint: wsEndpoint,
|
|
})
|
|
return configSpec, nil
|
|
}
|
|
|
|
func addDestination(
|
|
network models.Network,
|
|
configSpec ConfigSpec,
|
|
chainSpec contract.ChainSpec,
|
|
rpcEndpoint string,
|
|
defaultKey string,
|
|
) (ConfigSpec, error) {
|
|
if !chainSpec.Defined() {
|
|
prompt := "Which blockchain do you want to set as destination?"
|
|
var err error
|
|
chainSpec, err = getBlockchain(network, prompt)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
rpcEndpoint, _, err = contract.GetBlockchainEndpoints(app, network, chainSpec, true, false)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
}
|
|
blockchainID, err := contract.GetBlockchainID(app, network, chainSpec)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
if foundDestination := utils.Find(configSpec.destinations, func(s DestinationSpec) bool { return s.blockchainID == blockchainID.String() }); foundDestination != nil {
|
|
ux.Logger.PrintToUser("blockchain is already a destination")
|
|
return configSpec, nil
|
|
}
|
|
blockchainDesc, err := contract.GetBlockchainDesc(chainSpec)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
subnetID, err := contract.GetSubnetID(app, network, chainSpec)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
privateKey := ""
|
|
if defaultKey != "" {
|
|
k, err := app.GetKey(defaultKey, network, false)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
privateKey = k.PrivKeyHex()
|
|
} else {
|
|
ux.Logger.PrintToUser(logging.Yellow.Wrap("Please provide a key that is not going to be used for any other purpose on destination"))
|
|
privateKey, err = prompts.PromptPrivateKey(
|
|
app.Prompt,
|
|
fmt.Sprintf("pay relayer fees on %s", blockchainDesc),
|
|
app.GetKeyDir(),
|
|
app.GetKey,
|
|
"",
|
|
"",
|
|
)
|
|
if err != nil {
|
|
return ConfigSpec{}, err
|
|
}
|
|
}
|
|
configSpec.destinations = append(configSpec.destinations, DestinationSpec{
|
|
blockchainDesc: blockchainDesc,
|
|
blockchainID: blockchainID.String(),
|
|
subnetID: subnetID.String(),
|
|
privateKey: privateKey,
|
|
rpcEndpoint: rpcEndpoint,
|
|
})
|
|
return configSpec, nil
|
|
}
|
|
|
|
func removeSource(
|
|
configSpec ConfigSpec,
|
|
) (ConfigSpec, bool, error) {
|
|
if len(configSpec.sources) == 0 {
|
|
ux.Logger.PrintToUser("There are no sources to remove")
|
|
ux.Logger.PrintToUser("")
|
|
return configSpec, true, nil
|
|
}
|
|
prompt := "Select the source you want to remove"
|
|
options := sdkutils.Map(configSpec.sources, func(s SourceSpec) string { return s.blockchainDesc })
|
|
options = append(options, cancelOption)
|
|
opt, err := app.Prompt.CaptureList(prompt, options)
|
|
if err != nil {
|
|
return configSpec, false, err
|
|
}
|
|
if opt != cancelOption {
|
|
configSpec.sources = utils.Filter(configSpec.sources, func(s SourceSpec) bool { return s.blockchainDesc != opt })
|
|
return configSpec, false, nil
|
|
}
|
|
return configSpec, true, nil
|
|
}
|
|
|
|
func removeDestination(
|
|
configSpec ConfigSpec,
|
|
) (ConfigSpec, bool, error) {
|
|
if len(configSpec.destinations) == 0 {
|
|
ux.Logger.PrintToUser("There are no destinations to remove")
|
|
ux.Logger.PrintToUser("")
|
|
return configSpec, true, nil
|
|
}
|
|
prompt := "Select the destination you want to remove"
|
|
options := sdkutils.Map(configSpec.destinations, func(d DestinationSpec) string { return d.blockchainDesc })
|
|
options = append(options, cancelOption)
|
|
opt, err := app.Prompt.CaptureList(prompt, options)
|
|
if err != nil {
|
|
return configSpec, false, err
|
|
}
|
|
if opt != cancelOption {
|
|
configSpec.destinations = utils.Filter(configSpec.destinations, func(d DestinationSpec) bool { return d.blockchainDesc != opt })
|
|
return configSpec, false, nil
|
|
}
|
|
return configSpec, true, nil
|
|
}
|
|
|
|
func GenerateConfigSpec(
|
|
network models.Network,
|
|
relayCChain bool,
|
|
blockchainsToRelay []string,
|
|
defaultKey string,
|
|
) (ConfigSpec, bool, error) {
|
|
configSpec := ConfigSpec{}
|
|
var err error
|
|
|
|
noPrompts := false
|
|
if relayCChain {
|
|
chainSpec := contract.ChainSpec{
|
|
CChain: true,
|
|
}
|
|
chainSpec.SetEnabled(true, true, false, false, false)
|
|
configSpec, err = addBoth(network, configSpec, chainSpec, defaultKey)
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
noPrompts = true
|
|
}
|
|
for _, blockchainName := range blockchainsToRelay {
|
|
chainSpec := contract.ChainSpec{
|
|
BlockchainName: blockchainName,
|
|
}
|
|
chainSpec.SetEnabled(true, true, false, false, false)
|
|
configSpec, err = addBoth(network, configSpec, chainSpec, defaultKey)
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
noPrompts = true
|
|
}
|
|
if noPrompts {
|
|
return configSpec, false, nil
|
|
}
|
|
|
|
prompt := "Configure the blockchains that will be interconnected by the relayer"
|
|
|
|
addOption := "Add a blockchain"
|
|
removeOption := "Remove a blockchain"
|
|
previewOption := "Preview"
|
|
confirmOption := "Confirm"
|
|
|
|
for {
|
|
options := []string{
|
|
addOption,
|
|
removeOption,
|
|
previewOption,
|
|
confirmOption,
|
|
cancelOption,
|
|
}
|
|
if len(configSpec.sources) == 0 && len(configSpec.destinations) == 0 {
|
|
options = utils.RemoveFromSlice(options, removeOption)
|
|
options = utils.RemoveFromSlice(options, previewOption)
|
|
options = utils.RemoveFromSlice(options, confirmOption)
|
|
}
|
|
option, err := app.Prompt.CaptureList(prompt, options)
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
switch option {
|
|
case addOption:
|
|
addPrompt := "What role should the blockchain have?"
|
|
addBothOption := "Source and Destination"
|
|
addSourceOption := "Source only"
|
|
addDestinationOption := "Destination only"
|
|
for {
|
|
options := []string{addBothOption, addSourceOption, addDestinationOption, explainOption, cancelOption}
|
|
roleOption, err := app.Prompt.CaptureList(addPrompt, options)
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
switch roleOption {
|
|
case addBothOption:
|
|
configSpec, err = addBoth(network, configSpec, contract.ChainSpec{}, "")
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
case addSourceOption:
|
|
configSpec, err = addSource(network, configSpec, contract.ChainSpec{}, "", "", "")
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
case addDestinationOption:
|
|
configSpec, err = addDestination(network, configSpec, contract.ChainSpec{}, "", "")
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
case explainOption:
|
|
ux.Logger.PrintToUser("A source blockchain is going to be listened by the relayer to check for new")
|
|
ux.Logger.PrintToUser("messages. You need to specify blockchain ID, Warp addresses.")
|
|
ux.Logger.PrintToUser("A destination blockchain is going to be connected by the relayer in order")
|
|
ux.Logger.PrintToUser("to deliver a message. You need to specify blockchain ID, private key")
|
|
continue
|
|
case cancelOption:
|
|
}
|
|
break
|
|
}
|
|
case removeOption:
|
|
keepAsking := true
|
|
for keepAsking {
|
|
removePrompt := "Which role do you want to remove?"
|
|
removeSourceOption := "Source"
|
|
removeDestinationOption := "Destination"
|
|
options := []string{}
|
|
if len(configSpec.sources) != 0 {
|
|
options = append(options, removeSourceOption)
|
|
}
|
|
if len(configSpec.destinations) != 0 {
|
|
options = append(options, removeDestinationOption)
|
|
}
|
|
options = append(options, cancelOption)
|
|
kindOption, err := app.Prompt.CaptureList(removePrompt, options)
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
switch kindOption {
|
|
case removeSourceOption:
|
|
configSpec, keepAsking, err = removeSource(configSpec)
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
case removeDestinationOption:
|
|
configSpec, keepAsking, err = removeDestination(configSpec)
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
case cancelOption:
|
|
keepAsking = false
|
|
}
|
|
}
|
|
case previewOption:
|
|
preview(configSpec)
|
|
case confirmOption:
|
|
preview(configSpec)
|
|
confirmPrompt := "Confirm?"
|
|
yesOption := "Yes"
|
|
noOption := "No, keep editing"
|
|
confirmOption, err := app.Prompt.CaptureList(
|
|
confirmPrompt, []string{yesOption, noOption},
|
|
)
|
|
if err != nil {
|
|
return ConfigSpec{}, false, err
|
|
}
|
|
if confirmOption == yesOption {
|
|
return configSpec, false, nil
|
|
}
|
|
case cancelOption:
|
|
return ConfigSpec{}, true, err
|
|
}
|
|
}
|
|
}
|