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 }