github.com/ava-labs/avalanchego@v1.11.11/wallet/chain/c/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 c
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"math/big"
    10  
    11  	"github.com/ava-labs/coreth/plugin/evm"
    12  
    13  	"github.com/ava-labs/avalanchego/ids"
    14  	"github.com/ava-labs/avalanchego/utils"
    15  	"github.com/ava-labs/avalanchego/utils/math"
    16  	"github.com/ava-labs/avalanchego/utils/set"
    17  	"github.com/ava-labs/avalanchego/vms/components/avax"
    18  	"github.com/ava-labs/avalanchego/vms/secp256k1fx"
    19  	"github.com/ava-labs/avalanchego/wallet/subnet/primary/common"
    20  
    21  	ethcommon "github.com/ethereum/go-ethereum/common"
    22  )
    23  
    24  const avaxConversionRateInt = 1_000_000_000
    25  
    26  var (
    27  	_ Builder = (*builder)(nil)
    28  
    29  	errInsufficientFunds = errors.New("insufficient funds")
    30  
    31  	// avaxConversionRate is the conversion rate between the smallest
    32  	// denomination on the X-Chain and P-chain, 1 nAVAX, and the smallest
    33  	// denomination on the C-Chain 1 wei. Where 1 nAVAX = 1 gWei.
    34  	//
    35  	// This is only required for AVAX because the denomination of 1 AVAX is 9
    36  	// decimal places on the X and P chains, but is 18 decimal places within the
    37  	// EVM.
    38  	avaxConversionRate = big.NewInt(avaxConversionRateInt)
    39  )
    40  
    41  // Builder provides a convenient interface for building unsigned C-chain
    42  // transactions.
    43  type Builder interface {
    44  	// Context returns the configuration of the chain that this builder uses to
    45  	// create transactions.
    46  	Context() *Context
    47  
    48  	// GetBalance calculates the amount of AVAX that this builder has control
    49  	// over.
    50  	GetBalance(
    51  		options ...common.Option,
    52  	) (*big.Int, error)
    53  
    54  	// GetImportableBalance calculates the amount of AVAX that this builder
    55  	// could import from the provided chain.
    56  	//
    57  	// - [chainID] specifies the chain the funds are from.
    58  	GetImportableBalance(
    59  		chainID ids.ID,
    60  		options ...common.Option,
    61  	) (uint64, error)
    62  
    63  	// NewImportTx creates an import transaction that attempts to consume all
    64  	// the available UTXOs and import the funds to [to].
    65  	//
    66  	// - [chainID] specifies the chain to be importing funds from.
    67  	// - [to] specifies where to send the imported funds to.
    68  	// - [baseFee] specifies the fee price willing to be paid by this tx.
    69  	NewImportTx(
    70  		chainID ids.ID,
    71  		to ethcommon.Address,
    72  		baseFee *big.Int,
    73  		options ...common.Option,
    74  	) (*evm.UnsignedImportTx, error)
    75  
    76  	// NewExportTx creates an export transaction that attempts to send all the
    77  	// provided [outputs] to the requested [chainID].
    78  	//
    79  	// - [chainID] specifies the chain to be exporting the funds to.
    80  	// - [outputs] specifies the outputs to send to the [chainID].
    81  	// - [baseFee] specifies the fee price willing to be paid by this tx.
    82  	NewExportTx(
    83  		chainID ids.ID,
    84  		outputs []*secp256k1fx.TransferOutput,
    85  		baseFee *big.Int,
    86  		options ...common.Option,
    87  	) (*evm.UnsignedExportTx, error)
    88  }
    89  
    90  // BuilderBackend specifies the required information needed to build unsigned
    91  // C-chain transactions.
    92  type BuilderBackend interface {
    93  	UTXOs(ctx context.Context, sourceChainID ids.ID) ([]*avax.UTXO, error)
    94  	Balance(ctx context.Context, addr ethcommon.Address) (*big.Int, error)
    95  	Nonce(ctx context.Context, addr ethcommon.Address) (uint64, error)
    96  }
    97  
    98  type builder struct {
    99  	avaxAddrs set.Set[ids.ShortID]
   100  	ethAddrs  set.Set[ethcommon.Address]
   101  	context   *Context
   102  	backend   BuilderBackend
   103  }
   104  
   105  // NewBuilder returns a new transaction builder.
   106  //
   107  //   - [avaxAddrs] is the set of addresses in the AVAX format that the builder
   108  //     assumes can be used when signing the transactions in the future.
   109  //   - [ethAddrs] is the set of addresses in the Eth format that the builder
   110  //     assumes can be used when signing the transactions in the future.
   111  //   - [backend] provides the required access to the chain's context and state
   112  //     to build out the transactions.
   113  func NewBuilder(
   114  	avaxAddrs set.Set[ids.ShortID],
   115  	ethAddrs set.Set[ethcommon.Address],
   116  	context *Context,
   117  	backend BuilderBackend,
   118  ) Builder {
   119  	return &builder{
   120  		avaxAddrs: avaxAddrs,
   121  		ethAddrs:  ethAddrs,
   122  		context:   context,
   123  		backend:   backend,
   124  	}
   125  }
   126  
   127  func (b *builder) Context() *Context {
   128  	return b.context
   129  }
   130  
   131  func (b *builder) GetBalance(
   132  	options ...common.Option,
   133  ) (*big.Int, error) {
   134  	var (
   135  		ops          = common.NewOptions(options)
   136  		ctx          = ops.Context()
   137  		addrs        = ops.EthAddresses(b.ethAddrs)
   138  		totalBalance = new(big.Int)
   139  	)
   140  	for addr := range addrs {
   141  		balance, err := b.backend.Balance(ctx, addr)
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  		totalBalance.Add(totalBalance, balance)
   146  	}
   147  
   148  	return totalBalance, nil
   149  }
   150  
   151  func (b *builder) GetImportableBalance(
   152  	chainID ids.ID,
   153  	options ...common.Option,
   154  ) (uint64, error) {
   155  	ops := common.NewOptions(options)
   156  	utxos, err := b.backend.UTXOs(ops.Context(), chainID)
   157  	if err != nil {
   158  		return 0, err
   159  	}
   160  
   161  	var (
   162  		addrs           = ops.Addresses(b.avaxAddrs)
   163  		minIssuanceTime = ops.MinIssuanceTime()
   164  		avaxAssetID     = b.context.AVAXAssetID
   165  		balance         uint64
   166  	)
   167  	for _, utxo := range utxos {
   168  		amount, _, ok := getSpendableAmount(utxo, addrs, minIssuanceTime, avaxAssetID)
   169  		if !ok {
   170  			continue
   171  		}
   172  
   173  		newBalance, err := math.Add(balance, amount)
   174  		if err != nil {
   175  			return 0, err
   176  		}
   177  		balance = newBalance
   178  	}
   179  
   180  	return balance, nil
   181  }
   182  
   183  func (b *builder) NewImportTx(
   184  	chainID ids.ID,
   185  	to ethcommon.Address,
   186  	baseFee *big.Int,
   187  	options ...common.Option,
   188  ) (*evm.UnsignedImportTx, error) {
   189  	ops := common.NewOptions(options)
   190  	utxos, err := b.backend.UTXOs(ops.Context(), chainID)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	var (
   196  		addrs           = ops.Addresses(b.avaxAddrs)
   197  		minIssuanceTime = ops.MinIssuanceTime()
   198  		avaxAssetID     = b.context.AVAXAssetID
   199  
   200  		importedInputs = make([]*avax.TransferableInput, 0, len(utxos))
   201  		importedAmount uint64
   202  	)
   203  	for _, utxo := range utxos {
   204  		amount, inputSigIndices, ok := getSpendableAmount(utxo, addrs, minIssuanceTime, avaxAssetID)
   205  		if !ok {
   206  			continue
   207  		}
   208  
   209  		importedInputs = append(importedInputs, &avax.TransferableInput{
   210  			UTXOID: utxo.UTXOID,
   211  			Asset:  utxo.Asset,
   212  			FxID:   secp256k1fx.ID,
   213  			In: &secp256k1fx.TransferInput{
   214  				Amt: amount,
   215  				Input: secp256k1fx.Input{
   216  					SigIndices: inputSigIndices,
   217  				},
   218  			},
   219  		})
   220  
   221  		newImportedAmount, err := math.Add(importedAmount, amount)
   222  		if err != nil {
   223  			return nil, err
   224  		}
   225  		importedAmount = newImportedAmount
   226  	}
   227  
   228  	utils.Sort(importedInputs)
   229  	tx := &evm.UnsignedImportTx{
   230  		NetworkID:      b.context.NetworkID,
   231  		BlockchainID:   b.context.BlockchainID,
   232  		SourceChain:    chainID,
   233  		ImportedInputs: importedInputs,
   234  	}
   235  
   236  	// We must initialize the bytes of the tx to calculate the initial cost
   237  	wrappedTx := &evm.Tx{UnsignedAtomicTx: tx}
   238  	if err := wrappedTx.Sign(evm.Codec, nil); err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	gasUsedWithoutOutput, err := tx.GasUsed(true /*=IsApricotPhase5*/)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	gasUsedWithOutput := gasUsedWithoutOutput + evm.EVMOutputGas
   247  
   248  	txFee, err := evm.CalculateDynamicFee(gasUsedWithOutput, baseFee)
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  
   253  	if importedAmount <= txFee {
   254  		return nil, errInsufficientFunds
   255  	}
   256  
   257  	tx.Outs = []evm.EVMOutput{{
   258  		Address: to,
   259  		Amount:  importedAmount - txFee,
   260  		AssetID: avaxAssetID,
   261  	}}
   262  	return tx, nil
   263  }
   264  
   265  func (b *builder) NewExportTx(
   266  	chainID ids.ID,
   267  	outputs []*secp256k1fx.TransferOutput,
   268  	baseFee *big.Int,
   269  	options ...common.Option,
   270  ) (*evm.UnsignedExportTx, error) {
   271  	var (
   272  		avaxAssetID     = b.context.AVAXAssetID
   273  		exportedOutputs = make([]*avax.TransferableOutput, len(outputs))
   274  		exportedAmount  uint64
   275  	)
   276  	for i, output := range outputs {
   277  		exportedOutputs[i] = &avax.TransferableOutput{
   278  			Asset: avax.Asset{ID: avaxAssetID},
   279  			FxID:  secp256k1fx.ID,
   280  			Out:   output,
   281  		}
   282  
   283  		newExportedAmount, err := math.Add(exportedAmount, output.Amt)
   284  		if err != nil {
   285  			return nil, err
   286  		}
   287  		exportedAmount = newExportedAmount
   288  	}
   289  
   290  	avax.SortTransferableOutputs(exportedOutputs, evm.Codec)
   291  	tx := &evm.UnsignedExportTx{
   292  		NetworkID:        b.context.NetworkID,
   293  		BlockchainID:     b.context.BlockchainID,
   294  		DestinationChain: chainID,
   295  		ExportedOutputs:  exportedOutputs,
   296  	}
   297  
   298  	// We must initialize the bytes of the tx to calculate the initial cost
   299  	wrappedTx := &evm.Tx{UnsignedAtomicTx: tx}
   300  	if err := wrappedTx.Sign(evm.Codec, nil); err != nil {
   301  		return nil, err
   302  	}
   303  
   304  	cost, err := tx.GasUsed(true /*=IsApricotPhase5*/)
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  
   309  	initialFee, err := evm.CalculateDynamicFee(cost, baseFee)
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  
   314  	amountToConsume, err := math.Add(exportedAmount, initialFee)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  
   319  	var (
   320  		ops    = common.NewOptions(options)
   321  		ctx    = ops.Context()
   322  		addrs  = ops.EthAddresses(b.ethAddrs)
   323  		inputs = make([]evm.EVMInput, 0, addrs.Len())
   324  	)
   325  	for addr := range addrs {
   326  		if amountToConsume == 0 {
   327  			break
   328  		}
   329  
   330  		prevFee, err := evm.CalculateDynamicFee(cost, baseFee)
   331  		if err != nil {
   332  			return nil, err
   333  		}
   334  
   335  		newCost := cost + evm.EVMInputGas
   336  		newFee, err := evm.CalculateDynamicFee(newCost, baseFee)
   337  		if err != nil {
   338  			return nil, err
   339  		}
   340  
   341  		additionalFee := newFee - prevFee
   342  
   343  		balance, err := b.backend.Balance(ctx, addr)
   344  		if err != nil {
   345  			return nil, err
   346  		}
   347  
   348  		// Since the asset is AVAX, we divide by the avaxConversionRate to
   349  		// convert back to the correct denomination of AVAX that can be
   350  		// exported.
   351  		avaxBalance := new(big.Int).Div(balance, avaxConversionRate).Uint64()
   352  
   353  		// If the balance for [addr] is insufficient to cover the additional
   354  		// cost of adding an input to the transaction, skip adding the input
   355  		// altogether.
   356  		if avaxBalance <= additionalFee {
   357  			continue
   358  		}
   359  
   360  		// Update the cost for the next iteration
   361  		cost = newCost
   362  
   363  		amountToConsume, err = math.Add(amountToConsume, additionalFee)
   364  		if err != nil {
   365  			return nil, err
   366  		}
   367  
   368  		nonce, err := b.backend.Nonce(ctx, addr)
   369  		if err != nil {
   370  			return nil, err
   371  		}
   372  
   373  		inputAmount := min(amountToConsume, avaxBalance)
   374  		inputs = append(inputs, evm.EVMInput{
   375  			Address: addr,
   376  			Amount:  inputAmount,
   377  			AssetID: avaxAssetID,
   378  			Nonce:   nonce,
   379  		})
   380  		amountToConsume -= inputAmount
   381  	}
   382  
   383  	if amountToConsume > 0 {
   384  		return nil, errInsufficientFunds
   385  	}
   386  
   387  	utils.Sort(inputs)
   388  	tx.Ins = inputs
   389  
   390  	snowCtx, err := newSnowContext(b.context)
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  	for _, out := range tx.ExportedOutputs {
   395  		out.InitCtx(snowCtx)
   396  	}
   397  	return tx, nil
   398  }
   399  
   400  func getSpendableAmount(
   401  	utxo *avax.UTXO,
   402  	addrs set.Set[ids.ShortID],
   403  	minIssuanceTime uint64,
   404  	avaxAssetID ids.ID,
   405  ) (uint64, []uint32, bool) {
   406  	if utxo.Asset.ID != avaxAssetID {
   407  		// Only AVAX can be imported
   408  		return 0, nil, false
   409  	}
   410  
   411  	out, ok := utxo.Out.(*secp256k1fx.TransferOutput)
   412  	if !ok {
   413  		// Can't import an unknown transfer output type
   414  		return 0, nil, false
   415  	}
   416  
   417  	inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
   418  	return out.Amt, inputSigIndices, ok
   419  }