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 }