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 }