mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
489 lines
15 KiB
Go
489 lines
15 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
package blockchaincmd
|
||
|
||
import (
|
||
"encoding/hex"
|
||
"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"
|
||
warpgenesis "github.com/luxfi/cli/v2/pkg/interchain/genesis"
|
||
"github.com/luxfi/cli/v2/pkg/key"
|
||
"github.com/luxfi/cli/v2/pkg/localnet"
|
||
"github.com/luxfi/cli/v2/pkg/models"
|
||
"github.com/luxfi/cli/v2/pkg/subnet"
|
||
"github.com/luxfi/cli/v2/pkg/txutils"
|
||
"github.com/luxfi/cli/v2/pkg/utils"
|
||
"github.com/luxfi/cli/v2/pkg/ux"
|
||
"github.com/luxfi/cli/v2/pkg/vm"
|
||
validatorManagerSDK "github.com/luxfi/cli/v2/sdk/validatormanager"
|
||
"github.com/luxfi/ids"
|
||
"github.com/luxfi/node/v2/v2/utils/logging"
|
||
"github.com/luxfi/evm/v2/core"
|
||
"github.com/luxfi/evm/v2/params"
|
||
"github.com/luxfi/evm/v2/precompile/contracts/deployerallowlist"
|
||
"github.com/luxfi/evm/v2/precompile/contracts/feemanager"
|
||
"github.com/luxfi/evm/v2/precompile/contracts/nativeminter"
|
||
"github.com/luxfi/evm/v2/precompile/contracts/rewardmanager"
|
||
"github.com/luxfi/evm/v2/precompile/contracts/txallowlist"
|
||
"github.com/luxfi/evm/v2/precompile/contracts/warp"
|
||
"github.com/ethereum/go-ethereum/common"
|
||
"github.com/jedib0t/go-pretty/v6/table"
|
||
"github.com/jedib0t/go-pretty/v6/text"
|
||
"github.com/spf13/cobra"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
var printGenesisOnly bool
|
||
|
||
// lux blockchain describe
|
||
func newDescribeCmd() *cobra.Command {
|
||
cmd := &cobra.Command{
|
||
Use: "describe [blockchainName]",
|
||
Short: "Print a summary of the blockchain’s configuration",
|
||
Long: `The blockchain describe command prints the details of a Blockchain configuration to the console.
|
||
By default, the command prints a summary of the configuration. By providing the --genesis
|
||
flag, the command instead prints out the raw genesis file.`,
|
||
RunE: describe,
|
||
Args: cobrautils.ExactArgs(1),
|
||
}
|
||
cmd.Flags().BoolVarP(
|
||
&printGenesisOnly,
|
||
"genesis",
|
||
"g",
|
||
false,
|
||
"Print the genesis to the console directly instead of the summary",
|
||
)
|
||
return cmd
|
||
}
|
||
|
||
func printGenesis(blockchainName string) error {
|
||
genesisFile := app.GetGenesisPath(blockchainName)
|
||
gen, err := os.ReadFile(genesisFile)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ux.Logger.PrintToUser("")
|
||
ux.Logger.PrintToUser(string(gen))
|
||
return nil
|
||
}
|
||
|
||
func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error {
|
||
sc, err := app.LoadSidecar(blockchainName)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
genesisBytes, err := app.LoadRawGenesis(sc.Subnet)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// VM/Deploys
|
||
t := ux.DefaultTable(sc.Name, nil)
|
||
t.SetColumnConfigs([]table.ColumnConfig{
|
||
{Number: 1, AutoMerge: true},
|
||
})
|
||
rowConfig := table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignLeft}
|
||
t.AppendRow(table.Row{"Name", sc.Name, sc.Name}, rowConfig)
|
||
vmIDstr := sc.ImportedVMID
|
||
if vmIDstr == "" {
|
||
vmID, err := utils.VMID(sc.Name)
|
||
if err == nil {
|
||
vmIDstr = vmID.String()
|
||
} else {
|
||
vmIDstr = constants.NotAvailableLabel
|
||
}
|
||
}
|
||
t.AppendRow(table.Row{"VM ID", vmIDstr, vmIDstr}, rowConfig)
|
||
t.AppendRow(table.Row{"VM Version", sc.VMVersion, sc.VMVersion}, rowConfig)
|
||
t.AppendRow(table.Row{"Validation", sc.ValidatorManagement, sc.ValidatorManagement}, rowConfig)
|
||
|
||
locallyDeployed := false
|
||
localEndpoint := ""
|
||
localChainID := ""
|
||
for net, data := range sc.Networks {
|
||
network, err := app.GetNetworkFromSidecarNetworkName(net)
|
||
if err != nil {
|
||
ux.Logger.RedXToUser("%s is supposed to be deployed to network %s: %s ", blockchainName, network.Name(), err)
|
||
ux.Logger.PrintToUser("")
|
||
continue
|
||
}
|
||
if network.Kind != models.Local && onlyLocalnetInfo {
|
||
continue
|
||
}
|
||
genesisBytes, err := contract.GetBlockchainGenesis(
|
||
app,
|
||
network,
|
||
contract.ChainSpec{
|
||
BlockchainName: sc.Name,
|
||
},
|
||
)
|
||
if err != nil {
|
||
if network.Kind != models.Local {
|
||
return err
|
||
}
|
||
// ignore local network errors for cases
|
||
// where local network is down but sidecar contains
|
||
// local network metadata
|
||
// (eg host restarts)
|
||
continue
|
||
} else if network.Kind == models.Local {
|
||
locallyDeployed = true
|
||
}
|
||
if utils.ByteSliceIsSubnetEvmGenesis(genesisBytes) {
|
||
genesis, err := utils.ByteSliceToSubnetEvmGenesis(genesisBytes)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
t.AppendRow(table.Row{net, "ChainID", genesis.Config.ChainID.String()})
|
||
if network.Kind == models.Local {
|
||
localChainID = genesis.Config.ChainID.String()
|
||
}
|
||
}
|
||
if data.SubnetID != ids.Empty {
|
||
t.AppendRow(table.Row{net, "SubnetID", data.SubnetID.String()})
|
||
_, owners, threshold, err := txutils.GetOwners(network, data.SubnetID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
t.AppendRow(table.Row{net, fmt.Sprintf("Owners (Threhold=%d)", threshold), strings.Join(owners, "\n")})
|
||
}
|
||
if data.BlockchainID != ids.Empty {
|
||
hexEncoding := "0x" + hex.EncodeToString(data.BlockchainID[:])
|
||
t.AppendRow(table.Row{net, "BlockchainID (CB58)", data.BlockchainID.String()})
|
||
t.AppendRow(table.Row{net, "BlockchainID (HEX)", hexEncoding})
|
||
}
|
||
endpoint, _, err := contract.GetBlockchainEndpoints(
|
||
app,
|
||
network,
|
||
contract.ChainSpec{
|
||
BlockchainName: sc.Name,
|
||
},
|
||
false,
|
||
false,
|
||
)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if network.Kind == models.Local {
|
||
localEndpoint = endpoint
|
||
}
|
||
t.AppendRow(table.Row{net, "RPC Endpoint", endpoint})
|
||
if data.ValidatorManagerAddress != "" {
|
||
t.AppendRow(table.Row{net, "Manager", data.ValidatorManagerAddress})
|
||
}
|
||
}
|
||
ux.Logger.PrintToUser(t.Render())
|
||
|
||
// Warp
|
||
t = ux.DefaultTable("Warp", nil)
|
||
t.SetColumnConfigs([]table.ColumnConfig{
|
||
{Number: 1, AutoMerge: true},
|
||
})
|
||
hasWarpInfo := false
|
||
for net, data := range sc.Networks {
|
||
network, err := app.GetNetworkFromSidecarNetworkName(net)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if network.Kind == models.Local && !locallyDeployed {
|
||
continue
|
||
}
|
||
if network.Kind != models.Local && onlyLocalnetInfo {
|
||
continue
|
||
}
|
||
if data.TeleporterMessengerAddress != "" {
|
||
t.AppendRow(table.Row{net, "Warp Messenger Address", data.TeleporterMessengerAddress})
|
||
hasWarpInfo = true
|
||
}
|
||
if data.TeleporterRegistryAddress != "" {
|
||
t.AppendRow(table.Row{net, "Warp Registry Address", data.TeleporterRegistryAddress})
|
||
hasWarpInfo = true
|
||
}
|
||
}
|
||
if hasWarpInfo {
|
||
ux.Logger.PrintToUser("")
|
||
ux.Logger.PrintToUser(t.Render())
|
||
}
|
||
|
||
// Token
|
||
ux.Logger.PrintToUser("")
|
||
t = ux.DefaultTable("Token", nil)
|
||
t.AppendRow(table.Row{"Token Name", sc.TokenName})
|
||
t.AppendRow(table.Row{"Token Symbol", sc.TokenSymbol})
|
||
ux.Logger.PrintToUser(t.Render())
|
||
|
||
if utils.ByteSliceIsSubnetEvmGenesis(genesisBytes) {
|
||
genesis, err := utils.ByteSliceToSubnetEvmGenesis(genesisBytes)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := printAllocations(sc, genesis); err != nil {
|
||
return err
|
||
}
|
||
printSmartContracts(sc, genesis)
|
||
printPrecompiles(genesis)
|
||
}
|
||
|
||
if locallyDeployed {
|
||
ux.Logger.PrintToUser("")
|
||
if err := localnet.PrintEndpoints(app, ux.Logger.PrintToUser, sc.Name); err != nil {
|
||
return err
|
||
}
|
||
|
||
codespaceEndpoint, err := utils.GetCodespaceURL(localEndpoint)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if codespaceEndpoint != "" {
|
||
_, port, _, err := utils.GetURIHostPortAndPath(localEndpoint)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
localEndpoint = codespaceEndpoint + "\n" + logging.Orange.Wrap(
|
||
fmt.Sprintf("Please make sure to set visibility of port %d to public", port),
|
||
)
|
||
}
|
||
|
||
// Wallet
|
||
t = ux.DefaultTable("Wallet Connection", nil)
|
||
t.AppendRow(table.Row{"Network RPC URL", localEndpoint})
|
||
t.AppendRow(table.Row{"Network Name", sc.Name})
|
||
t.AppendRow(table.Row{"Chain ID", localChainID})
|
||
t.AppendRow(table.Row{"Token Symbol", sc.TokenSymbol})
|
||
t.AppendRow(table.Row{"Token Name", sc.TokenName})
|
||
ux.Logger.PrintToUser("")
|
||
ux.Logger.PrintToUser(t.Render())
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func printAllocations(sc models.Sidecar, genesis core.Genesis) error {
|
||
warpKeyAddress := ""
|
||
if sc.TeleporterReady {
|
||
k, err := key.LoadSoft(models.NewLocalNetwork().ID, app.GetKeyPath(sc.TeleporterKey))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
warpKeyAddress = k.C()
|
||
}
|
||
_, subnetAirdropAddress, _, err := subnet.GetDefaultSubnetAirdropKeyInfo(app, sc.Name)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(genesis.Alloc) > 0 {
|
||
ux.Logger.PrintToUser("")
|
||
t := ux.DefaultTable(
|
||
"Initial Token Allocation",
|
||
table.Row{
|
||
"Description",
|
||
"Address and Private Key",
|
||
fmt.Sprintf("Amount (%s)", sc.TokenSymbol),
|
||
"Amount (wei)",
|
||
},
|
||
)
|
||
for address, allocation := range genesis.Alloc {
|
||
amount := allocation.Balance
|
||
// we are only interested in supply distribution here
|
||
if amount == nil || big.NewInt(0).Cmp(amount) == 0 {
|
||
continue
|
||
}
|
||
formattedAmount := new(big.Int).Div(amount, big.NewInt(params.Ether))
|
||
description := ""
|
||
privKey := ""
|
||
switch address.Hex() {
|
||
case warpKeyAddress:
|
||
description = logging.Orange.Wrap("Used by Warp")
|
||
case subnetAirdropAddress:
|
||
description = logging.Orange.Wrap("Main funded account")
|
||
case vm.PrefundedEwoqAddress.Hex():
|
||
description = logging.Orange.Wrap("Main funded account")
|
||
case sc.ValidatorManagerOwner:
|
||
description = logging.Orange.Wrap("Validator Manager Owner")
|
||
case sc.ProxyContractOwner:
|
||
description = logging.Orange.Wrap("Proxy Admin Owner")
|
||
}
|
||
var (
|
||
found bool
|
||
name string
|
||
)
|
||
found, name, _, privKey, err = contract.SearchForManagedKey(app, models.NewLocalNetwork(), address, true)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if found {
|
||
description = fmt.Sprintf("%s\n%s", description, name)
|
||
}
|
||
t.AppendRow(table.Row{description, address.Hex() + "\n" + privKey, formattedAmount.String(), amount.String()})
|
||
}
|
||
ux.Logger.PrintToUser(t.Render())
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func printSmartContracts(sc models.Sidecar, genesis core.Genesis) {
|
||
if len(genesis.Alloc) == 0 {
|
||
return
|
||
}
|
||
ux.Logger.PrintToUser("")
|
||
t := ux.DefaultTable(
|
||
"Smart Contracts",
|
||
table.Row{"Description", "Address", "Deployer"},
|
||
)
|
||
for address, allocation := range genesis.Alloc {
|
||
if len(allocation.Code) == 0 {
|
||
continue
|
||
}
|
||
var description, deployer string
|
||
switch {
|
||
case address == common.HexToAddress(warpgenesis.MessengerContractAddress):
|
||
description = "Warp Messenger"
|
||
deployer = warpgenesis.MessengerDeployerAddress
|
||
case address == common.HexToAddress(validatorManagerSDK.ValidatorMessagesContractAddress):
|
||
description = "Validator Messages Lib"
|
||
case address == common.HexToAddress(validatorManagerSDK.ValidatorContractAddress):
|
||
if sc.PoA() {
|
||
description = "PoA Validator Manager"
|
||
} else {
|
||
description = "Native Token Staking Manager"
|
||
}
|
||
if sc.UseACP99 {
|
||
description = "ACP99 Compatible " + description
|
||
} else {
|
||
description = "v1.0.0 Compatible " + description
|
||
}
|
||
case address == common.HexToAddress(validatorManagerSDK.ValidatorProxyContractAddress):
|
||
description = "Validator Transparent Proxy"
|
||
case address == common.HexToAddress(validatorManagerSDK.ValidatorProxyAdminContractAddress):
|
||
description = "Validator Proxy Admin"
|
||
deployer = sc.ProxyContractOwner
|
||
case address == common.HexToAddress(validatorManagerSDK.SpecializationProxyContractAddress):
|
||
description = "Validator Specialization Transparent Proxy"
|
||
case address == common.HexToAddress(validatorManagerSDK.SpecializationProxyAdminContractAddress):
|
||
description = "Validator Specialization Proxy Admin"
|
||
case address == common.HexToAddress(validatorManagerSDK.RewardCalculatorAddress):
|
||
description = "Reward Calculator"
|
||
}
|
||
t.AppendRow(table.Row{description, address.Hex(), deployer})
|
||
}
|
||
ux.Logger.PrintToUser(t.Render())
|
||
}
|
||
|
||
func printPrecompiles(genesis core.Genesis) {
|
||
ux.Logger.PrintToUser("")
|
||
t := ux.DefaultTable(
|
||
"Initial Precompile Configs",
|
||
table.Row{"Precompile", "Admin Addresses", "Manager Addresses", "Enabled Addresses"},
|
||
)
|
||
t.Style().Options.SeparateRows = false
|
||
t.SetColumnConfigs([]table.ColumnConfig{
|
||
{Number: 1, AutoMerge: true},
|
||
})
|
||
|
||
warpSet := false
|
||
allowListSet := false
|
||
// Warp
|
||
if genesis.Config.GenesisPrecompiles[warp.ConfigKey] != nil {
|
||
t.AppendRow(table.Row{"Warp", "n/a", "n/a", "n/a"})
|
||
warpSet = true
|
||
}
|
||
// Native Minting
|
||
if genesis.Config.GenesisPrecompiles[nativeminter.ConfigKey] != nil {
|
||
cfg := genesis.Config.GenesisPrecompiles[nativeminter.ConfigKey].(*nativeminter.Config)
|
||
addPrecompileAllowListToTable(t, "Native Minter", cfg.AdminAddresses, cfg.ManagerAddresses, cfg.EnabledAddresses)
|
||
allowListSet = true
|
||
}
|
||
// Contract allow list
|
||
if genesis.Config.GenesisPrecompiles[deployerallowlist.ConfigKey] != nil {
|
||
cfg := genesis.Config.GenesisPrecompiles[deployerallowlist.ConfigKey].(*deployerallowlist.Config)
|
||
addPrecompileAllowListToTable(t, "Contract Allow List", cfg.AdminAddresses, cfg.ManagerAddresses, cfg.EnabledAddresses)
|
||
allowListSet = true
|
||
}
|
||
// TX allow list
|
||
if genesis.Config.GenesisPrecompiles[txallowlist.ConfigKey] != nil {
|
||
cfg := genesis.Config.GenesisPrecompiles[txallowlist.Module.ConfigKey].(*txallowlist.Config)
|
||
addPrecompileAllowListToTable(t, "Tx Allow List", cfg.AdminAddresses, cfg.ManagerAddresses, cfg.EnabledAddresses)
|
||
allowListSet = true
|
||
}
|
||
// Fee config allow list
|
||
if genesis.Config.GenesisPrecompiles[feemanager.ConfigKey] != nil {
|
||
cfg := genesis.Config.GenesisPrecompiles[feemanager.ConfigKey].(*feemanager.Config)
|
||
addPrecompileAllowListToTable(t, "Fee Config Allow List", cfg.AdminAddresses, cfg.ManagerAddresses, cfg.EnabledAddresses)
|
||
allowListSet = true
|
||
}
|
||
// Reward config allow list
|
||
if genesis.Config.GenesisPrecompiles[rewardmanager.ConfigKey] != nil {
|
||
cfg := genesis.Config.GenesisPrecompiles[rewardmanager.ConfigKey].(*rewardmanager.Config)
|
||
addPrecompileAllowListToTable(t, "Reward Manager Allow List", cfg.AdminAddresses, cfg.ManagerAddresses, cfg.EnabledAddresses)
|
||
allowListSet = true
|
||
}
|
||
if warpSet || allowListSet {
|
||
ux.Logger.PrintToUser(t.Render())
|
||
if allowListSet {
|
||
note := logging.Orange.Wrap("The allowlist is taken from the genesis and is not being updated if you make adjustments\nvia the precompile. Use readAllowList(address) instead.")
|
||
ux.Logger.PrintToUser(note)
|
||
}
|
||
}
|
||
}
|
||
|
||
func addPrecompileAllowListToTable(
|
||
t table.Writer,
|
||
label string,
|
||
adminAddresses []common.Address,
|
||
managerAddresses []common.Address,
|
||
enabledAddresses []common.Address,
|
||
) {
|
||
t.AppendSeparator()
|
||
admins := len(adminAddresses)
|
||
managers := len(managerAddresses)
|
||
enabled := len(enabledAddresses)
|
||
max := max(admins, managers, enabled)
|
||
for i := 0; i < max; i++ {
|
||
var admin, manager, enable string
|
||
if i < len(adminAddresses) && adminAddresses[i] != (common.Address{}) {
|
||
admin = adminAddresses[i].Hex()
|
||
}
|
||
if i < len(managerAddresses) && managerAddresses[i] != (common.Address{}) {
|
||
manager = managerAddresses[i].Hex()
|
||
}
|
||
if i < len(enabledAddresses) && enabledAddresses[i] != (common.Address{}) {
|
||
enable = enabledAddresses[i].Hex()
|
||
}
|
||
t.AppendRow(table.Row{label, admin, manager, enable})
|
||
}
|
||
}
|
||
|
||
func describe(_ *cobra.Command, args []string) error {
|
||
blockchainName := args[0]
|
||
if !app.GenesisExists(blockchainName) {
|
||
ux.Logger.PrintToUser("The provided blockchain name %q does not exist", blockchainName)
|
||
return nil
|
||
}
|
||
if printGenesisOnly {
|
||
return printGenesis(blockchainName)
|
||
}
|
||
if err := PrintSubnetInfo(blockchainName, false); err != nil {
|
||
return err
|
||
}
|
||
if isEVM, _, err := app.HasSubnetEVMGenesis(blockchainName); err != nil {
|
||
return err
|
||
} else if !isEVM {
|
||
sc, err := app.LoadSidecar(blockchainName)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
app.Log.Warn("Unknown genesis format", zap.Any("vm-type", sc.VM))
|
||
ux.Logger.PrintToUser("")
|
||
ux.Logger.PrintToUser("Printing genesis")
|
||
return printGenesis(blockchainName)
|
||
}
|
||
return nil
|
||
}
|