github.com/ava-labs/avalanchego@v1.11.11/vms/avm/wallet_service.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  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  
    11  	"go.uber.org/zap"
    12  	"golang.org/x/exp/maps"
    13  
    14  	"github.com/ava-labs/avalanchego/api"
    15  	"github.com/ava-labs/avalanchego/ids"
    16  	"github.com/ava-labs/avalanchego/utils/formatting"
    17  	"github.com/ava-labs/avalanchego/utils/linked"
    18  	"github.com/ava-labs/avalanchego/utils/logging"
    19  	"github.com/ava-labs/avalanchego/utils/math"
    20  	"github.com/ava-labs/avalanchego/vms/avm/txs"
    21  	"github.com/ava-labs/avalanchego/vms/components/avax"
    22  	"github.com/ava-labs/avalanchego/vms/secp256k1fx"
    23  	"github.com/ava-labs/avalanchego/vms/txs/mempool"
    24  )
    25  
    26  var errMissingUTXO = errors.New("missing utxo")
    27  
    28  type WalletService struct {
    29  	vm         *VM
    30  	pendingTxs *linked.Hashmap[ids.ID, *txs.Tx]
    31  }
    32  
    33  func (w *WalletService) decided(txID ids.ID) {
    34  	if !w.pendingTxs.Delete(txID) {
    35  		return
    36  	}
    37  
    38  	w.vm.ctx.Log.Info("tx decided over wallet API",
    39  		zap.Stringer("txID", txID),
    40  	)
    41  	for {
    42  		txID, tx, ok := w.pendingTxs.Oldest()
    43  		if !ok {
    44  			return
    45  		}
    46  
    47  		err := w.vm.network.IssueTxFromRPCWithoutVerification(tx)
    48  		if err == nil {
    49  			w.vm.ctx.Log.Info("issued tx to mempool over wallet API",
    50  				zap.Stringer("txID", txID),
    51  			)
    52  			return
    53  		}
    54  		if errors.Is(err, mempool.ErrDuplicateTx) {
    55  			return
    56  		}
    57  
    58  		w.pendingTxs.Delete(txID)
    59  		w.vm.ctx.Log.Warn("dropping tx issued over wallet API",
    60  			zap.Stringer("txID", txID),
    61  			zap.Error(err),
    62  		)
    63  	}
    64  }
    65  
    66  func (w *WalletService) issue(tx *txs.Tx) (ids.ID, error) {
    67  	txID := tx.ID()
    68  	w.vm.ctx.Log.Info("issuing tx over wallet API",
    69  		zap.Stringer("txID", txID),
    70  	)
    71  
    72  	if _, ok := w.pendingTxs.Get(txID); ok {
    73  		w.vm.ctx.Log.Warn("issuing duplicate tx over wallet API",
    74  			zap.Stringer("txID", txID),
    75  		)
    76  		return txID, nil
    77  	}
    78  
    79  	if w.pendingTxs.Len() == 0 {
    80  		if err := w.vm.network.IssueTxFromRPCWithoutVerification(tx); err == nil {
    81  			w.vm.ctx.Log.Info("issued tx to mempool over wallet API",
    82  				zap.Stringer("txID", txID),
    83  			)
    84  		} else if !errors.Is(err, mempool.ErrDuplicateTx) {
    85  			w.vm.ctx.Log.Warn("failed to issue tx over wallet API",
    86  				zap.Stringer("txID", txID),
    87  				zap.Error(err),
    88  			)
    89  			return ids.Empty, err
    90  		}
    91  	} else {
    92  		w.vm.ctx.Log.Info("enqueueing tx over wallet API",
    93  			zap.Stringer("txID", txID),
    94  		)
    95  	}
    96  
    97  	w.pendingTxs.Put(txID, tx)
    98  	return txID, nil
    99  }
   100  
   101  func (w *WalletService) update(utxos []*avax.UTXO) ([]*avax.UTXO, error) {
   102  	utxoMap := make(map[ids.ID]*avax.UTXO, len(utxos))
   103  	for _, utxo := range utxos {
   104  		utxoMap[utxo.InputID()] = utxo
   105  	}
   106  
   107  	iter := w.pendingTxs.NewIterator()
   108  
   109  	for iter.Next() {
   110  		tx := iter.Value()
   111  		for _, inputUTXO := range tx.Unsigned.InputUTXOs() {
   112  			if inputUTXO.Symbolic() {
   113  				continue
   114  			}
   115  			utxoID := inputUTXO.InputID()
   116  			if _, exists := utxoMap[utxoID]; !exists {
   117  				return nil, errMissingUTXO
   118  			}
   119  			delete(utxoMap, utxoID)
   120  		}
   121  
   122  		for _, utxo := range tx.UTXOs() {
   123  			utxoMap[utxo.InputID()] = utxo
   124  		}
   125  	}
   126  
   127  	return maps.Values(utxoMap), nil
   128  }
   129  
   130  // IssueTx attempts to issue a transaction into consensus
   131  func (w *WalletService) IssueTx(_ *http.Request, args *api.FormattedTx, reply *api.JSONTxID) error {
   132  	w.vm.ctx.Log.Warn("deprecated API called",
   133  		zap.String("service", "wallet"),
   134  		zap.String("method", "issueTx"),
   135  		logging.UserString("tx", args.Tx),
   136  	)
   137  
   138  	txBytes, err := formatting.Decode(args.Encoding, args.Tx)
   139  	if err != nil {
   140  		return fmt.Errorf("problem decoding transaction: %w", err)
   141  	}
   142  
   143  	tx, err := w.vm.parser.ParseTx(txBytes)
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	w.vm.ctx.Lock.Lock()
   149  	defer w.vm.ctx.Lock.Unlock()
   150  
   151  	txID, err := w.issue(tx)
   152  	reply.TxID = txID
   153  	return err
   154  }
   155  
   156  // Send returns the ID of the newly created transaction
   157  func (w *WalletService) Send(r *http.Request, args *SendArgs, reply *api.JSONTxIDChangeAddr) error {
   158  	return w.SendMultiple(r, &SendMultipleArgs{
   159  		JSONSpendHeader: args.JSONSpendHeader,
   160  		Outputs:         []SendOutput{args.SendOutput},
   161  		Memo:            args.Memo,
   162  	}, reply)
   163  }
   164  
   165  // SendMultiple sends a transaction with multiple outputs.
   166  func (w *WalletService) SendMultiple(_ *http.Request, args *SendMultipleArgs, reply *api.JSONTxIDChangeAddr) error {
   167  	w.vm.ctx.Log.Warn("deprecated API called",
   168  		zap.String("service", "wallet"),
   169  		zap.String("method", "sendMultiple"),
   170  		logging.UserString("username", args.Username),
   171  	)
   172  
   173  	// Validate the memo field
   174  	memoBytes := []byte(args.Memo)
   175  	if l := len(memoBytes); l > avax.MaxMemoSize {
   176  		return fmt.Errorf("max memo length is %d but provided memo field is length %d",
   177  			avax.MaxMemoSize,
   178  			l)
   179  	} else if len(args.Outputs) == 0 {
   180  		return errNoOutputs
   181  	}
   182  
   183  	// Parse the from addresses
   184  	fromAddrs, err := avax.ParseServiceAddresses(w.vm, args.From)
   185  	if err != nil {
   186  		return fmt.Errorf("couldn't parse 'From' addresses: %w", err)
   187  	}
   188  
   189  	w.vm.ctx.Lock.Lock()
   190  	defer w.vm.ctx.Lock.Unlock()
   191  
   192  	// Load user's UTXOs/keys
   193  	utxos, kc, err := w.vm.LoadUser(args.Username, args.Password, fromAddrs)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	utxos, err = w.update(utxos)
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	// Parse the change address.
   204  	if len(kc.Keys) == 0 {
   205  		return errNoKeys
   206  	}
   207  	changeAddr, err := w.vm.selectChangeAddr(kc.Keys[0].PublicKey().Address(), args.ChangeAddr)
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	// Calculate required input amounts and create the desired outputs
   213  	// String repr. of asset ID --> asset ID
   214  	assetIDs := make(map[string]ids.ID)
   215  	// Asset ID --> amount of that asset being sent
   216  	amounts := make(map[ids.ID]uint64)
   217  	// Outputs of our tx
   218  	outs := []*avax.TransferableOutput{}
   219  	for _, output := range args.Outputs {
   220  		if output.Amount == 0 {
   221  			return errZeroAmount
   222  		}
   223  		assetID, ok := assetIDs[output.AssetID] // Asset ID of next output
   224  		if !ok {
   225  			assetID, err = w.vm.lookupAssetID(output.AssetID)
   226  			if err != nil {
   227  				return fmt.Errorf("couldn't find asset %s", output.AssetID)
   228  			}
   229  			assetIDs[output.AssetID] = assetID
   230  		}
   231  		currentAmount := amounts[assetID]
   232  		newAmount, err := math.Add(currentAmount, uint64(output.Amount))
   233  		if err != nil {
   234  			return fmt.Errorf("problem calculating required spend amount: %w", err)
   235  		}
   236  		amounts[assetID] = newAmount
   237  
   238  		// Parse the to address
   239  		to, err := avax.ParseServiceAddress(w.vm, output.To)
   240  		if err != nil {
   241  			return fmt.Errorf("problem parsing to address %q: %w", output.To, err)
   242  		}
   243  
   244  		// Create the Output
   245  		outs = append(outs, &avax.TransferableOutput{
   246  			Asset: avax.Asset{ID: assetID},
   247  			Out: &secp256k1fx.TransferOutput{
   248  				Amt: uint64(output.Amount),
   249  				OutputOwners: secp256k1fx.OutputOwners{
   250  					Locktime:  0,
   251  					Threshold: 1,
   252  					Addrs:     []ids.ShortID{to},
   253  				},
   254  			},
   255  		})
   256  	}
   257  
   258  	amountsWithFee := maps.Clone(amounts)
   259  
   260  	amountWithFee, err := math.Add(amounts[w.vm.feeAssetID], w.vm.TxFee)
   261  	if err != nil {
   262  		return fmt.Errorf("problem calculating required spend amount: %w", err)
   263  	}
   264  	amountsWithFee[w.vm.feeAssetID] = amountWithFee
   265  
   266  	amountsSpent, ins, keys, err := w.vm.Spend(
   267  		utxos,
   268  		kc,
   269  		amountsWithFee,
   270  	)
   271  	if err != nil {
   272  		return err
   273  	}
   274  
   275  	// Add the required change outputs
   276  	for assetID, amountWithFee := range amountsWithFee {
   277  		amountSpent := amountsSpent[assetID]
   278  
   279  		if amountSpent > amountWithFee {
   280  			outs = append(outs, &avax.TransferableOutput{
   281  				Asset: avax.Asset{ID: assetID},
   282  				Out: &secp256k1fx.TransferOutput{
   283  					Amt: amountSpent - amountWithFee,
   284  					OutputOwners: secp256k1fx.OutputOwners{
   285  						Locktime:  0,
   286  						Threshold: 1,
   287  						Addrs:     []ids.ShortID{changeAddr},
   288  					},
   289  				},
   290  			})
   291  		}
   292  	}
   293  
   294  	codec := w.vm.parser.Codec()
   295  	avax.SortTransferableOutputs(outs, codec)
   296  
   297  	tx := &txs.Tx{Unsigned: &txs.BaseTx{BaseTx: avax.BaseTx{
   298  		NetworkID:    w.vm.ctx.NetworkID,
   299  		BlockchainID: w.vm.ctx.ChainID,
   300  		Outs:         outs,
   301  		Ins:          ins,
   302  		Memo:         memoBytes,
   303  	}}}
   304  	if err := tx.SignSECP256K1Fx(codec, keys); err != nil {
   305  		return err
   306  	}
   307  
   308  	txID, err := w.issue(tx)
   309  	if err != nil {
   310  		return fmt.Errorf("problem issuing transaction: %w", err)
   311  	}
   312  
   313  	reply.TxID = txID
   314  	reply.ChangeAddr, err = w.vm.FormatLocalAddress(changeAddr)
   315  	return err
   316  }