github.com/ava-labs/avalanchego@v1.11.11/vms/platformvm/client.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package platformvm
     5  
     6  import (
     7  	"context"
     8  	"time"
     9  
    10  	"github.com/ava-labs/avalanchego/api"
    11  	"github.com/ava-labs/avalanchego/ids"
    12  	"github.com/ava-labs/avalanchego/snow/validators"
    13  	"github.com/ava-labs/avalanchego/utils/constants"
    14  	"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
    15  	"github.com/ava-labs/avalanchego/utils/formatting"
    16  	"github.com/ava-labs/avalanchego/utils/formatting/address"
    17  	"github.com/ava-labs/avalanchego/utils/json"
    18  	"github.com/ava-labs/avalanchego/utils/rpc"
    19  	"github.com/ava-labs/avalanchego/vms/components/gas"
    20  	"github.com/ava-labs/avalanchego/vms/platformvm/fx"
    21  	"github.com/ava-labs/avalanchego/vms/platformvm/status"
    22  	"github.com/ava-labs/avalanchego/vms/secp256k1fx"
    23  )
    24  
    25  var _ Client = (*client)(nil)
    26  
    27  // Client interface for interacting with the P Chain endpoint
    28  type Client interface {
    29  	// GetHeight returns the current block height of the P Chain
    30  	GetHeight(ctx context.Context, options ...rpc.Option) (uint64, error)
    31  	// ExportKey returns the private key corresponding to [address] from [user]'s account
    32  	//
    33  	// Deprecated: Keys should no longer be stored on the node.
    34  	ExportKey(ctx context.Context, user api.UserPass, address ids.ShortID, options ...rpc.Option) (*secp256k1.PrivateKey, error)
    35  	// GetBalance returns the balance of [addrs] on the P Chain
    36  	//
    37  	// Deprecated: GetUTXOs should be used instead.
    38  	GetBalance(ctx context.Context, addrs []ids.ShortID, options ...rpc.Option) (*GetBalanceResponse, error)
    39  	// ListAddresses returns an array of platform addresses controlled by [user]
    40  	//
    41  	// Deprecated: Keys should no longer be stored on the node.
    42  	ListAddresses(ctx context.Context, user api.UserPass, options ...rpc.Option) ([]ids.ShortID, error)
    43  	// GetUTXOs returns the byte representation of the UTXOs controlled by [addrs]
    44  	GetUTXOs(
    45  		ctx context.Context,
    46  		addrs []ids.ShortID,
    47  		limit uint32,
    48  		startAddress ids.ShortID,
    49  		startUTXOID ids.ID,
    50  		options ...rpc.Option,
    51  	) ([][]byte, ids.ShortID, ids.ID, error)
    52  	// GetAtomicUTXOs returns the byte representation of the atomic UTXOs controlled by [addrs]
    53  	// from [sourceChain]
    54  	GetAtomicUTXOs(
    55  		ctx context.Context,
    56  		addrs []ids.ShortID,
    57  		sourceChain string,
    58  		limit uint32,
    59  		startAddress ids.ShortID,
    60  		startUTXOID ids.ID,
    61  		options ...rpc.Option,
    62  	) ([][]byte, ids.ShortID, ids.ID, error)
    63  	// GetSubnet returns information about the specified subnet
    64  	GetSubnet(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (GetSubnetClientResponse, error)
    65  	// GetSubnets returns information about the specified subnets
    66  	//
    67  	// Deprecated: Subnets should be fetched from a dedicated indexer.
    68  	GetSubnets(ctx context.Context, subnetIDs []ids.ID, options ...rpc.Option) ([]ClientSubnet, error)
    69  	// GetStakingAssetID returns the assetID of the asset used for staking on
    70  	// subnet corresponding to [subnetID]
    71  	GetStakingAssetID(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (ids.ID, error)
    72  	// GetCurrentValidators returns the list of current validators for subnet with ID [subnetID]
    73  	GetCurrentValidators(ctx context.Context, subnetID ids.ID, nodeIDs []ids.NodeID, options ...rpc.Option) ([]ClientPermissionlessValidator, error)
    74  	// GetCurrentSupply returns an upper bound on the supply of AVAX in the system along with the P-chain height
    75  	GetCurrentSupply(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (uint64, uint64, error)
    76  	// SampleValidators returns the nodeIDs of a sample of [sampleSize] validators from the current validator set for subnet with ID [subnetID]
    77  	SampleValidators(ctx context.Context, subnetID ids.ID, sampleSize uint16, options ...rpc.Option) ([]ids.NodeID, error)
    78  	// GetBlockchainStatus returns the current status of blockchain with ID: [blockchainID]
    79  	GetBlockchainStatus(ctx context.Context, blockchainID string, options ...rpc.Option) (status.BlockchainStatus, error)
    80  	// ValidatedBy returns the ID of the Subnet that validates [blockchainID]
    81  	ValidatedBy(ctx context.Context, blockchainID ids.ID, options ...rpc.Option) (ids.ID, error)
    82  	// Validates returns the list of blockchains that are validated by the subnet with ID [subnetID]
    83  	Validates(ctx context.Context, subnetID ids.ID, options ...rpc.Option) ([]ids.ID, error)
    84  	// GetBlockchains returns the list of blockchains on the platform
    85  	//
    86  	// Deprecated: Blockchains should be fetched from a dedicated indexer.
    87  	GetBlockchains(ctx context.Context, options ...rpc.Option) ([]APIBlockchain, error)
    88  	// IssueTx issues the transaction and returns its txID
    89  	IssueTx(ctx context.Context, tx []byte, options ...rpc.Option) (ids.ID, error)
    90  	// GetTx returns the byte representation of the transaction corresponding to [txID]
    91  	GetTx(ctx context.Context, txID ids.ID, options ...rpc.Option) ([]byte, error)
    92  	// GetTxStatus returns the status of the transaction corresponding to [txID]
    93  	GetTxStatus(ctx context.Context, txID ids.ID, options ...rpc.Option) (*GetTxStatusResponse, error)
    94  	// GetStake returns the amount of nAVAX that [addrs] have cumulatively
    95  	// staked on the Primary Network.
    96  	//
    97  	// Deprecated: Stake should be calculated using GetTx and GetCurrentValidators.
    98  	GetStake(
    99  		ctx context.Context,
   100  		addrs []ids.ShortID,
   101  		validatorsOnly bool,
   102  		options ...rpc.Option,
   103  	) (map[ids.ID]uint64, [][]byte, error)
   104  	// GetMinStake returns the minimum staking amount in nAVAX for validators
   105  	// and delegators respectively
   106  	GetMinStake(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (uint64, uint64, error)
   107  	// GetTotalStake returns the total amount (in nAVAX) staked on the network
   108  	GetTotalStake(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (uint64, error)
   109  	// GetRewardUTXOs returns the reward UTXOs for a transaction
   110  	//
   111  	// Deprecated: GetRewardUTXOs should be fetched from a dedicated indexer.
   112  	GetRewardUTXOs(context.Context, *api.GetTxArgs, ...rpc.Option) ([][]byte, error)
   113  	// GetTimestamp returns the current chain timestamp
   114  	GetTimestamp(ctx context.Context, options ...rpc.Option) (time.Time, error)
   115  	// GetValidatorsAt returns the weights of the validator set of a provided
   116  	// subnet at the specified height.
   117  	GetValidatorsAt(
   118  		ctx context.Context,
   119  		subnetID ids.ID,
   120  		height uint64,
   121  		options ...rpc.Option,
   122  	) (map[ids.NodeID]*validators.GetValidatorOutput, error)
   123  	// GetBlock returns the block with the given id.
   124  	GetBlock(ctx context.Context, blockID ids.ID, options ...rpc.Option) ([]byte, error)
   125  	// GetBlockByHeight returns the block at the given [height].
   126  	GetBlockByHeight(ctx context.Context, height uint64, options ...rpc.Option) ([]byte, error)
   127  	// GetFeeConfig returns the dynamic fee config of the chain.
   128  	GetFeeConfig(ctx context.Context, options ...rpc.Option) (*gas.Config, error)
   129  	// GetFeeState returns the current fee state of the chain.
   130  	GetFeeState(ctx context.Context, options ...rpc.Option) (gas.State, gas.Price, time.Time, error)
   131  }
   132  
   133  // Client implementation for interacting with the P Chain endpoint
   134  type client struct {
   135  	requester rpc.EndpointRequester
   136  }
   137  
   138  // NewClient returns a Client for interacting with the P Chain endpoint
   139  func NewClient(uri string) Client {
   140  	return &client{requester: rpc.NewEndpointRequester(
   141  		uri + "/ext/P",
   142  	)}
   143  }
   144  
   145  func (c *client) GetHeight(ctx context.Context, options ...rpc.Option) (uint64, error) {
   146  	res := &api.GetHeightResponse{}
   147  	err := c.requester.SendRequest(ctx, "platform.getHeight", struct{}{}, res, options...)
   148  	return uint64(res.Height), err
   149  }
   150  
   151  func (c *client) ExportKey(ctx context.Context, user api.UserPass, address ids.ShortID, options ...rpc.Option) (*secp256k1.PrivateKey, error) {
   152  	res := &ExportKeyReply{}
   153  	err := c.requester.SendRequest(ctx, "platform.exportKey", &ExportKeyArgs{
   154  		UserPass: user,
   155  		Address:  address.String(),
   156  	}, res, options...)
   157  	return res.PrivateKey, err
   158  }
   159  
   160  func (c *client) GetBalance(ctx context.Context, addrs []ids.ShortID, options ...rpc.Option) (*GetBalanceResponse, error) {
   161  	res := &GetBalanceResponse{}
   162  	err := c.requester.SendRequest(ctx, "platform.getBalance", &GetBalanceRequest{
   163  		Addresses: ids.ShortIDsToStrings(addrs),
   164  	}, res, options...)
   165  	return res, err
   166  }
   167  
   168  func (c *client) ListAddresses(ctx context.Context, user api.UserPass, options ...rpc.Option) ([]ids.ShortID, error) {
   169  	res := &api.JSONAddresses{}
   170  	err := c.requester.SendRequest(ctx, "platform.listAddresses", &user, res, options...)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	return address.ParseToIDs(res.Addresses)
   175  }
   176  
   177  func (c *client) GetUTXOs(
   178  	ctx context.Context,
   179  	addrs []ids.ShortID,
   180  	limit uint32,
   181  	startAddress ids.ShortID,
   182  	startUTXOID ids.ID,
   183  	options ...rpc.Option,
   184  ) ([][]byte, ids.ShortID, ids.ID, error) {
   185  	return c.GetAtomicUTXOs(ctx, addrs, "", limit, startAddress, startUTXOID, options...)
   186  }
   187  
   188  func (c *client) GetAtomicUTXOs(
   189  	ctx context.Context,
   190  	addrs []ids.ShortID,
   191  	sourceChain string,
   192  	limit uint32,
   193  	startAddress ids.ShortID,
   194  	startUTXOID ids.ID,
   195  	options ...rpc.Option,
   196  ) ([][]byte, ids.ShortID, ids.ID, error) {
   197  	res := &api.GetUTXOsReply{}
   198  	err := c.requester.SendRequest(ctx, "platform.getUTXOs", &api.GetUTXOsArgs{
   199  		Addresses:   ids.ShortIDsToStrings(addrs),
   200  		SourceChain: sourceChain,
   201  		Limit:       json.Uint32(limit),
   202  		StartIndex: api.Index{
   203  			Address: startAddress.String(),
   204  			UTXO:    startUTXOID.String(),
   205  		},
   206  		Encoding: formatting.Hex,
   207  	}, res, options...)
   208  	if err != nil {
   209  		return nil, ids.ShortID{}, ids.Empty, err
   210  	}
   211  
   212  	utxos := make([][]byte, len(res.UTXOs))
   213  	for i, utxo := range res.UTXOs {
   214  		utxoBytes, err := formatting.Decode(res.Encoding, utxo)
   215  		if err != nil {
   216  			return nil, ids.ShortID{}, ids.Empty, err
   217  		}
   218  		utxos[i] = utxoBytes
   219  	}
   220  	endAddr, err := address.ParseToID(res.EndIndex.Address)
   221  	if err != nil {
   222  		return nil, ids.ShortID{}, ids.Empty, err
   223  	}
   224  	endUTXOID, err := ids.FromString(res.EndIndex.UTXO)
   225  	return utxos, endAddr, endUTXOID, err
   226  }
   227  
   228  // GetSubnetClientResponse is the response from calling GetSubnet on the client
   229  type GetSubnetClientResponse struct {
   230  	// whether it is permissioned or not
   231  	IsPermissioned bool
   232  	// subnet auth information for a permissioned subnet
   233  	ControlKeys []ids.ShortID
   234  	Threshold   uint32
   235  	Locktime    uint64
   236  	// subnet transformation tx ID for a permissionless subnet
   237  	SubnetTransformationTxID ids.ID
   238  }
   239  
   240  func (c *client) GetSubnet(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (GetSubnetClientResponse, error) {
   241  	res := &GetSubnetResponse{}
   242  	err := c.requester.SendRequest(ctx, "platform.getSubnet", &GetSubnetArgs{
   243  		SubnetID: subnetID,
   244  	}, res, options...)
   245  	if err != nil {
   246  		return GetSubnetClientResponse{}, err
   247  	}
   248  	controlKeys, err := address.ParseToIDs(res.ControlKeys)
   249  	if err != nil {
   250  		return GetSubnetClientResponse{}, err
   251  	}
   252  
   253  	return GetSubnetClientResponse{
   254  		IsPermissioned:           res.IsPermissioned,
   255  		ControlKeys:              controlKeys,
   256  		Threshold:                uint32(res.Threshold),
   257  		Locktime:                 uint64(res.Locktime),
   258  		SubnetTransformationTxID: res.SubnetTransformationTxID,
   259  	}, nil
   260  }
   261  
   262  // ClientSubnet is a representation of a subnet used in client methods
   263  type ClientSubnet struct {
   264  	// ID of the subnet
   265  	ID ids.ID
   266  	// Each element of [ControlKeys] the address of a public key.
   267  	// A transaction to add a validator to this subnet requires
   268  	// signatures from [Threshold] of these keys to be valid.
   269  	ControlKeys []ids.ShortID
   270  	Threshold   uint32
   271  }
   272  
   273  func (c *client) GetSubnets(ctx context.Context, ids []ids.ID, options ...rpc.Option) ([]ClientSubnet, error) {
   274  	res := &GetSubnetsResponse{}
   275  	err := c.requester.SendRequest(ctx, "platform.getSubnets", &GetSubnetsArgs{
   276  		IDs: ids,
   277  	}, res, options...)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	subnets := make([]ClientSubnet, len(res.Subnets))
   282  	for i, apiSubnet := range res.Subnets {
   283  		controlKeys, err := address.ParseToIDs(apiSubnet.ControlKeys)
   284  		if err != nil {
   285  			return nil, err
   286  		}
   287  
   288  		subnets[i] = ClientSubnet{
   289  			ID:          apiSubnet.ID,
   290  			ControlKeys: controlKeys,
   291  			Threshold:   uint32(apiSubnet.Threshold),
   292  		}
   293  	}
   294  	return subnets, nil
   295  }
   296  
   297  func (c *client) GetStakingAssetID(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (ids.ID, error) {
   298  	res := &GetStakingAssetIDResponse{}
   299  	err := c.requester.SendRequest(ctx, "platform.getStakingAssetID", &GetStakingAssetIDArgs{
   300  		SubnetID: subnetID,
   301  	}, res, options...)
   302  	return res.AssetID, err
   303  }
   304  
   305  func (c *client) GetCurrentValidators(
   306  	ctx context.Context,
   307  	subnetID ids.ID,
   308  	nodeIDs []ids.NodeID,
   309  	options ...rpc.Option,
   310  ) ([]ClientPermissionlessValidator, error) {
   311  	res := &GetCurrentValidatorsReply{}
   312  	err := c.requester.SendRequest(ctx, "platform.getCurrentValidators", &GetCurrentValidatorsArgs{
   313  		SubnetID: subnetID,
   314  		NodeIDs:  nodeIDs,
   315  	}, res, options...)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  	return getClientPermissionlessValidators(res.Validators)
   320  }
   321  
   322  func (c *client) GetCurrentSupply(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (uint64, uint64, error) {
   323  	res := &GetCurrentSupplyReply{}
   324  	err := c.requester.SendRequest(ctx, "platform.getCurrentSupply", &GetCurrentSupplyArgs{
   325  		SubnetID: subnetID,
   326  	}, res, options...)
   327  	return uint64(res.Supply), uint64(res.Height), err
   328  }
   329  
   330  func (c *client) SampleValidators(ctx context.Context, subnetID ids.ID, sampleSize uint16, options ...rpc.Option) ([]ids.NodeID, error) {
   331  	res := &SampleValidatorsReply{}
   332  	err := c.requester.SendRequest(ctx, "platform.sampleValidators", &SampleValidatorsArgs{
   333  		SubnetID: subnetID,
   334  		Size:     json.Uint16(sampleSize),
   335  	}, res, options...)
   336  	return res.Validators, err
   337  }
   338  
   339  func (c *client) GetBlockchainStatus(ctx context.Context, blockchainID string, options ...rpc.Option) (status.BlockchainStatus, error) {
   340  	res := &GetBlockchainStatusReply{}
   341  	err := c.requester.SendRequest(ctx, "platform.getBlockchainStatus", &GetBlockchainStatusArgs{
   342  		BlockchainID: blockchainID,
   343  	}, res, options...)
   344  	return res.Status, err
   345  }
   346  
   347  func (c *client) ValidatedBy(ctx context.Context, blockchainID ids.ID, options ...rpc.Option) (ids.ID, error) {
   348  	res := &ValidatedByResponse{}
   349  	err := c.requester.SendRequest(ctx, "platform.validatedBy", &ValidatedByArgs{
   350  		BlockchainID: blockchainID,
   351  	}, res, options...)
   352  	return res.SubnetID, err
   353  }
   354  
   355  func (c *client) Validates(ctx context.Context, subnetID ids.ID, options ...rpc.Option) ([]ids.ID, error) {
   356  	res := &ValidatesResponse{}
   357  	err := c.requester.SendRequest(ctx, "platform.validates", &ValidatesArgs{
   358  		SubnetID: subnetID,
   359  	}, res, options...)
   360  	return res.BlockchainIDs, err
   361  }
   362  
   363  func (c *client) GetBlockchains(ctx context.Context, options ...rpc.Option) ([]APIBlockchain, error) {
   364  	res := &GetBlockchainsResponse{}
   365  	err := c.requester.SendRequest(ctx, "platform.getBlockchains", struct{}{}, res, options...)
   366  	return res.Blockchains, err
   367  }
   368  
   369  func (c *client) IssueTx(ctx context.Context, txBytes []byte, options ...rpc.Option) (ids.ID, error) {
   370  	txStr, err := formatting.Encode(formatting.Hex, txBytes)
   371  	if err != nil {
   372  		return ids.Empty, err
   373  	}
   374  
   375  	res := &api.JSONTxID{}
   376  	err = c.requester.SendRequest(ctx, "platform.issueTx", &api.FormattedTx{
   377  		Tx:       txStr,
   378  		Encoding: formatting.Hex,
   379  	}, res, options...)
   380  	return res.TxID, err
   381  }
   382  
   383  func (c *client) GetTx(ctx context.Context, txID ids.ID, options ...rpc.Option) ([]byte, error) {
   384  	res := &api.FormattedTx{}
   385  	err := c.requester.SendRequest(ctx, "platform.getTx", &api.GetTxArgs{
   386  		TxID:     txID,
   387  		Encoding: formatting.Hex,
   388  	}, res, options...)
   389  	if err != nil {
   390  		return nil, err
   391  	}
   392  	return formatting.Decode(res.Encoding, res.Tx)
   393  }
   394  
   395  func (c *client) GetTxStatus(ctx context.Context, txID ids.ID, options ...rpc.Option) (*GetTxStatusResponse, error) {
   396  	res := &GetTxStatusResponse{}
   397  	err := c.requester.SendRequest(
   398  		ctx,
   399  		"platform.getTxStatus",
   400  		&GetTxStatusArgs{
   401  			TxID: txID,
   402  		},
   403  		res,
   404  		options...,
   405  	)
   406  	return res, err
   407  }
   408  
   409  func (c *client) GetStake(
   410  	ctx context.Context,
   411  	addrs []ids.ShortID,
   412  	validatorsOnly bool,
   413  	options ...rpc.Option,
   414  ) (map[ids.ID]uint64, [][]byte, error) {
   415  	res := &GetStakeReply{}
   416  	err := c.requester.SendRequest(ctx, "platform.getStake", &GetStakeArgs{
   417  		JSONAddresses: api.JSONAddresses{
   418  			Addresses: ids.ShortIDsToStrings(addrs),
   419  		},
   420  		ValidatorsOnly: validatorsOnly,
   421  		Encoding:       formatting.Hex,
   422  	}, res, options...)
   423  	if err != nil {
   424  		return nil, nil, err
   425  	}
   426  
   427  	staked := make(map[ids.ID]uint64, len(res.Stakeds))
   428  	for assetID, amount := range res.Stakeds {
   429  		staked[assetID] = uint64(amount)
   430  	}
   431  
   432  	outputs := make([][]byte, len(res.Outputs))
   433  	for i, outputStr := range res.Outputs {
   434  		output, err := formatting.Decode(res.Encoding, outputStr)
   435  		if err != nil {
   436  			return nil, nil, err
   437  		}
   438  		outputs[i] = output
   439  	}
   440  	return staked, outputs, err
   441  }
   442  
   443  func (c *client) GetMinStake(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (uint64, uint64, error) {
   444  	res := &GetMinStakeReply{}
   445  	err := c.requester.SendRequest(ctx, "platform.getMinStake", &GetMinStakeArgs{
   446  		SubnetID: subnetID,
   447  	}, res, options...)
   448  	return uint64(res.MinValidatorStake), uint64(res.MinDelegatorStake), err
   449  }
   450  
   451  func (c *client) GetTotalStake(ctx context.Context, subnetID ids.ID, options ...rpc.Option) (uint64, error) {
   452  	res := &GetTotalStakeReply{}
   453  	err := c.requester.SendRequest(ctx, "platform.getTotalStake", &GetTotalStakeArgs{
   454  		SubnetID: subnetID,
   455  	}, res, options...)
   456  	var amount json.Uint64
   457  	if subnetID == constants.PrimaryNetworkID {
   458  		amount = res.Stake
   459  	} else {
   460  		amount = res.Weight
   461  	}
   462  	return uint64(amount), err
   463  }
   464  
   465  func (c *client) GetRewardUTXOs(ctx context.Context, args *api.GetTxArgs, options ...rpc.Option) ([][]byte, error) {
   466  	res := &GetRewardUTXOsReply{}
   467  	err := c.requester.SendRequest(ctx, "platform.getRewardUTXOs", args, res, options...)
   468  	if err != nil {
   469  		return nil, err
   470  	}
   471  	utxos := make([][]byte, len(res.UTXOs))
   472  	for i, utxoStr := range res.UTXOs {
   473  		utxoBytes, err := formatting.Decode(res.Encoding, utxoStr)
   474  		if err != nil {
   475  			return nil, err
   476  		}
   477  		utxos[i] = utxoBytes
   478  	}
   479  	return utxos, err
   480  }
   481  
   482  func (c *client) GetTimestamp(ctx context.Context, options ...rpc.Option) (time.Time, error) {
   483  	res := &GetTimestampReply{}
   484  	err := c.requester.SendRequest(ctx, "platform.getTimestamp", struct{}{}, res, options...)
   485  	return res.Timestamp, err
   486  }
   487  
   488  func (c *client) GetValidatorsAt(
   489  	ctx context.Context,
   490  	subnetID ids.ID,
   491  	height uint64,
   492  	options ...rpc.Option,
   493  ) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
   494  	res := &GetValidatorsAtReply{}
   495  	err := c.requester.SendRequest(ctx, "platform.getValidatorsAt", &GetValidatorsAtArgs{
   496  		SubnetID: subnetID,
   497  		Height:   json.Uint64(height),
   498  	}, res, options...)
   499  	return res.Validators, err
   500  }
   501  
   502  func (c *client) GetBlock(ctx context.Context, blockID ids.ID, options ...rpc.Option) ([]byte, error) {
   503  	res := &api.FormattedBlock{}
   504  	if err := c.requester.SendRequest(ctx, "platform.getBlock", &api.GetBlockArgs{
   505  		BlockID:  blockID,
   506  		Encoding: formatting.Hex,
   507  	}, res, options...); err != nil {
   508  		return nil, err
   509  	}
   510  	return formatting.Decode(res.Encoding, res.Block)
   511  }
   512  
   513  func (c *client) GetBlockByHeight(ctx context.Context, height uint64, options ...rpc.Option) ([]byte, error) {
   514  	res := &api.FormattedBlock{}
   515  	err := c.requester.SendRequest(ctx, "platform.getBlockByHeight", &api.GetBlockByHeightArgs{
   516  		Height:   json.Uint64(height),
   517  		Encoding: formatting.HexNC,
   518  	}, res, options...)
   519  	if err != nil {
   520  		return nil, err
   521  	}
   522  	return formatting.Decode(res.Encoding, res.Block)
   523  }
   524  
   525  func (c *client) GetFeeConfig(ctx context.Context, options ...rpc.Option) (*gas.Config, error) {
   526  	res := &gas.Config{}
   527  	err := c.requester.SendRequest(ctx, "platform.getFeeConfig", struct{}{}, res, options...)
   528  	return res, err
   529  }
   530  
   531  func (c *client) GetFeeState(ctx context.Context, options ...rpc.Option) (gas.State, gas.Price, time.Time, error) {
   532  	res := &GetFeeStateReply{}
   533  	err := c.requester.SendRequest(ctx, "platform.getFeeState", struct{}{}, res, options...)
   534  	return res.State, res.Price, res.Time, err
   535  }
   536  
   537  func AwaitTxAccepted(
   538  	c Client,
   539  	ctx context.Context,
   540  	txID ids.ID,
   541  	freq time.Duration,
   542  	options ...rpc.Option,
   543  ) error {
   544  	ticker := time.NewTicker(freq)
   545  	defer ticker.Stop()
   546  
   547  	for {
   548  		res, err := c.GetTxStatus(ctx, txID, options...)
   549  		if err != nil {
   550  			return err
   551  		}
   552  
   553  		switch res.Status {
   554  		case status.Committed, status.Aborted:
   555  			return nil
   556  		}
   557  
   558  		select {
   559  		case <-ticker.C:
   560  		case <-ctx.Done():
   561  			return ctx.Err()
   562  		}
   563  	}
   564  }
   565  
   566  // GetSubnetOwners returns a map of subnet ID to current subnet's owner
   567  func GetSubnetOwners(
   568  	c Client,
   569  	ctx context.Context,
   570  	subnetIDs ...ids.ID,
   571  ) (map[ids.ID]fx.Owner, error) {
   572  	subnetOwners := map[ids.ID]fx.Owner{}
   573  	for _, subnetID := range subnetIDs {
   574  		subnetInfo, err := c.GetSubnet(ctx, subnetID)
   575  		if err != nil {
   576  			return nil, err
   577  		}
   578  		subnetOwners[subnetID] = &secp256k1fx.OutputOwners{
   579  			Locktime:  subnetInfo.Locktime,
   580  			Threshold: subnetInfo.Threshold,
   581  			Addrs:     subnetInfo.ControlKeys,
   582  		}
   583  	}
   584  	return subnetOwners, nil
   585  }