mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
577 lines
19 KiB
Go
577 lines
19 KiB
Go
// Copyright (C) 2022, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
package subnetcmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/prompts"
|
|
"github.com/luxfi/node/v2/v2/utils/formatting/address"
|
|
|
|
"github.com/luxfi/cli/v2/cmd/flags"
|
|
"github.com/luxfi/cli/v2/pkg/constants"
|
|
"github.com/luxfi/cli/v2/pkg/models"
|
|
"github.com/luxfi/cli/v2/pkg/plugins"
|
|
"github.com/luxfi/cli/v2/pkg/subnet"
|
|
"github.com/luxfi/cli/v2/pkg/ux"
|
|
"github.com/luxfi/node/v2/v2/genesis"
|
|
"github.com/luxfi/ids"
|
|
luxlog "github.com/luxfi/log"
|
|
"github.com/luxfi/node/v2/v2/utils/rpc"
|
|
"github.com/luxfi/node/v2/v2/vms/platformvm"
|
|
"github.com/luxfi/node/v2/v2/vms/secp256k1fx"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const ewoqPChainAddr = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p"
|
|
|
|
var (
|
|
// path to node config file
|
|
luxConfigPath string
|
|
// path to node plugin dir
|
|
pluginDir string
|
|
// if true, print the manual instructions to screen
|
|
printManual bool
|
|
// skipWhitelistCheck if true doesn't prompt, skip the check
|
|
skipWhitelistCheck bool
|
|
// forceWhitelistCheck if true doesn't prompt, run the check
|
|
forceWhitelistCheck bool
|
|
// failIfNotValidating
|
|
failIfNotValidating bool
|
|
// if true, doesn't ask for overwriting the config file
|
|
forceWrite bool
|
|
// if true, validator is joining a permissionless subnet
|
|
joinElastic bool
|
|
// for permissionless subnet only: how much subnet native token will be staked in the validator
|
|
stakeAmount uint64
|
|
// default node ids of nodes in local network
|
|
defaultLocalNetworkNodeIDs = []string{"NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg", "NodeID-MFrZFVCXPv5iCn6M9K6XduxGTYp891xXZ", "NodeID-NFBbbJ4qCmNaCzeW7sxErhvWqvEQMnYcN", "NodeID-GWPcbFJZFfZreETSoWjPimr846mXEKCtu", "NodeID-P7oB2McjBGgW2NXXWVYjV8JEDFoW9xDE5"}
|
|
)
|
|
|
|
// lux subnet deploy
|
|
func newJoinCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "join [subnetName]",
|
|
Short: "Configure your validator node to begin validating a new subnet",
|
|
Long: `The subnet join command configures your validator node to begin validating a new Subnet.
|
|
|
|
To complete this process, you must have access to the machine running your validator. If the
|
|
CLI is running on the same machine as your validator, it can generate or update your node's
|
|
config file automatically. Alternatively, the command can print the necessary instructions
|
|
to update your node manually. To complete the validation process, the Subnet's admins must add
|
|
the NodeID of your validator to the Subnet's allow list by calling addValidator with your
|
|
NodeID.
|
|
|
|
After you update your validator's config, you need to restart your validator manually. If
|
|
you provide the --node-config flag, this command attempts to edit the config file
|
|
at that path.
|
|
|
|
This command currently only supports Subnets deployed on the Testnet and Mainnet.`,
|
|
RunE: joinCmd,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
cmd.Flags().StringVar(&luxConfigPath, "node-config", "", "file path of the node config file")
|
|
cmd.Flags().StringVar(&pluginDir, "plugin-dir", "", "file path of node's plugin directory")
|
|
cmd.Flags().BoolVar(&deployTestnet, "testnet", false, "join on `testnet` (alias for `testnet`)")
|
|
cmd.Flags().BoolVar(&deployTestnet, "testnet", false, "join on `testnet` (alias for `testnet`)")
|
|
cmd.Flags().BoolVar(&deployLocal, "local", false, "join on `local` (for elastic subnet only)")
|
|
cmd.Flags().BoolVar(&deployMainnet, "mainnet", false, "join on `mainnet`")
|
|
cmd.Flags().BoolVar(&printManual, "print", false, "if true, print the manual config without prompting")
|
|
cmd.Flags().BoolVar(&skipWhitelistCheck, "skip-whitelist-check", false, "if true, skip the whitelist check")
|
|
cmd.Flags().BoolVar(&forceWhitelistCheck, "force-whitelist-check", false, "if true, force the whitelist check")
|
|
cmd.Flags().BoolVar(&failIfNotValidating, "fail-if-not-validating", false, "fail if whitelist check fails")
|
|
cmd.Flags().StringVar(&nodeIDStr, "nodeID", "", "set the NodeID of the validator to check")
|
|
cmd.Flags().BoolVar(&forceWrite, "force-write", false, "if true, skip to prompt to overwrite the config file")
|
|
cmd.Flags().BoolVar(&joinElastic, "elastic", false, "set flag as true if joining elastic subnet")
|
|
cmd.Flags().Uint64Var(&stakeAmount, "stake-amount", 0, "amount of tokens to stake on validator")
|
|
cmd.Flags().StringVar(&startTimeStr, "start-time", "", "start time that validator starts validating")
|
|
cmd.Flags().DurationVar(&duration, "staking-period", 0, "how long validator validates for after start time")
|
|
return cmd
|
|
}
|
|
|
|
func joinCmd(_ *cobra.Command, args []string) error {
|
|
if printManual && (luxConfigPath != "" || pluginDir != "") {
|
|
return errors.New("--print cannot be used with --node-config or --plugin-dir")
|
|
}
|
|
|
|
chains, err := validateSubnetNameAndGetChains(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
subnetName := chains[0]
|
|
|
|
sc, err := app.LoadSidecar(subnetName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !flags.EnsureMutuallyExclusive([]bool{deployMainnet, deployTestnet}) {
|
|
return errors.New("--testnet and --mainnet are mutually exclusive")
|
|
}
|
|
|
|
var network models.Network
|
|
switch {
|
|
case deployLocal:
|
|
network = models.Local
|
|
case deployTestnet:
|
|
network = models.Testnet
|
|
case deployMainnet:
|
|
network = models.Mainnet
|
|
}
|
|
|
|
if network == models.Undefined {
|
|
if joinElastic {
|
|
networkToUpgrade, err := promptNetworkElastic(sc, "Which network is the elastic subnet that the node wants to join on?")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch networkToUpgrade {
|
|
case testnetDeployment:
|
|
return errors.New("joining elastic subnet is not yet supported on Testnet network")
|
|
case mainnetDeployment:
|
|
return errors.New("joining elastic subnet is not yet supported on Mainnet")
|
|
}
|
|
network = models.Local
|
|
} else {
|
|
networkStr, err := app.Prompt.CaptureList(
|
|
"Choose a network to validate on (this command only supports public networks)",
|
|
[]string{models.Testnet.String(), models.Mainnet.String()},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// flag provided
|
|
networkStr = strings.Title(networkStr)
|
|
// as we are allowing a flag, we need to check if a supported network has been provided
|
|
if !(networkStr == models.Testnet.String() || networkStr == models.Mainnet.String()) {
|
|
return errors.New("unsupported network")
|
|
}
|
|
network = models.NetworkFromString(networkStr)
|
|
}
|
|
}
|
|
|
|
if joinElastic {
|
|
return handleValidatorJoinElasticSubnet(sc, network, subnetName)
|
|
}
|
|
|
|
// used in E2E to simulate public network execution paths on a local network
|
|
if os.Getenv(constants.SimulatePublicNetwork) != "" {
|
|
network = models.Local
|
|
}
|
|
|
|
networkLower := strings.ToLower(network.String())
|
|
|
|
subnetID := sc.Networks[network.String()].SubnetID
|
|
if subnetID == ids.Empty {
|
|
return errNoSubnetID
|
|
}
|
|
subnetIDStr := subnetID.String()
|
|
|
|
if !skipWhitelistCheck {
|
|
yes := true
|
|
if !forceWhitelistCheck {
|
|
ask := "Would you like to check if your node is allowed to join this subnet?\n" +
|
|
"If not, the subnet's control key holder must call lux subnet\n" +
|
|
"addValidator with your NodeID."
|
|
ux.Logger.PrintToUser("%s", ask)
|
|
yes, err = app.Prompt.CaptureYesNo("Check whitelist?")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if yes {
|
|
isValidating, err := isNodeValidatingSubnet(subnetID, network)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isValidating {
|
|
if failIfNotValidating {
|
|
ux.Logger.PrintToUser("The node is not whitelisted to validate this subnet.")
|
|
return nil
|
|
}
|
|
ux.Logger.PrintToUser(`The node is not whitelisted to validate this subnet.
|
|
You can continue with this command, generating a config file or printing the whitelisting configuration,
|
|
but until the node is whitelisted, it will not be able to validate this subnet.`)
|
|
y, err := app.Prompt.CaptureYesNo("Do you wish to continue")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !y {
|
|
return nil
|
|
}
|
|
}
|
|
ux.Logger.PrintToUser("The node is already whitelisted! You are good to go.")
|
|
}
|
|
}
|
|
|
|
if printManual {
|
|
pluginDir = app.GetTmpPluginDir()
|
|
vmPath, err := plugins.CreatePlugin(app, sc.Name, pluginDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printJoinCmd(subnetIDStr, networkLower, vmPath)
|
|
return nil
|
|
}
|
|
|
|
// if **both** flags were set, nothing special needs to be done
|
|
// just check the following blocks
|
|
if luxConfigPath == "" && pluginDir == "" {
|
|
// both flags are NOT set
|
|
const (
|
|
choiceManual = "Manual"
|
|
choiceAutomatic = "Automatic"
|
|
)
|
|
choice, err := app.Prompt.CaptureList(
|
|
"How would you like to update the node config?",
|
|
[]string{choiceAutomatic, choiceManual},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if choice == choiceManual {
|
|
pluginDir = app.GetTmpPluginDir()
|
|
vmPath, err := plugins.CreatePlugin(app, sc.Name, pluginDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printJoinCmd(subnetIDStr, networkLower, vmPath)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// if choice is automatic, we just pass through this block
|
|
// or, pluginDir was set but not luxConfigPath
|
|
// if **both** flags were set, this will be skipped...
|
|
if luxConfigPath == "" {
|
|
luxConfigPath, err = plugins.FindLuxConfigPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if luxConfigPath != "" {
|
|
ux.Logger.PrintToUser(luxlog.Bold.Wrap(luxlog.Green.Wrap("Found a config file at %s")), luxConfigPath)
|
|
yes, err := app.Prompt.CaptureYesNo("Is this the file we should update?")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if yes {
|
|
ux.Logger.PrintToUser("Will use file at path %s to update the configuration", luxConfigPath)
|
|
} else {
|
|
luxConfigPath = ""
|
|
}
|
|
}
|
|
if luxConfigPath == "" {
|
|
luxConfigPath, err = app.Prompt.CaptureString("Path to your existing config file (or where it will be generated)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// ...but not this
|
|
luxConfigPath, err := plugins.SanitizePath(luxConfigPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// luxConfigPath was set but not pluginDir
|
|
// if **both** flags were set, this will be skipped...
|
|
if pluginDir == "" {
|
|
pluginDir, err = plugins.FindPluginDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pluginDir != "" {
|
|
ux.Logger.PrintToUser(luxlog.Bold.Wrap(luxlog.Green.Wrap("Found the VM plugin directory at %s")), pluginDir)
|
|
yes, err := app.Prompt.CaptureYesNo("Is this where we should install the VM?")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if yes {
|
|
ux.Logger.PrintToUser("Will use plugin directory at %s to install the VM", pluginDir)
|
|
} else {
|
|
pluginDir = ""
|
|
}
|
|
}
|
|
if pluginDir == "" {
|
|
pluginDir, err = app.Prompt.CaptureString("Path to your node plugin dir (likely node/build/plugins)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// ...but not this
|
|
pluginDir, err := plugins.SanitizePath(pluginDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
vmPath, err := plugins.CreatePlugin(app, sc.Name, pluginDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ux.Logger.PrintToUser("VM binary written to %s", vmPath)
|
|
|
|
if err := plugins.EditConfigFile(app, subnetIDStr, networkLower, luxConfigPath, forceWrite); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleValidatorJoinElasticSubnet(sc models.Sidecar, network models.Network, subnetName string) error {
|
|
fmt.Printf("network name %s \n", network.String())
|
|
if network != models.Local {
|
|
return errors.New("unsupported network")
|
|
}
|
|
if !checkIfSubnetIsElasticOnLocal(sc) {
|
|
return fmt.Errorf("%s is not an elastic subnet", subnetName)
|
|
}
|
|
nodeID, err := promptNodeIDToAdd(sc.Networks[models.Local.String()].SubnetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stakedTokenAmount, err := promptStakeAmount(subnetName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
start, stakeDuration, err := getTimeParameters(network, nodeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
endTime := start.Add(stakeDuration)
|
|
ux.Logger.PrintToUser("Inputs complete, issuing transaction for the provided validator to join elastic subnet...")
|
|
ux.Logger.PrintToUser("")
|
|
|
|
assetID := sc.ElasticSubnet[models.Local.String()].AssetID
|
|
testKey := genesis.EWOQKey
|
|
keyChain := secp256k1fx.NewKeychain(testKey)
|
|
subnetID := sc.Networks[models.Local.String()].SubnetID
|
|
txID, err := subnet.IssueAddPermissionlessValidatorTx(keyChain, subnetID, nodeID, stakedTokenAmount, assetID, uint64(start.Unix()), uint64(endTime.Unix()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ux.Logger.PrintToUser("Validator successfully joined elastic subnet!")
|
|
ux.Logger.PrintToUser("TX ID: %s", txID.String())
|
|
ux.Logger.PrintToUser("NodeID: %s", nodeID.String())
|
|
ux.Logger.PrintToUser("Network: %s", network.String())
|
|
ux.Logger.PrintToUser("Start time: %s", start.UTC().Format(constants.TimeParseLayout))
|
|
ux.Logger.PrintToUser("End time: %s", endTime.Format(constants.TimeParseLayout))
|
|
ux.Logger.PrintToUser("Stake Amount: %d", stakedTokenAmount)
|
|
if err = app.UpdateSidecarPermissionlessValidator(&sc, models.Local, nodeID.String(), txID); err != nil {
|
|
return fmt.Errorf("joining permissionless subnet was successful, but failed to update sidecar: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isNodeValidatingSubnet(subnetID ids.ID, network models.Network) (bool, error) {
|
|
var (
|
|
nodeID ids.NodeID
|
|
err error
|
|
)
|
|
if nodeIDStr == "" {
|
|
ux.Logger.PrintToUser("Next, we need the NodeID of the validator you want to whitelist.")
|
|
ux.Logger.PrintToUser("")
|
|
ux.Logger.PrintToUser("Check https://docs.lux.network/apis/node/apis/info#infogetnodeid for instructions about how to query the NodeID from your node")
|
|
ux.Logger.PrintToUser("(Edit host IP address and port to match your deployment, if needed).")
|
|
|
|
promptStr := "What is the NodeID of the validator you'd like to whitelist?"
|
|
nodeID, err = app.Prompt.CaptureNodeID(promptStr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
} else {
|
|
nodeID, err = ids.NodeIDFromString(nodeIDStr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
var api string
|
|
switch network {
|
|
case models.Testnet:
|
|
api = constants.TestnetAPIEndpoint
|
|
case models.Mainnet:
|
|
api = constants.MainnetAPIEndpoint
|
|
case models.Local:
|
|
api = constants.LocalAPIEndpoint
|
|
default:
|
|
return false, fmt.Errorf("network not supported")
|
|
}
|
|
|
|
pClient := platformvm.NewClient(api)
|
|
|
|
return checkIsValidating(subnetID, nodeID, pClient)
|
|
}
|
|
|
|
// PlatformClient interface for platform VM client operations
|
|
type PlatformClient interface {
|
|
GetCurrentValidators(ctx context.Context, subnetID ids.ID, nodeIDs []ids.NodeID, options ...rpc.Option) ([]platformvm.ClientPermissionlessValidator, error)
|
|
}
|
|
|
|
func checkIsValidating(subnetID ids.ID, nodeID ids.NodeID, pClient PlatformClient) (bool, error) {
|
|
// first check if the node is already an accepted validator on the subnet
|
|
ctx := context.Background()
|
|
nodeIDs := []ids.NodeID{nodeID}
|
|
vals, err := pClient.GetCurrentValidators(ctx, subnetID, nodeIDs)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, v := range vals {
|
|
// strictly this is not needed, as we are providing the nodeID as param
|
|
// just a double check
|
|
if v.NodeID == nodeID {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
// TODO: Check pending validators when GetPendingValidators method is available
|
|
// For now, we only check current validators
|
|
return false, nil
|
|
}
|
|
|
|
func promptNodeIDToAdd(subnetID ids.ID) (ids.NodeID, error) {
|
|
if nodeIDStr == "" {
|
|
// Get NodeIDs of all validators on the subnet
|
|
validators, err := subnet.GetSubnetValidators(subnetID)
|
|
if err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
// construct list of validators to choose from
|
|
var validatorList []string
|
|
fmt.Printf("defaultLocalNetworkNodeIDs %s \n", defaultLocalNetworkNodeIDs)
|
|
|
|
for _, localNodeID := range defaultLocalNetworkNodeIDs {
|
|
nodeIDFound := false
|
|
for _, v := range validators {
|
|
if v.NodeID.String() == localNodeID {
|
|
nodeIDFound = true
|
|
break
|
|
}
|
|
}
|
|
if !nodeIDFound {
|
|
fmt.Printf("adding validators %s \n", localNodeID)
|
|
|
|
validatorList = append(validatorList, localNodeID)
|
|
}
|
|
}
|
|
nodeIDStr, err = app.Prompt.CaptureList("Which validator you'd like to join this elastic subnet?", validatorList)
|
|
if err != nil {
|
|
return ids.EmptyNodeID, err
|
|
}
|
|
}
|
|
nodeID, err := ids.NodeIDFromString(nodeIDStr)
|
|
if err != nil {
|
|
return ids.NodeID{}, err
|
|
}
|
|
return nodeID, nil
|
|
}
|
|
|
|
func promptStakeAmount(subnetName string) (uint64, error) {
|
|
if stakeAmount > 0 {
|
|
return stakeAmount, nil
|
|
}
|
|
esc, err := app.LoadElasticSubnetConfig(subnetName)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
maxValidatorStake := fmt.Sprintf("Maximum Validator Stake (%d)", esc.MaxValidatorStake)
|
|
customWeight := "Custom (Has to be between minValidatorStake and maxValidatorStake defined during elastic subnet transformation)"
|
|
|
|
txt := "What amount of the subnet native token would you like to stake in the validator?"
|
|
weightOptions := []string{maxValidatorStake, customWeight}
|
|
|
|
weightOption, err := app.Prompt.CaptureList(txt, weightOptions)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
ctx := context.Background()
|
|
pClient := platformvm.NewClient(constants.LocalAPIEndpoint)
|
|
walletBalance, err := getAssetBalance(ctx, pClient, ewoqPChainAddr, esc.AssetID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
switch weightOption {
|
|
case maxValidatorStake:
|
|
return esc.MaxValidatorStake, nil
|
|
default:
|
|
return app.Prompt.CaptureUint64Compare(
|
|
txt,
|
|
[]prompts.Comparator{
|
|
{
|
|
Label: fmt.Sprintf("Max Validator Stake(%d)", esc.MaxValidatorStake),
|
|
Type: prompts.LessThanEq,
|
|
Value: esc.MaxValidatorStake,
|
|
},
|
|
{
|
|
Label: fmt.Sprintf("Min Validator Stake(%d)", esc.MinValidatorStake),
|
|
Type: prompts.MoreThanEq,
|
|
Value: esc.MinValidatorStake,
|
|
},
|
|
{
|
|
Label: fmt.Sprintf("Wallet Balance(%d)", walletBalance),
|
|
Type: prompts.LessThanEq,
|
|
Value: walletBalance,
|
|
},
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func printJoinCmd(subnetID string, networkID string, vmPath string) {
|
|
msg := `
|
|
To setup your node, you must do two things:
|
|
|
|
1. Add your VM binary to your node's plugin directory
|
|
2. Update your node config to start validating the subnet
|
|
|
|
To add the VM to your plugin directory, copy or scp from %s
|
|
|
|
If you installed node with the install script, your plugin directory is likely
|
|
~/.node/build/plugins.
|
|
|
|
If you start your node from the command line WITHOUT a config file (e.g. via command
|
|
line or systemd script), add the following flag to your node's startup command:
|
|
|
|
--track-subnets=%s
|
|
(if the node already has a track-subnets config, append the new value by
|
|
comma-separating it).
|
|
|
|
For example:
|
|
./build/node --network-id=%s --track-subnets=%s
|
|
|
|
If you start the node via a JSON config file, add this to your config file:
|
|
track-subnets: %s
|
|
|
|
NOTE: The flag --track-subnets is a replacement of the deprecated --whitelisted-subnets.
|
|
If the later is present in config, please rename it to track-subnets first.
|
|
|
|
TIP: Try this command with the --node-config flag pointing to your config file,
|
|
this tool will try to update the file automatically (make sure it can write to it).
|
|
|
|
After you update your config, you will need to restart your node for the changes to
|
|
take effect.`
|
|
|
|
ux.Logger.PrintToUser(msg, vmPath, subnetID, networkID, subnetID, subnetID)
|
|
}
|
|
|
|
func getAssetBalance(ctx context.Context, pClient *platformvm.Client, addr string, assetID ids.ID) (uint64, error) {
|
|
pID, err := address.ParseToID(addr)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, constants.RequestTimeout)
|
|
resp, err := pClient.GetBalance(ctx, []ids.ShortID{pID})
|
|
cancel()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
assetIDBalance := resp.Balances[assetID]
|
|
return uint64(assetIDBalance), nil
|
|
}
|