github.com/ava-labs/avalanchego@v1.11.11/wallet/chain/x/builder/builder.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package builder
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  
    11  	"github.com/ava-labs/avalanchego/ids"
    12  	"github.com/ava-labs/avalanchego/utils"
    13  	"github.com/ava-labs/avalanchego/utils/math"
    14  	"github.com/ava-labs/avalanchego/utils/set"
    15  	"github.com/ava-labs/avalanchego/vms/avm/txs"
    16  	"github.com/ava-labs/avalanchego/vms/components/avax"
    17  	"github.com/ava-labs/avalanchego/vms/components/verify"
    18  	"github.com/ava-labs/avalanchego/vms/nftfx"
    19  	"github.com/ava-labs/avalanchego/vms/propertyfx"
    20  	"github.com/ava-labs/avalanchego/vms/secp256k1fx"
    21  	"github.com/ava-labs/avalanchego/wallet/subnet/primary/common"
    22  )
    23  
    24  var (
    25  	errNoChangeAddress   = errors.New("no possible change address")
    26  	errInsufficientFunds = errors.New("insufficient funds")
    27  
    28  	fxIndexToID = map[uint32]ids.ID{
    29  		SECP256K1FxIndex: secp256k1fx.ID,
    30  		NFTFxIndex:       nftfx.ID,
    31  		PropertyFxIndex:  propertyfx.ID,
    32  	}
    33  
    34  	_ Builder = (*builder)(nil)
    35  )
    36  
    37  // Builder provides a convenient interface for building unsigned X-chain
    38  // transactions.
    39  type Builder interface {
    40  	// Context returns the configuration of the chain that this builder uses to
    41  	// create transactions.
    42  	Context() *Context
    43  
    44  	// GetFTBalance calculates the amount of each fungible asset that this
    45  	// builder has control over.
    46  	GetFTBalance(
    47  		options ...common.Option,
    48  	) (map[ids.ID]uint64, error)
    49  
    50  	// GetImportableBalance calculates the amount of each fungible asset that
    51  	// this builder could import from the provided chain.
    52  	//
    53  	// - [chainID] specifies the chain the funds are from.
    54  	GetImportableBalance(
    55  		chainID ids.ID,
    56  		options ...common.Option,
    57  	) (map[ids.ID]uint64, error)
    58  
    59  	// NewBaseTx creates a new simple value transfer.
    60  	//
    61  	// - [outputs] specifies all the recipients and amounts that should be sent
    62  	//   from this transaction.
    63  	NewBaseTx(
    64  		outputs []*avax.TransferableOutput,
    65  		options ...common.Option,
    66  	) (*txs.BaseTx, error)
    67  
    68  	// NewCreateAssetTx creates a new asset.
    69  	//
    70  	// - [name] specifies a human readable name for this asset.
    71  	// - [symbol] specifies a human readable abbreviation for this asset.
    72  	// - [denomination] specifies how many times the asset can be split. For
    73  	//   example, a denomination of [4] would mean that the smallest unit of the
    74  	//   asset would be 0.001 units.
    75  	// - [initialState] specifies the supported feature extensions for this
    76  	//   asset as well as the initial outputs for the asset.
    77  	NewCreateAssetTx(
    78  		name string,
    79  		symbol string,
    80  		denomination byte,
    81  		initialState map[uint32][]verify.State,
    82  		options ...common.Option,
    83  	) (*txs.CreateAssetTx, error)
    84  
    85  	// NewOperationTx performs state changes on the UTXO set. These state
    86  	// changes may be more complex than simple value transfers.
    87  	//
    88  	// - [operations] specifies the state changes to perform.
    89  	NewOperationTx(
    90  		operations []*txs.Operation,
    91  		options ...common.Option,
    92  	) (*txs.OperationTx, error)
    93  
    94  	// NewOperationTxMintFT performs a set of state changes that mint new tokens
    95  	// for the requested assets.
    96  	//
    97  	// - [outputs] maps the assetID to the output that should be created for the
    98  	//   asset.
    99  	NewOperationTxMintFT(
   100  		outputs map[ids.ID]*secp256k1fx.TransferOutput,
   101  		options ...common.Option,
   102  	) (*txs.OperationTx, error)
   103  
   104  	// NewOperationTxMintNFT performs a state change that mints new NFTs for the
   105  	// requested asset.
   106  	//
   107  	// - [assetID] specifies the asset to mint the NFTs under.
   108  	// - [payload] specifies the payload to provide each new NFT.
   109  	// - [owners] specifies the new owners of each NFT.
   110  	NewOperationTxMintNFT(
   111  		assetID ids.ID,
   112  		payload []byte,
   113  		owners []*secp256k1fx.OutputOwners,
   114  		options ...common.Option,
   115  	) (*txs.OperationTx, error)
   116  
   117  	// NewOperationTxMintProperty performs a state change that mints a new
   118  	// property for the requested asset.
   119  	//
   120  	// - [assetID] specifies the asset to mint the property under.
   121  	// - [owner] specifies the new owner of the property.
   122  	NewOperationTxMintProperty(
   123  		assetID ids.ID,
   124  		owner *secp256k1fx.OutputOwners,
   125  		options ...common.Option,
   126  	) (*txs.OperationTx, error)
   127  
   128  	// NewOperationTxBurnProperty performs state changes that burns all the
   129  	// properties of the requested asset.
   130  	//
   131  	// - [assetID] specifies the asset to burn the property of.
   132  	NewOperationTxBurnProperty(
   133  		assetID ids.ID,
   134  		options ...common.Option,
   135  	) (*txs.OperationTx, error)
   136  
   137  	// NewImportTx creates an import transaction that attempts to consume all
   138  	// the available UTXOs and import the funds to [to].
   139  	//
   140  	// - [chainID] specifies the chain to be importing funds from.
   141  	// - [to] specifies where to send the imported funds to.
   142  	NewImportTx(
   143  		chainID ids.ID,
   144  		to *secp256k1fx.OutputOwners,
   145  		options ...common.Option,
   146  	) (*txs.ImportTx, error)
   147  
   148  	// NewExportTx creates an export transaction that attempts to send all the
   149  	// provided [outputs] to the requested [chainID].
   150  	//
   151  	// - [chainID] specifies the chain to be exporting the funds to.
   152  	// - [outputs] specifies the outputs to send to the [chainID].
   153  	NewExportTx(
   154  		chainID ids.ID,
   155  		outputs []*avax.TransferableOutput,
   156  		options ...common.Option,
   157  	) (*txs.ExportTx, error)
   158  }
   159  
   160  type Backend interface {
   161  	UTXOs(ctx context.Context, sourceChainID ids.ID) ([]*avax.UTXO, error)
   162  }
   163  
   164  type builder struct {
   165  	addrs   set.Set[ids.ShortID]
   166  	context *Context
   167  	backend Backend
   168  }
   169  
   170  // New returns a new transaction builder.
   171  //
   172  //   - [addrs] is the set of addresses that the builder assumes can be used when
   173  //     signing the transactions in the future.
   174  //   - [context] provides the chain's configuration.
   175  //   - [backend] provides the chain's state.
   176  func New(
   177  	addrs set.Set[ids.ShortID],
   178  	context *Context,
   179  	backend Backend,
   180  ) Builder {
   181  	return &builder{
   182  		addrs:   addrs,
   183  		context: context,
   184  		backend: backend,
   185  	}
   186  }
   187  
   188  func (b *builder) Context() *Context {
   189  	return b.context
   190  }
   191  
   192  func (b *builder) GetFTBalance(
   193  	options ...common.Option,
   194  ) (map[ids.ID]uint64, error) {
   195  	ops := common.NewOptions(options)
   196  	return b.getBalance(b.context.BlockchainID, ops)
   197  }
   198  
   199  func (b *builder) GetImportableBalance(
   200  	chainID ids.ID,
   201  	options ...common.Option,
   202  ) (map[ids.ID]uint64, error) {
   203  	ops := common.NewOptions(options)
   204  	return b.getBalance(chainID, ops)
   205  }
   206  
   207  func (b *builder) NewBaseTx(
   208  	outputs []*avax.TransferableOutput,
   209  	options ...common.Option,
   210  ) (*txs.BaseTx, error) {
   211  	toBurn := map[ids.ID]uint64{
   212  		b.context.AVAXAssetID: b.context.BaseTxFee,
   213  	}
   214  	for _, out := range outputs {
   215  		assetID := out.AssetID()
   216  		amountToBurn, err := math.Add(toBurn[assetID], out.Out.Amount())
   217  		if err != nil {
   218  			return nil, err
   219  		}
   220  		toBurn[assetID] = amountToBurn
   221  	}
   222  
   223  	ops := common.NewOptions(options)
   224  	inputs, changeOutputs, err := b.spend(toBurn, ops)
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  	outputs = append(outputs, changeOutputs...)
   229  	avax.SortTransferableOutputs(outputs, Parser.Codec()) // sort the outputs
   230  
   231  	tx := &txs.BaseTx{BaseTx: avax.BaseTx{
   232  		NetworkID:    b.context.NetworkID,
   233  		BlockchainID: b.context.BlockchainID,
   234  		Ins:          inputs,
   235  		Outs:         outputs,
   236  		Memo:         ops.Memo(),
   237  	}}
   238  	return tx, b.initCtx(tx)
   239  }
   240  
   241  func (b *builder) NewCreateAssetTx(
   242  	name string,
   243  	symbol string,
   244  	denomination byte,
   245  	initialState map[uint32][]verify.State,
   246  	options ...common.Option,
   247  ) (*txs.CreateAssetTx, error) {
   248  	toBurn := map[ids.ID]uint64{
   249  		b.context.AVAXAssetID: b.context.CreateAssetTxFee,
   250  	}
   251  	ops := common.NewOptions(options)
   252  	inputs, outputs, err := b.spend(toBurn, ops)
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	codec := Parser.Codec()
   258  	states := make([]*txs.InitialState, 0, len(initialState))
   259  	for fxIndex, outs := range initialState {
   260  		state := &txs.InitialState{
   261  			FxIndex: fxIndex,
   262  			FxID:    fxIndexToID[fxIndex],
   263  			Outs:    outs,
   264  		}
   265  		state.Sort(codec) // sort the outputs
   266  		states = append(states, state)
   267  	}
   268  
   269  	utils.Sort(states) // sort the initial states
   270  	tx := &txs.CreateAssetTx{
   271  		BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
   272  			NetworkID:    b.context.NetworkID,
   273  			BlockchainID: b.context.BlockchainID,
   274  			Ins:          inputs,
   275  			Outs:         outputs,
   276  			Memo:         ops.Memo(),
   277  		}},
   278  		Name:         name,
   279  		Symbol:       symbol,
   280  		Denomination: denomination,
   281  		States:       states,
   282  	}
   283  	return tx, b.initCtx(tx)
   284  }
   285  
   286  func (b *builder) NewOperationTx(
   287  	operations []*txs.Operation,
   288  	options ...common.Option,
   289  ) (*txs.OperationTx, error) {
   290  	toBurn := map[ids.ID]uint64{
   291  		b.context.AVAXAssetID: b.context.BaseTxFee,
   292  	}
   293  	ops := common.NewOptions(options)
   294  	inputs, outputs, err := b.spend(toBurn, ops)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  
   299  	txs.SortOperations(operations, Parser.Codec())
   300  	tx := &txs.OperationTx{
   301  		BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
   302  			NetworkID:    b.context.NetworkID,
   303  			BlockchainID: b.context.BlockchainID,
   304  			Ins:          inputs,
   305  			Outs:         outputs,
   306  			Memo:         ops.Memo(),
   307  		}},
   308  		Ops: operations,
   309  	}
   310  	return tx, b.initCtx(tx)
   311  }
   312  
   313  func (b *builder) NewOperationTxMintFT(
   314  	outputs map[ids.ID]*secp256k1fx.TransferOutput,
   315  	options ...common.Option,
   316  ) (*txs.OperationTx, error) {
   317  	ops := common.NewOptions(options)
   318  	operations, err := b.mintFTs(outputs, ops)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	return b.NewOperationTx(operations, options...)
   323  }
   324  
   325  func (b *builder) NewOperationTxMintNFT(
   326  	assetID ids.ID,
   327  	payload []byte,
   328  	owners []*secp256k1fx.OutputOwners,
   329  	options ...common.Option,
   330  ) (*txs.OperationTx, error) {
   331  	ops := common.NewOptions(options)
   332  	operations, err := b.mintNFTs(assetID, payload, owners, ops)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  	return b.NewOperationTx(operations, options...)
   337  }
   338  
   339  func (b *builder) NewOperationTxMintProperty(
   340  	assetID ids.ID,
   341  	owner *secp256k1fx.OutputOwners,
   342  	options ...common.Option,
   343  ) (*txs.OperationTx, error) {
   344  	ops := common.NewOptions(options)
   345  	operations, err := b.mintProperty(assetID, owner, ops)
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  	return b.NewOperationTx(operations, options...)
   350  }
   351  
   352  func (b *builder) NewOperationTxBurnProperty(
   353  	assetID ids.ID,
   354  	options ...common.Option,
   355  ) (*txs.OperationTx, error) {
   356  	ops := common.NewOptions(options)
   357  	operations, err := b.burnProperty(assetID, ops)
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  	return b.NewOperationTx(operations, options...)
   362  }
   363  
   364  func (b *builder) NewImportTx(
   365  	chainID ids.ID,
   366  	to *secp256k1fx.OutputOwners,
   367  	options ...common.Option,
   368  ) (*txs.ImportTx, error) {
   369  	ops := common.NewOptions(options)
   370  	utxos, err := b.backend.UTXOs(ops.Context(), chainID)
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  
   375  	var (
   376  		addrs           = ops.Addresses(b.addrs)
   377  		minIssuanceTime = ops.MinIssuanceTime()
   378  		avaxAssetID     = b.context.AVAXAssetID
   379  		txFee           = b.context.BaseTxFee
   380  
   381  		importedInputs  = make([]*avax.TransferableInput, 0, len(utxos))
   382  		importedAmounts = make(map[ids.ID]uint64)
   383  	)
   384  	// Iterate over the unlocked UTXOs
   385  	for _, utxo := range utxos {
   386  		out, ok := utxo.Out.(*secp256k1fx.TransferOutput)
   387  		if !ok {
   388  			// Can't import an unknown transfer output type
   389  			continue
   390  		}
   391  
   392  		inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
   393  		if !ok {
   394  			// We couldn't spend this UTXO, so we skip to the next one
   395  			continue
   396  		}
   397  
   398  		importedInputs = append(importedInputs, &avax.TransferableInput{
   399  			UTXOID: utxo.UTXOID,
   400  			Asset:  utxo.Asset,
   401  			FxID:   secp256k1fx.ID,
   402  			In: &secp256k1fx.TransferInput{
   403  				Amt: out.Amt,
   404  				Input: secp256k1fx.Input{
   405  					SigIndices: inputSigIndices,
   406  				},
   407  			},
   408  		})
   409  
   410  		assetID := utxo.AssetID()
   411  		newImportedAmount, err := math.Add(importedAmounts[assetID], out.Amt)
   412  		if err != nil {
   413  			return nil, err
   414  		}
   415  		importedAmounts[assetID] = newImportedAmount
   416  	}
   417  	utils.Sort(importedInputs) // sort imported inputs
   418  
   419  	if len(importedAmounts) == 0 {
   420  		return nil, fmt.Errorf(
   421  			"%w: no UTXOs available to import",
   422  			errInsufficientFunds,
   423  		)
   424  	}
   425  
   426  	var (
   427  		inputs       []*avax.TransferableInput
   428  		outputs      = make([]*avax.TransferableOutput, 0, len(importedAmounts))
   429  		importedAVAX = importedAmounts[avaxAssetID]
   430  	)
   431  	if importedAVAX > txFee {
   432  		importedAmounts[avaxAssetID] -= txFee
   433  	} else {
   434  		if importedAVAX < txFee { // imported amount goes toward paying tx fee
   435  			toBurn := map[ids.ID]uint64{
   436  				avaxAssetID: txFee - importedAVAX,
   437  			}
   438  			var err error
   439  			inputs, outputs, err = b.spend(toBurn, ops)
   440  			if err != nil {
   441  				return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
   442  			}
   443  		}
   444  		delete(importedAmounts, avaxAssetID)
   445  	}
   446  
   447  	for assetID, amount := range importedAmounts {
   448  		outputs = append(outputs, &avax.TransferableOutput{
   449  			Asset: avax.Asset{ID: assetID},
   450  			FxID:  secp256k1fx.ID,
   451  			Out: &secp256k1fx.TransferOutput{
   452  				Amt:          amount,
   453  				OutputOwners: *to,
   454  			},
   455  		})
   456  	}
   457  
   458  	avax.SortTransferableOutputs(outputs, Parser.Codec())
   459  	tx := &txs.ImportTx{
   460  		BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
   461  			NetworkID:    b.context.NetworkID,
   462  			BlockchainID: b.context.BlockchainID,
   463  			Ins:          inputs,
   464  			Outs:         outputs,
   465  			Memo:         ops.Memo(),
   466  		}},
   467  		SourceChain: chainID,
   468  		ImportedIns: importedInputs,
   469  	}
   470  	return tx, b.initCtx(tx)
   471  }
   472  
   473  func (b *builder) NewExportTx(
   474  	chainID ids.ID,
   475  	outputs []*avax.TransferableOutput,
   476  	options ...common.Option,
   477  ) (*txs.ExportTx, error) {
   478  	toBurn := map[ids.ID]uint64{
   479  		b.context.AVAXAssetID: b.context.BaseTxFee,
   480  	}
   481  	for _, out := range outputs {
   482  		assetID := out.AssetID()
   483  		amountToBurn, err := math.Add(toBurn[assetID], out.Out.Amount())
   484  		if err != nil {
   485  			return nil, err
   486  		}
   487  		toBurn[assetID] = amountToBurn
   488  	}
   489  
   490  	ops := common.NewOptions(options)
   491  	inputs, changeOutputs, err := b.spend(toBurn, ops)
   492  	if err != nil {
   493  		return nil, err
   494  	}
   495  
   496  	avax.SortTransferableOutputs(outputs, Parser.Codec())
   497  	tx := &txs.ExportTx{
   498  		BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
   499  			NetworkID:    b.context.NetworkID,
   500  			BlockchainID: b.context.BlockchainID,
   501  			Ins:          inputs,
   502  			Outs:         changeOutputs,
   503  			Memo:         ops.Memo(),
   504  		}},
   505  		DestinationChain: chainID,
   506  		ExportedOuts:     outputs,
   507  	}
   508  	return tx, b.initCtx(tx)
   509  }
   510  
   511  func (b *builder) getBalance(
   512  	chainID ids.ID,
   513  	options *common.Options,
   514  ) (
   515  	balance map[ids.ID]uint64,
   516  	err error,
   517  ) {
   518  	utxos, err := b.backend.UTXOs(options.Context(), chainID)
   519  	if err != nil {
   520  		return nil, err
   521  	}
   522  
   523  	addrs := options.Addresses(b.addrs)
   524  	minIssuanceTime := options.MinIssuanceTime()
   525  	balance = make(map[ids.ID]uint64)
   526  
   527  	// Iterate over the UTXOs
   528  	for _, utxo := range utxos {
   529  		outIntf := utxo.Out
   530  		out, ok := outIntf.(*secp256k1fx.TransferOutput)
   531  		if !ok {
   532  			// We only support [secp256k1fx.TransferOutput]s.
   533  			continue
   534  		}
   535  
   536  		_, ok = common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
   537  		if !ok {
   538  			// We couldn't spend this UTXO, so we skip to the next one
   539  			continue
   540  		}
   541  
   542  		assetID := utxo.AssetID()
   543  		balance[assetID], err = math.Add(balance[assetID], out.Amt)
   544  		if err != nil {
   545  			return nil, err
   546  		}
   547  	}
   548  	return balance, nil
   549  }
   550  
   551  func (b *builder) spend(
   552  	amountsToBurn map[ids.ID]uint64,
   553  	options *common.Options,
   554  ) (
   555  	inputs []*avax.TransferableInput,
   556  	outputs []*avax.TransferableOutput,
   557  	err error,
   558  ) {
   559  	utxos, err := b.backend.UTXOs(options.Context(), b.context.BlockchainID)
   560  	if err != nil {
   561  		return nil, nil, err
   562  	}
   563  
   564  	addrs := options.Addresses(b.addrs)
   565  	minIssuanceTime := options.MinIssuanceTime()
   566  
   567  	addr, ok := addrs.Peek()
   568  	if !ok {
   569  		return nil, nil, errNoChangeAddress
   570  	}
   571  	changeOwner := options.ChangeOwner(&secp256k1fx.OutputOwners{
   572  		Threshold: 1,
   573  		Addrs:     []ids.ShortID{addr},
   574  	})
   575  
   576  	// Iterate over the UTXOs
   577  	for _, utxo := range utxos {
   578  		assetID := utxo.AssetID()
   579  		remainingAmountToBurn := amountsToBurn[assetID]
   580  
   581  		// If we have consumed enough of the asset, then we have no need burn
   582  		// more.
   583  		if remainingAmountToBurn == 0 {
   584  			continue
   585  		}
   586  
   587  		outIntf := utxo.Out
   588  		out, ok := outIntf.(*secp256k1fx.TransferOutput)
   589  		if !ok {
   590  			// We only support burning [secp256k1fx.TransferOutput]s.
   591  			continue
   592  		}
   593  
   594  		inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
   595  		if !ok {
   596  			// We couldn't spend this UTXO, so we skip to the next one
   597  			continue
   598  		}
   599  
   600  		inputs = append(inputs, &avax.TransferableInput{
   601  			UTXOID: utxo.UTXOID,
   602  			Asset:  utxo.Asset,
   603  			FxID:   secp256k1fx.ID,
   604  			In: &secp256k1fx.TransferInput{
   605  				Amt: out.Amt,
   606  				Input: secp256k1fx.Input{
   607  					SigIndices: inputSigIndices,
   608  				},
   609  			},
   610  		})
   611  
   612  		// Burn any value that should be burned
   613  		amountToBurn := min(
   614  			remainingAmountToBurn, // Amount we still need to burn
   615  			out.Amt,               // Amount available to burn
   616  		)
   617  		amountsToBurn[assetID] -= amountToBurn
   618  		if remainingAmount := out.Amt - amountToBurn; remainingAmount > 0 {
   619  			// This input had extra value, so some of it must be returned
   620  			outputs = append(outputs, &avax.TransferableOutput{
   621  				Asset: utxo.Asset,
   622  				FxID:  secp256k1fx.ID,
   623  				Out: &secp256k1fx.TransferOutput{
   624  					Amt:          remainingAmount,
   625  					OutputOwners: *changeOwner,
   626  				},
   627  			})
   628  		}
   629  	}
   630  
   631  	for assetID, amount := range amountsToBurn {
   632  		if amount != 0 {
   633  			return nil, nil, fmt.Errorf(
   634  				"%w: provided UTXOs need %d more units of asset %q",
   635  				errInsufficientFunds,
   636  				amount,
   637  				assetID,
   638  			)
   639  		}
   640  	}
   641  
   642  	utils.Sort(inputs)                                    // sort inputs
   643  	avax.SortTransferableOutputs(outputs, Parser.Codec()) // sort the change outputs
   644  	return inputs, outputs, nil
   645  }
   646  
   647  func (b *builder) mintFTs(
   648  	outputs map[ids.ID]*secp256k1fx.TransferOutput,
   649  	options *common.Options,
   650  ) (
   651  	operations []*txs.Operation,
   652  	err error,
   653  ) {
   654  	utxos, err := b.backend.UTXOs(options.Context(), b.context.BlockchainID)
   655  	if err != nil {
   656  		return nil, err
   657  	}
   658  
   659  	addrs := options.Addresses(b.addrs)
   660  	minIssuanceTime := options.MinIssuanceTime()
   661  
   662  	for _, utxo := range utxos {
   663  		assetID := utxo.AssetID()
   664  		output, ok := outputs[assetID]
   665  		if !ok {
   666  			continue
   667  		}
   668  
   669  		out, ok := utxo.Out.(*secp256k1fx.MintOutput)
   670  		if !ok {
   671  			continue
   672  		}
   673  
   674  		inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
   675  		if !ok {
   676  			continue
   677  		}
   678  
   679  		// add the operation to the array
   680  		operations = append(operations, &txs.Operation{
   681  			Asset:   utxo.Asset,
   682  			UTXOIDs: []*avax.UTXOID{&utxo.UTXOID},
   683  			FxID:    secp256k1fx.ID,
   684  			Op: &secp256k1fx.MintOperation{
   685  				MintInput: secp256k1fx.Input{
   686  					SigIndices: inputSigIndices,
   687  				},
   688  				MintOutput:     *out,
   689  				TransferOutput: *output,
   690  			},
   691  		})
   692  
   693  		// remove the asset from the required outputs to mint
   694  		delete(outputs, assetID)
   695  	}
   696  
   697  	for assetID := range outputs {
   698  		return nil, fmt.Errorf(
   699  			"%w: provided UTXOs not able to mint asset %q",
   700  			errInsufficientFunds,
   701  			assetID,
   702  		)
   703  	}
   704  	return operations, nil
   705  }
   706  
   707  // TODO: make this able to generate multiple NFT groups
   708  func (b *builder) mintNFTs(
   709  	assetID ids.ID,
   710  	payload []byte,
   711  	owners []*secp256k1fx.OutputOwners,
   712  	options *common.Options,
   713  ) (
   714  	operations []*txs.Operation,
   715  	err error,
   716  ) {
   717  	utxos, err := b.backend.UTXOs(options.Context(), b.context.BlockchainID)
   718  	if err != nil {
   719  		return nil, err
   720  	}
   721  
   722  	addrs := options.Addresses(b.addrs)
   723  	minIssuanceTime := options.MinIssuanceTime()
   724  
   725  	for _, utxo := range utxos {
   726  		if assetID != utxo.AssetID() {
   727  			continue
   728  		}
   729  
   730  		out, ok := utxo.Out.(*nftfx.MintOutput)
   731  		if !ok {
   732  			// wrong output type
   733  			continue
   734  		}
   735  
   736  		inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
   737  		if !ok {
   738  			continue
   739  		}
   740  
   741  		// add the operation to the array
   742  		operations = append(operations, &txs.Operation{
   743  			Asset: avax.Asset{ID: assetID},
   744  			UTXOIDs: []*avax.UTXOID{
   745  				&utxo.UTXOID,
   746  			},
   747  			FxID: nftfx.ID,
   748  			Op: &nftfx.MintOperation{
   749  				MintInput: secp256k1fx.Input{
   750  					SigIndices: inputSigIndices,
   751  				},
   752  				GroupID: out.GroupID,
   753  				Payload: payload,
   754  				Outputs: owners,
   755  			},
   756  		})
   757  		return operations, nil
   758  	}
   759  	return nil, fmt.Errorf(
   760  		"%w: provided UTXOs not able to mint NFT %q",
   761  		errInsufficientFunds,
   762  		assetID,
   763  	)
   764  }
   765  
   766  func (b *builder) mintProperty(
   767  	assetID ids.ID,
   768  	owner *secp256k1fx.OutputOwners,
   769  	options *common.Options,
   770  ) (
   771  	operations []*txs.Operation,
   772  	err error,
   773  ) {
   774  	utxos, err := b.backend.UTXOs(options.Context(), b.context.BlockchainID)
   775  	if err != nil {
   776  		return nil, err
   777  	}
   778  
   779  	addrs := options.Addresses(b.addrs)
   780  	minIssuanceTime := options.MinIssuanceTime()
   781  
   782  	for _, utxo := range utxos {
   783  		if assetID != utxo.AssetID() {
   784  			continue
   785  		}
   786  
   787  		out, ok := utxo.Out.(*propertyfx.MintOutput)
   788  		if !ok {
   789  			// wrong output type
   790  			continue
   791  		}
   792  
   793  		inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
   794  		if !ok {
   795  			continue
   796  		}
   797  
   798  		// add the operation to the array
   799  		operations = append(operations, &txs.Operation{
   800  			Asset: avax.Asset{ID: assetID},
   801  			UTXOIDs: []*avax.UTXOID{
   802  				&utxo.UTXOID,
   803  			},
   804  			FxID: propertyfx.ID,
   805  			Op: &propertyfx.MintOperation{
   806  				MintInput: secp256k1fx.Input{
   807  					SigIndices: inputSigIndices,
   808  				},
   809  				MintOutput: *out,
   810  				OwnedOutput: propertyfx.OwnedOutput{
   811  					OutputOwners: *owner,
   812  				},
   813  			},
   814  		})
   815  		return operations, nil
   816  	}
   817  	return nil, fmt.Errorf(
   818  		"%w: provided UTXOs not able to mint property %q",
   819  		errInsufficientFunds,
   820  		assetID,
   821  	)
   822  }
   823  
   824  func (b *builder) burnProperty(
   825  	assetID ids.ID,
   826  	options *common.Options,
   827  ) (
   828  	operations []*txs.Operation,
   829  	err error,
   830  ) {
   831  	utxos, err := b.backend.UTXOs(options.Context(), b.context.BlockchainID)
   832  	if err != nil {
   833  		return nil, err
   834  	}
   835  
   836  	addrs := options.Addresses(b.addrs)
   837  	minIssuanceTime := options.MinIssuanceTime()
   838  
   839  	for _, utxo := range utxos {
   840  		if assetID != utxo.AssetID() {
   841  			continue
   842  		}
   843  
   844  		out, ok := utxo.Out.(*propertyfx.OwnedOutput)
   845  		if !ok {
   846  			// wrong output type
   847  			continue
   848  		}
   849  
   850  		inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
   851  		if !ok {
   852  			continue
   853  		}
   854  
   855  		// add the operation to the array
   856  		operations = append(operations, &txs.Operation{
   857  			Asset: avax.Asset{ID: assetID},
   858  			UTXOIDs: []*avax.UTXOID{
   859  				&utxo.UTXOID,
   860  			},
   861  			FxID: propertyfx.ID,
   862  			Op: &propertyfx.BurnOperation{
   863  				Input: secp256k1fx.Input{
   864  					SigIndices: inputSigIndices,
   865  				},
   866  			},
   867  		})
   868  	}
   869  	if len(operations) == 0 {
   870  		return nil, fmt.Errorf(
   871  			"%w: provided UTXOs not able to burn property %q",
   872  			errInsufficientFunds,
   873  			assetID,
   874  		)
   875  	}
   876  	return operations, nil
   877  }
   878  
   879  func (b *builder) initCtx(tx txs.UnsignedTx) error {
   880  	ctx, err := NewSnowContext(
   881  		b.context.NetworkID,
   882  		b.context.BlockchainID,
   883  		b.context.AVAXAssetID,
   884  	)
   885  	if err != nil {
   886  		return err
   887  	}
   888  
   889  	tx.InitCtx(ctx)
   890  	return nil
   891  }