mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Tail of the xvm codec kill (ddb3fbca93): the X-chain wallet builder + signer
now rebuild signed wire bytes as unsigned ‖ fx credential envelopes over the
native luxfi/utxo/wire form, and components/lux parses fx Inputs from their wire
envelope by concrete type. No linearcodec, no reflection on the wallet path.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
862 lines
21 KiB
Go
862 lines
21 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package x
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
stdcontext "context"
|
|
|
|
"github.com/luxfi/ids"
|
|
math "github.com/luxfi/math/safe"
|
|
"github.com/luxfi/math/set"
|
|
lux "github.com/luxfi/utxo"
|
|
"github.com/luxfi/node/vms/components/verify"
|
|
"github.com/luxfi/node/vms/xvm/txs"
|
|
"github.com/luxfi/node/wallet/chain/x/builder"
|
|
"github.com/luxfi/node/wallet/network/primary/common"
|
|
"github.com/luxfi/utils"
|
|
"github.com/luxfi/utxo/nftfx"
|
|
"github.com/luxfi/utxo/propertyfx"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
)
|
|
|
|
var (
|
|
errNoChangeAddress = errors.New("no possible change address")
|
|
errInsufficientFunds = errors.New("insufficient funds")
|
|
|
|
_ Builder = (*txBuilder)(nil)
|
|
)
|
|
|
|
// Builder provides a convenient interface for building unsigned X-chain
|
|
// transactions.
|
|
type Builder interface {
|
|
// GetFTBalance calculates the amount of each fungible asset that this
|
|
// builder has control over.
|
|
GetFTBalance(
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error)
|
|
|
|
// GetImportableBalance calculates the amount of each fungible asset that
|
|
// this builder could import from the provided chain.
|
|
//
|
|
// - [chainID] specifies the chain the funds are from.
|
|
GetImportableBalance(
|
|
chainID ids.ID,
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error)
|
|
|
|
// NewBaseTx creates a new simple value transfer.
|
|
//
|
|
// - [outputs] specifies all the recipients and amounts that should be sent
|
|
// from this transaction.
|
|
NewBaseTx(
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.BaseTx, error)
|
|
|
|
// NewCreateAssetTx creates a new asset.
|
|
//
|
|
// - [name] specifies a human readable name for this asset.
|
|
// - [symbol] specifies a human readable abbreviation for this asset.
|
|
// - [denomination] specifies how many times the asset can be split. For
|
|
// example, a denomination of [4] would mean that the smallest unit of the
|
|
// asset would be 0.001 constants.
|
|
// - [initialState] specifies the supported feature extensions for this
|
|
// asset as well as the initial outputs for the asset.
|
|
NewCreateAssetTx(
|
|
name string,
|
|
symbol string,
|
|
denomination byte,
|
|
initialState map[uint32][]verify.State,
|
|
options ...common.Option,
|
|
) (*txs.CreateAssetTx, error)
|
|
|
|
// NewOperationTx performs state changes on the UTXO set. These state
|
|
// changes may be more complex than simple value transfers.
|
|
//
|
|
// - [operations] specifies the state changes to perform.
|
|
NewOperationTx(
|
|
operations []*txs.Operation,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error)
|
|
|
|
// NewOperationTxMintFT performs a set of state changes that mint new tokens
|
|
// for the requested assets.
|
|
//
|
|
// - [outputs] maps the assetID to the output that should be created for the
|
|
// asset.
|
|
NewOperationTxMintFT(
|
|
outputs map[ids.ID]*secp256k1fx.TransferOutput,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error)
|
|
|
|
// NewOperationTxMintNFT performs a state change that mints new NFTs for the
|
|
// requested asset.
|
|
//
|
|
// - [assetID] specifies the asset to mint the NFTs under.
|
|
// - [payload] specifies the payload to provide each new NFT.
|
|
// - [owners] specifies the new owners of each NFT.
|
|
NewOperationTxMintNFT(
|
|
assetID ids.ID,
|
|
payload []byte,
|
|
owners []*secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error)
|
|
|
|
// NewOperationTxMintProperty performs a state change that mints a new
|
|
// property for the requested asset.
|
|
//
|
|
// - [assetID] specifies the asset to mint the property under.
|
|
// - [owner] specifies the new owner of the property.
|
|
NewOperationTxMintProperty(
|
|
assetID ids.ID,
|
|
owner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error)
|
|
|
|
// NewOperationTxBurnProperty performs state changes that burns all the
|
|
// properties of the requested asset.
|
|
//
|
|
// - [assetID] specifies the asset to burn the property of.
|
|
NewOperationTxBurnProperty(
|
|
assetID ids.ID,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error)
|
|
|
|
// NewImportTx creates an import transaction that attempts to consume all
|
|
// the available UTXOs and import the funds to [to].
|
|
//
|
|
// - [chainID] specifies the chain to be importing funds from.
|
|
// - [to] specifies where to send the imported funds to.
|
|
NewImportTx(
|
|
chainID ids.ID,
|
|
to *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.ImportTx, error)
|
|
|
|
// NewExportTx creates an export transaction that attempts to send all the
|
|
// provided [outputs] to the requested [chainID].
|
|
//
|
|
// - [chainID] specifies the chain to be exporting the funds to.
|
|
// - [outputs] specifies the outputs to send to the [chainID].
|
|
NewExportTx(
|
|
chainID ids.ID,
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.ExportTx, error)
|
|
}
|
|
|
|
// Context represents the X-chain context
|
|
type Context struct {
|
|
NetworkID uint32
|
|
BlockchainID ids.ID
|
|
UTXOAssetID ids.ID
|
|
BaseTxFee uint64
|
|
CreateAssetTxFee uint64
|
|
}
|
|
|
|
// BuilderBackend specifies the required information needed to build unsigned
|
|
// X-chain transactions.
|
|
type BuilderBackend interface {
|
|
Context() *builder.Context
|
|
BlockchainID() ids.ID
|
|
UTXOAssetID() ids.ID
|
|
BaseTxFee() uint64
|
|
CreateAssetTxFee() uint64
|
|
|
|
UTXOs(ctx stdcontext.Context, sourceChainID ids.ID) ([]*lux.UTXO, error)
|
|
}
|
|
|
|
type txBuilder struct {
|
|
addrs set.Set[ids.ShortID]
|
|
backend BuilderBackend
|
|
}
|
|
|
|
// NewBuilder returns a new transaction builder.
|
|
//
|
|
// - [addrs] is the set of addresses that the builder assumes can be used when
|
|
// signing the transactions in the future.
|
|
// - [backend] provides the required access to the chain's context and state
|
|
// to build out the transactions.
|
|
func NewBuilder(addrs set.Set[ids.ShortID], backend BuilderBackend) Builder {
|
|
return &txBuilder{
|
|
addrs: addrs,
|
|
backend: backend,
|
|
}
|
|
}
|
|
|
|
func (b *txBuilder) GetFTBalance(
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error) {
|
|
ops := common.NewOptions(options)
|
|
return b.getBalance(b.backend.BlockchainID(), ops)
|
|
}
|
|
|
|
func (b *txBuilder) GetImportableBalance(
|
|
chainID ids.ID,
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error) {
|
|
ops := common.NewOptions(options)
|
|
return b.getBalance(chainID, ops)
|
|
}
|
|
|
|
func (b *txBuilder) NewBaseTx(
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.BaseTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.backend.UTXOAssetID(): b.backend.BaseTxFee(),
|
|
}
|
|
for _, out := range outputs {
|
|
assetID := out.AssetID()
|
|
amountToBurn, err := math.Add64(toBurn[assetID], out.Out.Amount())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
toBurn[assetID] = amountToBurn
|
|
}
|
|
|
|
ops := common.NewOptions(options)
|
|
inputs, changeOutputs, err := b.spend(toBurn, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputs = append(outputs, changeOutputs...)
|
|
lux.SortTransferableOutputs(outputs) // sort the outputs
|
|
|
|
return &txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: b.backend.Context().NetworkID,
|
|
BlockchainID: b.backend.BlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
}}, nil
|
|
}
|
|
|
|
func (b *txBuilder) NewCreateAssetTx(
|
|
name string,
|
|
symbol string,
|
|
denomination byte,
|
|
initialState map[uint32][]verify.State,
|
|
options ...common.Option,
|
|
) (*txs.CreateAssetTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.backend.UTXOAssetID(): b.backend.CreateAssetTxFee(),
|
|
}
|
|
ops := common.NewOptions(options)
|
|
inputs, outputs, err := b.spend(toBurn, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
states := make([]*txs.InitialState, 0, len(initialState))
|
|
for fxIndex, outs := range initialState {
|
|
state := &txs.InitialState{
|
|
FxIndex: fxIndex,
|
|
Outs: outs,
|
|
}
|
|
state.Sort() // sort the outputs
|
|
states = append(states, state)
|
|
}
|
|
|
|
tx := &txs.CreateAssetTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: b.backend.Context().NetworkID,
|
|
BlockchainID: b.backend.BlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
}},
|
|
Name: name,
|
|
Symbol: symbol,
|
|
Denomination: denomination,
|
|
States: states,
|
|
}
|
|
utils.Sort(tx.States) // sort the initial states
|
|
return tx, nil
|
|
}
|
|
|
|
func (b *txBuilder) NewOperationTx(
|
|
operations []*txs.Operation,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.backend.UTXOAssetID(): b.backend.BaseTxFee(),
|
|
}
|
|
ops := common.NewOptions(options)
|
|
inputs, outputs, err := b.spend(toBurn, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
txs.SortOperations(operations)
|
|
return &txs.OperationTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: b.backend.Context().NetworkID,
|
|
BlockchainID: b.backend.BlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
}},
|
|
Ops: operations,
|
|
}, nil
|
|
}
|
|
|
|
func (b *txBuilder) NewOperationTxMintFT(
|
|
outputs map[ids.ID]*secp256k1fx.TransferOutput,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error) {
|
|
ops := common.NewOptions(options)
|
|
operations, err := b.mintFTs(outputs, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.NewOperationTx(operations, options...)
|
|
}
|
|
|
|
func (b *txBuilder) NewOperationTxMintNFT(
|
|
assetID ids.ID,
|
|
payload []byte,
|
|
owners []*secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error) {
|
|
ops := common.NewOptions(options)
|
|
operations, err := b.mintNFTs(assetID, payload, owners, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.NewOperationTx(operations, options...)
|
|
}
|
|
|
|
func (b *txBuilder) NewOperationTxMintProperty(
|
|
assetID ids.ID,
|
|
owner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error) {
|
|
ops := common.NewOptions(options)
|
|
operations, err := b.mintProperty(assetID, owner, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.NewOperationTx(operations, options...)
|
|
}
|
|
|
|
func (b *txBuilder) NewOperationTxBurnProperty(
|
|
assetID ids.ID,
|
|
options ...common.Option,
|
|
) (*txs.OperationTx, error) {
|
|
ops := common.NewOptions(options)
|
|
operations, err := b.burnProperty(assetID, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.NewOperationTx(operations, options...)
|
|
}
|
|
|
|
func (b *txBuilder) NewImportTx(
|
|
chainID ids.ID,
|
|
to *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.ImportTx, error) {
|
|
ops := common.NewOptions(options)
|
|
utxos, err := b.backend.UTXOs(ops.Context(), chainID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
addrs = ops.Addresses(b.addrs)
|
|
minIssuanceTime = ops.MinIssuanceTime()
|
|
utxoAssetID = b.backend.UTXOAssetID()
|
|
txFee = b.backend.BaseTxFee()
|
|
|
|
importedInputs = make([]*lux.TransferableInput, 0, len(utxos))
|
|
importedAmounts = make(map[ids.ID]uint64)
|
|
)
|
|
// Iterate over the unlocked UTXOs
|
|
for _, utxo := range utxos {
|
|
out, ok := utxo.Out.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
// Can't import an unknown transfer output type
|
|
continue
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
importedInputs = append(importedInputs, &lux.TransferableInput{
|
|
UTXOID: utxo.UTXOID,
|
|
Asset: utxo.Asset,
|
|
In: &secp256k1fx.TransferInput{
|
|
Amt: out.Amt,
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
},
|
|
})
|
|
|
|
assetID := utxo.AssetID()
|
|
newImportedAmount, err := math.Add64(importedAmounts[assetID], out.Amt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
importedAmounts[assetID] = newImportedAmount
|
|
}
|
|
utils.Sort(importedInputs) // sort imported inputs
|
|
|
|
if len(importedAmounts) == 0 {
|
|
return nil, fmt.Errorf(
|
|
"%w: no UTXOs available to import",
|
|
errInsufficientFunds,
|
|
)
|
|
}
|
|
|
|
var (
|
|
inputs []*lux.TransferableInput
|
|
outputs = make([]*lux.TransferableOutput, 0, len(importedAmounts))
|
|
importedLUX = importedAmounts[utxoAssetID]
|
|
)
|
|
if importedLUX > txFee {
|
|
importedAmounts[utxoAssetID] -= txFee
|
|
} else {
|
|
if importedLUX < txFee { // imported amount goes toward paying tx fee
|
|
toBurn := map[ids.ID]uint64{
|
|
utxoAssetID: txFee - importedLUX,
|
|
}
|
|
var err error
|
|
inputs, outputs, err = b.spend(toBurn, ops)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
|
|
}
|
|
}
|
|
delete(importedAmounts, utxoAssetID)
|
|
}
|
|
|
|
for assetID, amount := range importedAmounts {
|
|
outputs = append(outputs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: assetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amount,
|
|
OutputOwners: *to,
|
|
},
|
|
})
|
|
}
|
|
|
|
lux.SortTransferableOutputs(outputs)
|
|
return &txs.ImportTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: b.backend.Context().NetworkID,
|
|
BlockchainID: b.backend.BlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
}},
|
|
SourceChain: chainID,
|
|
ImportedIns: importedInputs,
|
|
}, nil
|
|
}
|
|
|
|
func (b *txBuilder) NewExportTx(
|
|
chainID ids.ID,
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.ExportTx, error) {
|
|
toBurn := map[ids.ID]uint64{
|
|
b.backend.UTXOAssetID(): b.backend.BaseTxFee(),
|
|
}
|
|
for _, out := range outputs {
|
|
assetID := out.AssetID()
|
|
amountToBurn, err := math.Add64(toBurn[assetID], out.Out.Amount())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
toBurn[assetID] = amountToBurn
|
|
}
|
|
|
|
ops := common.NewOptions(options)
|
|
inputs, changeOutputs, err := b.spend(toBurn, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lux.SortTransferableOutputs(outputs)
|
|
return &txs.ExportTx{
|
|
BaseTx: txs.BaseTx{BaseTx: lux.BaseTx{
|
|
NetworkID: b.backend.Context().NetworkID,
|
|
BlockchainID: b.backend.BlockchainID(),
|
|
Ins: inputs,
|
|
Outs: changeOutputs,
|
|
Memo: ops.Memo(),
|
|
}},
|
|
DestinationChain: chainID,
|
|
ExportedOuts: outputs,
|
|
}, nil
|
|
}
|
|
|
|
func (b *txBuilder) getBalance(
|
|
chainID ids.ID,
|
|
options *common.Options,
|
|
) (
|
|
balance map[ids.ID]uint64,
|
|
err error,
|
|
) {
|
|
utxos, err := b.backend.UTXOs(options.Context(), chainID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addrs := options.Addresses(b.addrs)
|
|
minIssuanceTime := options.MinIssuanceTime()
|
|
balance = make(map[ids.ID]uint64)
|
|
|
|
// Iterate over the UTXOs
|
|
for _, utxo := range utxos {
|
|
outIntf := utxo.Out
|
|
out, ok := outIntf.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
// We only support [secp256k1fx.TransferOutput]s.
|
|
continue
|
|
}
|
|
|
|
_, ok = common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
assetID := utxo.AssetID()
|
|
balance[assetID], err = math.Add64(balance[assetID], out.Amt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return balance, nil
|
|
}
|
|
|
|
func (b *txBuilder) spend(
|
|
amountsToBurn map[ids.ID]uint64,
|
|
options *common.Options,
|
|
) (
|
|
inputs []*lux.TransferableInput,
|
|
outputs []*lux.TransferableOutput,
|
|
err error,
|
|
) {
|
|
utxos, err := b.backend.UTXOs(options.Context(), b.backend.BlockchainID())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
addrs := options.Addresses(b.addrs)
|
|
minIssuanceTime := options.MinIssuanceTime()
|
|
|
|
addr, ok := addrs.Peek()
|
|
if !ok {
|
|
return nil, nil, errNoChangeAddress
|
|
}
|
|
changeOwner := options.ChangeOwner(&secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{addr},
|
|
})
|
|
|
|
// Iterate over the UTXOs
|
|
for _, utxo := range utxos {
|
|
assetID := utxo.AssetID()
|
|
remainingAmountToBurn := amountsToBurn[assetID]
|
|
|
|
// If we have consumed enough of the asset, then we have no need burn
|
|
// more.
|
|
if remainingAmountToBurn == 0 {
|
|
continue
|
|
}
|
|
|
|
outIntf := utxo.Out
|
|
out, ok := outIntf.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
// We only support burning [secp256k1fx.TransferOutput]s.
|
|
continue
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
inputs = append(inputs, &lux.TransferableInput{
|
|
UTXOID: utxo.UTXOID,
|
|
Asset: utxo.Asset,
|
|
In: &secp256k1fx.TransferInput{
|
|
Amt: out.Amt,
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
},
|
|
})
|
|
|
|
// Burn any value that should be burned
|
|
amountToBurn := math.Min(
|
|
remainingAmountToBurn, // Amount we still need to burn
|
|
out.Amt, // Amount available to burn
|
|
)
|
|
amountsToBurn[assetID] -= amountToBurn
|
|
if remainingAmount := out.Amt - amountToBurn; remainingAmount > 0 {
|
|
// This input had extra value, so some of it must be returned
|
|
outputs = append(outputs, &lux.TransferableOutput{
|
|
Asset: utxo.Asset,
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: remainingAmount,
|
|
OutputOwners: *changeOwner,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
for assetID, amount := range amountsToBurn {
|
|
if amount != 0 {
|
|
return nil, nil, fmt.Errorf(
|
|
"%w: provided UTXOs need %d more units of asset %q",
|
|
errInsufficientFunds,
|
|
amount,
|
|
assetID,
|
|
)
|
|
}
|
|
}
|
|
|
|
utils.Sort(inputs) // sort inputs
|
|
lux.SortTransferableOutputs(outputs) // sort the change outputs
|
|
return inputs, outputs, nil
|
|
}
|
|
|
|
func (b *txBuilder) mintFTs(
|
|
outputs map[ids.ID]*secp256k1fx.TransferOutput,
|
|
options *common.Options,
|
|
) (
|
|
operations []*txs.Operation,
|
|
err error,
|
|
) {
|
|
utxos, err := b.backend.UTXOs(options.Context(), b.backend.BlockchainID())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addrs := options.Addresses(b.addrs)
|
|
minIssuanceTime := options.MinIssuanceTime()
|
|
|
|
for _, utxo := range utxos {
|
|
assetID := utxo.AssetID()
|
|
output, ok := outputs[assetID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
out, ok := utxo.Out.(*secp256k1fx.MintOutput)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// add the operation to the array
|
|
operations = append(operations, &txs.Operation{
|
|
Asset: utxo.Asset,
|
|
UTXOIDs: []*lux.UTXOID{&utxo.UTXOID},
|
|
Op: &secp256k1fx.MintOperation{
|
|
MintInput: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
MintOutput: *out,
|
|
TransferOutput: *output,
|
|
},
|
|
})
|
|
|
|
// remove the asset from the required outputs to mint
|
|
delete(outputs, assetID)
|
|
}
|
|
|
|
for assetID := range outputs {
|
|
return nil, fmt.Errorf(
|
|
"%w: provided UTXOs not able to mint asset %q",
|
|
errInsufficientFunds,
|
|
assetID,
|
|
)
|
|
}
|
|
return operations, nil
|
|
}
|
|
|
|
func (b *txBuilder) mintNFTs(
|
|
assetID ids.ID,
|
|
payload []byte,
|
|
owners []*secp256k1fx.OutputOwners,
|
|
options *common.Options,
|
|
) (
|
|
operations []*txs.Operation,
|
|
err error,
|
|
) {
|
|
utxos, err := b.backend.UTXOs(options.Context(), b.backend.BlockchainID())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addrs := options.Addresses(b.addrs)
|
|
minIssuanceTime := options.MinIssuanceTime()
|
|
|
|
for _, utxo := range utxos {
|
|
if assetID != utxo.AssetID() {
|
|
continue
|
|
}
|
|
|
|
out, ok := utxo.Out.(*nftfx.MintOutput)
|
|
if !ok {
|
|
// wrong output type
|
|
continue
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// add the operation to the array
|
|
operations = append(operations, &txs.Operation{
|
|
Asset: lux.Asset{ID: assetID},
|
|
UTXOIDs: []*lux.UTXOID{
|
|
&utxo.UTXOID,
|
|
},
|
|
Op: &nftfx.MintOperation{
|
|
MintInput: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
GroupID: out.GroupID,
|
|
Payload: payload,
|
|
Outputs: owners,
|
|
},
|
|
})
|
|
return operations, nil
|
|
}
|
|
return nil, fmt.Errorf(
|
|
"%w: provided UTXOs not able to mint NFT %q",
|
|
errInsufficientFunds,
|
|
assetID,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) mintProperty(
|
|
assetID ids.ID,
|
|
owner *secp256k1fx.OutputOwners,
|
|
options *common.Options,
|
|
) (
|
|
operations []*txs.Operation,
|
|
err error,
|
|
) {
|
|
utxos, err := b.backend.UTXOs(options.Context(), b.backend.BlockchainID())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addrs := options.Addresses(b.addrs)
|
|
minIssuanceTime := options.MinIssuanceTime()
|
|
|
|
for _, utxo := range utxos {
|
|
if assetID != utxo.AssetID() {
|
|
continue
|
|
}
|
|
|
|
out, ok := utxo.Out.(*propertyfx.MintOutput)
|
|
if !ok {
|
|
// wrong output type
|
|
continue
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// add the operation to the array
|
|
operations = append(operations, &txs.Operation{
|
|
Asset: lux.Asset{ID: assetID},
|
|
UTXOIDs: []*lux.UTXOID{
|
|
&utxo.UTXOID,
|
|
},
|
|
Op: &propertyfx.MintOperation{
|
|
MintInput: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
MintOutput: *out,
|
|
OwnedOutput: propertyfx.OwnedOutput{
|
|
OutputOwners: *owner,
|
|
},
|
|
},
|
|
})
|
|
return operations, nil
|
|
}
|
|
return nil, fmt.Errorf(
|
|
"%w: provided UTXOs not able to mint property %q",
|
|
errInsufficientFunds,
|
|
assetID,
|
|
)
|
|
}
|
|
|
|
func (b *txBuilder) burnProperty(
|
|
assetID ids.ID,
|
|
options *common.Options,
|
|
) (
|
|
operations []*txs.Operation,
|
|
err error,
|
|
) {
|
|
utxos, err := b.backend.UTXOs(options.Context(), b.backend.BlockchainID())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addrs := options.Addresses(b.addrs)
|
|
minIssuanceTime := options.MinIssuanceTime()
|
|
|
|
for _, utxo := range utxos {
|
|
if assetID != utxo.AssetID() {
|
|
continue
|
|
}
|
|
|
|
out, ok := utxo.Out.(*propertyfx.OwnedOutput)
|
|
if !ok {
|
|
// wrong output type
|
|
continue
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// add the operation to the array
|
|
operations = append(operations, &txs.Operation{
|
|
Asset: lux.Asset{ID: assetID},
|
|
UTXOIDs: []*lux.UTXOID{
|
|
&utxo.UTXOID,
|
|
},
|
|
Op: &propertyfx.BurnOperation{
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if len(operations) == 0 {
|
|
return nil, fmt.Errorf(
|
|
"%w: provided UTXOs not able to burn property %q",
|
|
errInsufficientFunds,
|
|
assetID,
|
|
)
|
|
}
|
|
return operations, nil
|
|
}
|