github.com/MetalBlockchain/metalgo@v1.11.9/vms/avm/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 avm
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"time"
    11  
    12  	"github.com/MetalBlockchain/metalgo/api"
    13  	"github.com/MetalBlockchain/metalgo/ids"
    14  	"github.com/MetalBlockchain/metalgo/snow/choices"
    15  	"github.com/MetalBlockchain/metalgo/utils/constants"
    16  	"github.com/MetalBlockchain/metalgo/utils/crypto/secp256k1"
    17  	"github.com/MetalBlockchain/metalgo/utils/formatting"
    18  	"github.com/MetalBlockchain/metalgo/utils/formatting/address"
    19  	"github.com/MetalBlockchain/metalgo/utils/json"
    20  	"github.com/MetalBlockchain/metalgo/utils/rpc"
    21  )
    22  
    23  var (
    24  	_ Client = (*client)(nil)
    25  
    26  	ErrRejected = errors.New("rejected")
    27  )
    28  
    29  // Client for interacting with an AVM (X-Chain) instance
    30  type Client interface {
    31  	WalletClient
    32  	// GetBlock returns the block with the given id.
    33  	GetBlock(ctx context.Context, blkID ids.ID, options ...rpc.Option) ([]byte, error)
    34  	// GetBlockByHeight returns the block at the given [height].
    35  	GetBlockByHeight(ctx context.Context, height uint64, options ...rpc.Option) ([]byte, error)
    36  	// GetHeight returns the height of the last accepted block.
    37  	GetHeight(ctx context.Context, options ...rpc.Option) (uint64, error)
    38  	// GetTxStatus returns the status of [txID]
    39  	//
    40  	// Deprecated: GetTxStatus only returns Accepted or Unknown, GetTx should be
    41  	// used instead to determine if the tx was accepted.
    42  	GetTxStatus(ctx context.Context, txID ids.ID, options ...rpc.Option) (choices.Status, error)
    43  	// GetTx returns the byte representation of [txID]
    44  	GetTx(ctx context.Context, txID ids.ID, options ...rpc.Option) ([]byte, error)
    45  	// GetUTXOs returns the byte representation of the UTXOs controlled by [addrs]
    46  	GetUTXOs(
    47  		ctx context.Context,
    48  		addrs []ids.ShortID,
    49  		limit uint32,
    50  		startAddress ids.ShortID,
    51  		startUTXOID ids.ID,
    52  		options ...rpc.Option,
    53  	) ([][]byte, ids.ShortID, ids.ID, error)
    54  	// GetAtomicUTXOs returns the byte representation of the atomic UTXOs controlled by [addrs]
    55  	// from [sourceChain]
    56  	GetAtomicUTXOs(
    57  		ctx context.Context,
    58  		addrs []ids.ShortID,
    59  		sourceChain string,
    60  		limit uint32,
    61  		startAddress ids.ShortID,
    62  		startUTXOID ids.ID,
    63  		options ...rpc.Option,
    64  	) ([][]byte, ids.ShortID, ids.ID, error)
    65  	// GetAssetDescription returns a description of [assetID]
    66  	GetAssetDescription(ctx context.Context, assetID string, options ...rpc.Option) (*GetAssetDescriptionReply, error)
    67  	// GetBalance returns the balance of [assetID] held by [addr].
    68  	// If [includePartial], balance includes partial owned (i.e. in a multisig) funds.
    69  	//
    70  	// Deprecated: GetUTXOs should be used instead.
    71  	GetBalance(ctx context.Context, addr ids.ShortID, assetID string, includePartial bool, options ...rpc.Option) (*GetBalanceReply, error)
    72  	// GetAllBalances returns all asset balances for [addr]
    73  	//
    74  	// Deprecated: GetUTXOs should be used instead.
    75  	GetAllBalances(ctx context.Context, addr ids.ShortID, includePartial bool, options ...rpc.Option) ([]Balance, error)
    76  	// CreateAsset creates a new asset and returns its assetID
    77  	//
    78  	// Deprecated: Transactions should be issued using the
    79  	// `avalanchego/wallet/chain/x.Wallet` utility.
    80  	CreateAsset(
    81  		ctx context.Context,
    82  		user api.UserPass,
    83  		from []ids.ShortID,
    84  		changeAddr ids.ShortID,
    85  		name string,
    86  		symbol string,
    87  		denomination byte,
    88  		holders []*ClientHolder,
    89  		minters []ClientOwners,
    90  		options ...rpc.Option,
    91  	) (ids.ID, error)
    92  	// CreateFixedCapAsset creates a new fixed cap asset and returns its assetID
    93  	//
    94  	// Deprecated: Transactions should be issued using the
    95  	// `avalanchego/wallet/chain/x.Wallet` utility.
    96  	CreateFixedCapAsset(
    97  		ctx context.Context,
    98  		user api.UserPass,
    99  		from []ids.ShortID,
   100  		changeAddr ids.ShortID,
   101  		name string,
   102  		symbol string,
   103  		denomination byte,
   104  		holders []*ClientHolder,
   105  		options ...rpc.Option,
   106  	) (ids.ID, error)
   107  	// CreateVariableCapAsset creates a new variable cap asset and returns its assetID
   108  	//
   109  	// Deprecated: Transactions should be issued using the
   110  	// `avalanchego/wallet/chain/x.Wallet` utility.
   111  	CreateVariableCapAsset(
   112  		ctx context.Context,
   113  		user api.UserPass,
   114  		from []ids.ShortID,
   115  		changeAddr ids.ShortID,
   116  		name string,
   117  		symbol string,
   118  		denomination byte,
   119  		minters []ClientOwners,
   120  		options ...rpc.Option,
   121  	) (ids.ID, error)
   122  	// CreateNFTAsset creates a new NFT asset and returns its assetID
   123  	//
   124  	// Deprecated: Transactions should be issued using the
   125  	// `avalanchego/wallet/chain/x.Wallet` utility.
   126  	CreateNFTAsset(
   127  		ctx context.Context,
   128  		user api.UserPass,
   129  		from []ids.ShortID,
   130  		changeAddr ids.ShortID,
   131  		name string,
   132  		symbol string,
   133  		minters []ClientOwners,
   134  		options ...rpc.Option,
   135  	) (ids.ID, error)
   136  	// CreateAddress creates a new address controlled by [user]
   137  	//
   138  	// Deprecated: Keys should no longer be stored on the node.
   139  	CreateAddress(ctx context.Context, user api.UserPass, options ...rpc.Option) (ids.ShortID, error)
   140  	// ListAddresses returns all addresses on this chain controlled by [user]
   141  	//
   142  	// Deprecated: Keys should no longer be stored on the node.
   143  	ListAddresses(ctx context.Context, user api.UserPass, options ...rpc.Option) ([]ids.ShortID, error)
   144  	// ExportKey returns the private key corresponding to [addr] controlled by [user]
   145  	//
   146  	// Deprecated: Keys should no longer be stored on the node.
   147  	ExportKey(ctx context.Context, user api.UserPass, addr ids.ShortID, options ...rpc.Option) (*secp256k1.PrivateKey, error)
   148  	// ImportKey imports [privateKey] to [user]
   149  	//
   150  	// Deprecated: Keys should no longer be stored on the node.
   151  	ImportKey(ctx context.Context, user api.UserPass, privateKey *secp256k1.PrivateKey, options ...rpc.Option) (ids.ShortID, error)
   152  	// Mint [amount] of [assetID] to be owned by [to]
   153  	//
   154  	// Deprecated: Transactions should be issued using the
   155  	// `avalanchego/wallet/chain/x.Wallet` utility.
   156  	Mint(
   157  		ctx context.Context,
   158  		user api.UserPass,
   159  		from []ids.ShortID,
   160  		changeAddr ids.ShortID,
   161  		amount uint64,
   162  		assetID string,
   163  		to ids.ShortID,
   164  		options ...rpc.Option,
   165  	) (ids.ID, error)
   166  	// SendNFT sends an NFT and returns the ID of the newly created transaction
   167  	//
   168  	// Deprecated: Transactions should be issued using the
   169  	// `avalanchego/wallet/chain/x.Wallet` utility.
   170  	SendNFT(
   171  		ctx context.Context,
   172  		user api.UserPass,
   173  		from []ids.ShortID,
   174  		changeAddr ids.ShortID,
   175  		assetID string,
   176  		groupID uint32,
   177  		to ids.ShortID,
   178  		options ...rpc.Option,
   179  	) (ids.ID, error)
   180  	// MintNFT issues a MintNFT transaction and returns the ID of the newly created transaction
   181  	//
   182  	// Deprecated: Transactions should be issued using the
   183  	// `avalanchego/wallet/chain/x.Wallet` utility.
   184  	MintNFT(
   185  		ctx context.Context,
   186  		user api.UserPass,
   187  		from []ids.ShortID,
   188  		changeAddr ids.ShortID,
   189  		assetID string,
   190  		payload []byte,
   191  		to ids.ShortID,
   192  		options ...rpc.Option,
   193  	) (ids.ID, error)
   194  	// Import sends an import transaction to import funds from [sourceChain] and
   195  	// returns the ID of the newly created transaction
   196  	//
   197  	// Deprecated: Transactions should be issued using the
   198  	// `avalanchego/wallet/chain/x.Wallet` utility.
   199  	Import(ctx context.Context, user api.UserPass, to ids.ShortID, sourceChain string, options ...rpc.Option) (ids.ID, error) // Export sends an asset from this chain to the P/C-Chain.
   200  	// After this tx is accepted, the AVAX must be imported to the P/C-chain with an importTx.
   201  	// Returns the ID of the newly created atomic transaction
   202  	//
   203  	// Deprecated: Transactions should be issued using the
   204  	// `avalanchego/wallet/chain/x.Wallet` utility.
   205  	Export(
   206  		ctx context.Context,
   207  		user api.UserPass,
   208  		from []ids.ShortID,
   209  		changeAddr ids.ShortID,
   210  		amount uint64,
   211  		to ids.ShortID,
   212  		toChainIDAlias string,
   213  		assetID string,
   214  		options ...rpc.Option,
   215  	) (ids.ID, error)
   216  }
   217  
   218  // implementation for an AVM client for interacting with avm [chain]
   219  type client struct {
   220  	requester rpc.EndpointRequester
   221  }
   222  
   223  // NewClient returns an AVM client for interacting with avm [chain]
   224  func NewClient(uri, chain string) Client {
   225  	path := fmt.Sprintf(
   226  		"%s/ext/%s/%s",
   227  		uri,
   228  		constants.ChainAliasPrefix,
   229  		chain,
   230  	)
   231  	return &client{
   232  		requester: rpc.NewEndpointRequester(path),
   233  	}
   234  }
   235  
   236  func (c *client) GetBlock(ctx context.Context, blkID ids.ID, options ...rpc.Option) ([]byte, error) {
   237  	res := &api.FormattedBlock{}
   238  	err := c.requester.SendRequest(ctx, "avm.getBlock", &api.GetBlockArgs{
   239  		BlockID:  blkID,
   240  		Encoding: formatting.HexNC,
   241  	}, res, options...)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	return formatting.Decode(res.Encoding, res.Block)
   246  }
   247  
   248  func (c *client) GetBlockByHeight(ctx context.Context, height uint64, options ...rpc.Option) ([]byte, error) {
   249  	res := &api.FormattedBlock{}
   250  	err := c.requester.SendRequest(ctx, "avm.getBlockByHeight", &api.GetBlockByHeightArgs{
   251  		Height:   json.Uint64(height),
   252  		Encoding: formatting.HexNC,
   253  	}, res, options...)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	return formatting.Decode(res.Encoding, res.Block)
   258  }
   259  
   260  func (c *client) GetHeight(ctx context.Context, options ...rpc.Option) (uint64, error) {
   261  	res := &api.GetHeightResponse{}
   262  	err := c.requester.SendRequest(ctx, "avm.getHeight", struct{}{}, res, options...)
   263  	return uint64(res.Height), err
   264  }
   265  
   266  func (c *client) IssueTx(ctx context.Context, txBytes []byte, options ...rpc.Option) (ids.ID, error) {
   267  	txStr, err := formatting.Encode(formatting.Hex, txBytes)
   268  	if err != nil {
   269  		return ids.Empty, err
   270  	}
   271  	res := &api.JSONTxID{}
   272  	err = c.requester.SendRequest(ctx, "avm.issueTx", &api.FormattedTx{
   273  		Tx:       txStr,
   274  		Encoding: formatting.Hex,
   275  	}, res, options...)
   276  	return res.TxID, err
   277  }
   278  
   279  func (c *client) GetTxStatus(ctx context.Context, txID ids.ID, options ...rpc.Option) (choices.Status, error) {
   280  	res := &GetTxStatusReply{}
   281  	err := c.requester.SendRequest(ctx, "avm.getTxStatus", &api.JSONTxID{
   282  		TxID: txID,
   283  	}, res, options...)
   284  	return res.Status, err
   285  }
   286  
   287  func (c *client) GetTx(ctx context.Context, txID ids.ID, options ...rpc.Option) ([]byte, error) {
   288  	res := &api.FormattedTx{}
   289  	err := c.requester.SendRequest(ctx, "avm.getTx", &api.GetTxArgs{
   290  		TxID:     txID,
   291  		Encoding: formatting.Hex,
   292  	}, res, options...)
   293  	if err != nil {
   294  		return nil, err
   295  	}
   296  	return formatting.Decode(res.Encoding, res.Tx)
   297  }
   298  
   299  func (c *client) GetUTXOs(
   300  	ctx context.Context,
   301  	addrs []ids.ShortID,
   302  	limit uint32,
   303  	startAddress ids.ShortID,
   304  	startUTXOID ids.ID,
   305  	options ...rpc.Option,
   306  ) ([][]byte, ids.ShortID, ids.ID, error) {
   307  	return c.GetAtomicUTXOs(ctx, addrs, "", limit, startAddress, startUTXOID, options...)
   308  }
   309  
   310  func (c *client) GetAtomicUTXOs(
   311  	ctx context.Context,
   312  	addrs []ids.ShortID,
   313  	sourceChain string,
   314  	limit uint32,
   315  	startAddress ids.ShortID,
   316  	startUTXOID ids.ID,
   317  	options ...rpc.Option,
   318  ) ([][]byte, ids.ShortID, ids.ID, error) {
   319  	res := &api.GetUTXOsReply{}
   320  	err := c.requester.SendRequest(ctx, "avm.getUTXOs", &api.GetUTXOsArgs{
   321  		Addresses:   ids.ShortIDsToStrings(addrs),
   322  		SourceChain: sourceChain,
   323  		Limit:       json.Uint32(limit),
   324  		StartIndex: api.Index{
   325  			Address: startAddress.String(),
   326  			UTXO:    startUTXOID.String(),
   327  		},
   328  		Encoding: formatting.Hex,
   329  	}, res, options...)
   330  	if err != nil {
   331  		return nil, ids.ShortID{}, ids.Empty, err
   332  	}
   333  
   334  	utxos := make([][]byte, len(res.UTXOs))
   335  	for i, utxo := range res.UTXOs {
   336  		utxoBytes, err := formatting.Decode(res.Encoding, utxo)
   337  		if err != nil {
   338  			return nil, ids.ShortID{}, ids.Empty, err
   339  		}
   340  		utxos[i] = utxoBytes
   341  	}
   342  	endAddr, err := address.ParseToID(res.EndIndex.Address)
   343  	if err != nil {
   344  		return nil, ids.ShortID{}, ids.Empty, err
   345  	}
   346  	endUTXOID, err := ids.FromString(res.EndIndex.UTXO)
   347  	return utxos, endAddr, endUTXOID, err
   348  }
   349  
   350  func (c *client) GetAssetDescription(ctx context.Context, assetID string, options ...rpc.Option) (*GetAssetDescriptionReply, error) {
   351  	res := &GetAssetDescriptionReply{}
   352  	err := c.requester.SendRequest(ctx, "avm.getAssetDescription", &GetAssetDescriptionArgs{
   353  		AssetID: assetID,
   354  	}, res, options...)
   355  	return res, err
   356  }
   357  
   358  func (c *client) GetBalance(
   359  	ctx context.Context,
   360  	addr ids.ShortID,
   361  	assetID string,
   362  	includePartial bool,
   363  	options ...rpc.Option,
   364  ) (*GetBalanceReply, error) {
   365  	res := &GetBalanceReply{}
   366  	err := c.requester.SendRequest(ctx, "avm.getBalance", &GetBalanceArgs{
   367  		Address:        addr.String(),
   368  		AssetID:        assetID,
   369  		IncludePartial: includePartial,
   370  	}, res, options...)
   371  	return res, err
   372  }
   373  
   374  func (c *client) GetAllBalances(
   375  	ctx context.Context,
   376  	addr ids.ShortID,
   377  	includePartial bool,
   378  	options ...rpc.Option,
   379  ) ([]Balance, error) {
   380  	res := &GetAllBalancesReply{}
   381  	err := c.requester.SendRequest(ctx, "avm.getAllBalances", &GetAllBalancesArgs{
   382  		JSONAddress:    api.JSONAddress{Address: addr.String()},
   383  		IncludePartial: includePartial,
   384  	}, res, options...)
   385  	return res.Balances, err
   386  }
   387  
   388  // ClientHolder describes how much an address owns of an asset
   389  type ClientHolder struct {
   390  	Amount  uint64
   391  	Address ids.ShortID
   392  }
   393  
   394  // ClientOwners describes who can perform an action
   395  type ClientOwners struct {
   396  	Threshold uint32
   397  	Minters   []ids.ShortID
   398  }
   399  
   400  func (c *client) CreateAsset(
   401  	ctx context.Context,
   402  	user api.UserPass,
   403  	from []ids.ShortID,
   404  	changeAddr ids.ShortID,
   405  	name string,
   406  	symbol string,
   407  	denomination byte,
   408  	clientHolders []*ClientHolder,
   409  	clientMinters []ClientOwners,
   410  	options ...rpc.Option,
   411  ) (ids.ID, error) {
   412  	res := &FormattedAssetID{}
   413  	holders := make([]*Holder, len(clientHolders))
   414  	for i, clientHolder := range clientHolders {
   415  		holders[i] = &Holder{
   416  			Amount:  json.Uint64(clientHolder.Amount),
   417  			Address: clientHolder.Address.String(),
   418  		}
   419  	}
   420  	minters := make([]Owners, len(clientMinters))
   421  	for i, clientMinter := range clientMinters {
   422  		minters[i] = Owners{
   423  			Threshold: json.Uint32(clientMinter.Threshold),
   424  			Minters:   ids.ShortIDsToStrings(clientMinter.Minters),
   425  		}
   426  	}
   427  	err := c.requester.SendRequest(ctx, "avm.createAsset", &CreateAssetArgs{
   428  		JSONSpendHeader: api.JSONSpendHeader{
   429  			UserPass:       user,
   430  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   431  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   432  		},
   433  		Name:           name,
   434  		Symbol:         symbol,
   435  		Denomination:   denomination,
   436  		InitialHolders: holders,
   437  		MinterSets:     minters,
   438  	}, res, options...)
   439  	return res.AssetID, err
   440  }
   441  
   442  func (c *client) CreateFixedCapAsset(
   443  	ctx context.Context,
   444  	user api.UserPass,
   445  	from []ids.ShortID,
   446  	changeAddr ids.ShortID,
   447  	name string,
   448  	symbol string,
   449  	denomination byte,
   450  	clientHolders []*ClientHolder,
   451  	options ...rpc.Option,
   452  ) (ids.ID, error) {
   453  	res := &FormattedAssetID{}
   454  	holders := make([]*Holder, len(clientHolders))
   455  	for i, clientHolder := range clientHolders {
   456  		holders[i] = &Holder{
   457  			Amount:  json.Uint64(clientHolder.Amount),
   458  			Address: clientHolder.Address.String(),
   459  		}
   460  	}
   461  	err := c.requester.SendRequest(ctx, "avm.createAsset", &CreateAssetArgs{
   462  		JSONSpendHeader: api.JSONSpendHeader{
   463  			UserPass:       user,
   464  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   465  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   466  		},
   467  		Name:           name,
   468  		Symbol:         symbol,
   469  		Denomination:   denomination,
   470  		InitialHolders: holders,
   471  	}, res, options...)
   472  	return res.AssetID, err
   473  }
   474  
   475  func (c *client) CreateVariableCapAsset(
   476  	ctx context.Context,
   477  	user api.UserPass,
   478  	from []ids.ShortID,
   479  	changeAddr ids.ShortID,
   480  	name string,
   481  	symbol string,
   482  	denomination byte,
   483  	clientMinters []ClientOwners,
   484  	options ...rpc.Option,
   485  ) (ids.ID, error) {
   486  	res := &FormattedAssetID{}
   487  	minters := make([]Owners, len(clientMinters))
   488  	for i, clientMinter := range clientMinters {
   489  		minters[i] = Owners{
   490  			Threshold: json.Uint32(clientMinter.Threshold),
   491  			Minters:   ids.ShortIDsToStrings(clientMinter.Minters),
   492  		}
   493  	}
   494  	err := c.requester.SendRequest(ctx, "avm.createAsset", &CreateAssetArgs{
   495  		JSONSpendHeader: api.JSONSpendHeader{
   496  			UserPass:       user,
   497  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   498  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   499  		},
   500  		Name:         name,
   501  		Symbol:       symbol,
   502  		Denomination: denomination,
   503  		MinterSets:   minters,
   504  	}, res, options...)
   505  	return res.AssetID, err
   506  }
   507  
   508  func (c *client) CreateNFTAsset(
   509  	ctx context.Context,
   510  	user api.UserPass,
   511  	from []ids.ShortID,
   512  	changeAddr ids.ShortID,
   513  	name string,
   514  	symbol string,
   515  	clientMinters []ClientOwners,
   516  	options ...rpc.Option,
   517  ) (ids.ID, error) {
   518  	res := &FormattedAssetID{}
   519  	minters := make([]Owners, len(clientMinters))
   520  	for i, clientMinter := range clientMinters {
   521  		minters[i] = Owners{
   522  			Threshold: json.Uint32(clientMinter.Threshold),
   523  			Minters:   ids.ShortIDsToStrings(clientMinter.Minters),
   524  		}
   525  	}
   526  	err := c.requester.SendRequest(ctx, "avm.createNFTAsset", &CreateNFTAssetArgs{
   527  		JSONSpendHeader: api.JSONSpendHeader{
   528  			UserPass:       user,
   529  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   530  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   531  		},
   532  		Name:       name,
   533  		Symbol:     symbol,
   534  		MinterSets: minters,
   535  	}, res, options...)
   536  	return res.AssetID, err
   537  }
   538  
   539  func (c *client) CreateAddress(ctx context.Context, user api.UserPass, options ...rpc.Option) (ids.ShortID, error) {
   540  	res := &api.JSONAddress{}
   541  	err := c.requester.SendRequest(ctx, "avm.createAddress", &user, res, options...)
   542  	if err != nil {
   543  		return ids.ShortID{}, err
   544  	}
   545  	return address.ParseToID(res.Address)
   546  }
   547  
   548  func (c *client) ListAddresses(ctx context.Context, user api.UserPass, options ...rpc.Option) ([]ids.ShortID, error) {
   549  	res := &api.JSONAddresses{}
   550  	err := c.requester.SendRequest(ctx, "avm.listAddresses", &user, res, options...)
   551  	if err != nil {
   552  		return nil, err
   553  	}
   554  	return address.ParseToIDs(res.Addresses)
   555  }
   556  
   557  func (c *client) ExportKey(ctx context.Context, user api.UserPass, addr ids.ShortID, options ...rpc.Option) (*secp256k1.PrivateKey, error) {
   558  	res := &ExportKeyReply{}
   559  	err := c.requester.SendRequest(ctx, "avm.exportKey", &ExportKeyArgs{
   560  		UserPass: user,
   561  		Address:  addr.String(),
   562  	}, res, options...)
   563  	return res.PrivateKey, err
   564  }
   565  
   566  func (c *client) ImportKey(ctx context.Context, user api.UserPass, privateKey *secp256k1.PrivateKey, options ...rpc.Option) (ids.ShortID, error) {
   567  	res := &api.JSONAddress{}
   568  	err := c.requester.SendRequest(ctx, "avm.importKey", &ImportKeyArgs{
   569  		UserPass:   user,
   570  		PrivateKey: privateKey,
   571  	}, res, options...)
   572  	if err != nil {
   573  		return ids.ShortID{}, err
   574  	}
   575  	return address.ParseToID(res.Address)
   576  }
   577  
   578  func (c *client) Send(
   579  	ctx context.Context,
   580  	user api.UserPass,
   581  	from []ids.ShortID,
   582  	changeAddr ids.ShortID,
   583  	amount uint64,
   584  	assetID string,
   585  	to ids.ShortID,
   586  	memo string,
   587  	options ...rpc.Option,
   588  ) (ids.ID, error) {
   589  	res := &api.JSONTxID{}
   590  	err := c.requester.SendRequest(ctx, "avm.send", &SendArgs{
   591  		JSONSpendHeader: api.JSONSpendHeader{
   592  			UserPass:       user,
   593  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   594  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   595  		},
   596  		SendOutput: SendOutput{
   597  			Amount:  json.Uint64(amount),
   598  			AssetID: assetID,
   599  			To:      to.String(),
   600  		},
   601  		Memo: memo,
   602  	}, res, options...)
   603  	return res.TxID, err
   604  }
   605  
   606  func (c *client) SendMultiple(
   607  	ctx context.Context,
   608  	user api.UserPass,
   609  	from []ids.ShortID,
   610  	changeAddr ids.ShortID,
   611  	clientOutputs []ClientSendOutput,
   612  	memo string,
   613  	options ...rpc.Option,
   614  ) (ids.ID, error) {
   615  	res := &api.JSONTxID{}
   616  	outputs := make([]SendOutput, len(clientOutputs))
   617  	for i, clientOutput := range clientOutputs {
   618  		outputs[i] = SendOutput{
   619  			Amount:  json.Uint64(clientOutput.Amount),
   620  			AssetID: clientOutput.AssetID,
   621  			To:      clientOutput.To.String(),
   622  		}
   623  	}
   624  	err := c.requester.SendRequest(ctx, "avm.sendMultiple", &SendMultipleArgs{
   625  		JSONSpendHeader: api.JSONSpendHeader{
   626  			UserPass:       user,
   627  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   628  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   629  		},
   630  		Outputs: outputs,
   631  		Memo:    memo,
   632  	}, res, options...)
   633  	return res.TxID, err
   634  }
   635  
   636  func (c *client) Mint(
   637  	ctx context.Context,
   638  	user api.UserPass,
   639  	from []ids.ShortID,
   640  	changeAddr ids.ShortID,
   641  	amount uint64,
   642  	assetID string,
   643  	to ids.ShortID,
   644  	options ...rpc.Option,
   645  ) (ids.ID, error) {
   646  	res := &api.JSONTxID{}
   647  	err := c.requester.SendRequest(ctx, "avm.mint", &MintArgs{
   648  		JSONSpendHeader: api.JSONSpendHeader{
   649  			UserPass:       user,
   650  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   651  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   652  		},
   653  		Amount:  json.Uint64(amount),
   654  		AssetID: assetID,
   655  		To:      to.String(),
   656  	}, res, options...)
   657  	return res.TxID, err
   658  }
   659  
   660  func (c *client) SendNFT(
   661  	ctx context.Context,
   662  	user api.UserPass,
   663  	from []ids.ShortID,
   664  	changeAddr ids.ShortID,
   665  	assetID string,
   666  	groupID uint32,
   667  	to ids.ShortID,
   668  	options ...rpc.Option,
   669  ) (ids.ID, error) {
   670  	res := &api.JSONTxID{}
   671  	err := c.requester.SendRequest(ctx, "avm.sendNFT", &SendNFTArgs{
   672  		JSONSpendHeader: api.JSONSpendHeader{
   673  			UserPass:       user,
   674  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   675  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   676  		},
   677  		AssetID: assetID,
   678  		GroupID: json.Uint32(groupID),
   679  		To:      to.String(),
   680  	}, res, options...)
   681  	return res.TxID, err
   682  }
   683  
   684  func (c *client) MintNFT(
   685  	ctx context.Context,
   686  	user api.UserPass,
   687  	from []ids.ShortID,
   688  	changeAddr ids.ShortID,
   689  	assetID string,
   690  	payload []byte,
   691  	to ids.ShortID,
   692  	options ...rpc.Option,
   693  ) (ids.ID, error) {
   694  	payloadStr, err := formatting.Encode(formatting.Hex, payload)
   695  	if err != nil {
   696  		return ids.Empty, err
   697  	}
   698  	res := &api.JSONTxID{}
   699  	err = c.requester.SendRequest(ctx, "avm.mintNFT", &MintNFTArgs{
   700  		JSONSpendHeader: api.JSONSpendHeader{
   701  			UserPass:       user,
   702  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   703  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   704  		},
   705  		AssetID:  assetID,
   706  		Payload:  payloadStr,
   707  		To:       to.String(),
   708  		Encoding: formatting.Hex,
   709  	}, res, options...)
   710  	return res.TxID, err
   711  }
   712  
   713  func (c *client) Import(ctx context.Context, user api.UserPass, to ids.ShortID, sourceChain string, options ...rpc.Option) (ids.ID, error) {
   714  	res := &api.JSONTxID{}
   715  	err := c.requester.SendRequest(ctx, "avm.import", &ImportArgs{
   716  		UserPass:    user,
   717  		To:          to.String(),
   718  		SourceChain: sourceChain,
   719  	}, res, options...)
   720  	return res.TxID, err
   721  }
   722  
   723  func (c *client) Export(
   724  	ctx context.Context,
   725  	user api.UserPass,
   726  	from []ids.ShortID,
   727  	changeAddr ids.ShortID,
   728  	amount uint64,
   729  	to ids.ShortID,
   730  	targetChain string,
   731  	assetID string,
   732  	options ...rpc.Option,
   733  ) (ids.ID, error) {
   734  	res := &api.JSONTxID{}
   735  	err := c.requester.SendRequest(ctx, "avm.export", &ExportArgs{
   736  		JSONSpendHeader: api.JSONSpendHeader{
   737  			UserPass:       user,
   738  			JSONFromAddrs:  api.JSONFromAddrs{From: ids.ShortIDsToStrings(from)},
   739  			JSONChangeAddr: api.JSONChangeAddr{ChangeAddr: changeAddr.String()},
   740  		},
   741  		Amount:      json.Uint64(amount),
   742  		TargetChain: targetChain,
   743  		To:          to.String(),
   744  		AssetID:     assetID,
   745  	}, res, options...)
   746  	return res.TxID, err
   747  }
   748  
   749  func AwaitTxAccepted(
   750  	c Client,
   751  	ctx context.Context,
   752  	txID ids.ID,
   753  	freq time.Duration,
   754  	options ...rpc.Option,
   755  ) error {
   756  	ticker := time.NewTicker(freq)
   757  	defer ticker.Stop()
   758  
   759  	for {
   760  		status, err := c.GetTxStatus(ctx, txID, options...)
   761  		if err != nil {
   762  			return err
   763  		}
   764  
   765  		switch status {
   766  		case choices.Accepted:
   767  			return nil
   768  		case choices.Rejected:
   769  			return ErrRejected
   770  		}
   771  
   772  		select {
   773  		case <-ticker.C:
   774  		case <-ctx.Done():
   775  			return ctx.Err()
   776  		}
   777  	}
   778  }