Files
node/wallet/chain/p/builder.go
T
zeekayandHanzo Dev c44cc2289f platformvm builds GREEN off the codec: executor fold + full consumer flip
vms/platformvm/... build + vet PASS with pcodecs/txs.Codec gone from the VM.

Executor semantics (standard_tx_executor):
- registerOwnSet(): shared primitive — per-validator state.L1Validator with
  native owner blobs (txs.MarshalOwner/UnmarshalOwner, no codec), active-set
  capacity check, EndAccumulatedFee=balance+accruedFees, SetNetToL1Conversion
  manager-authority recording (byte-for-byte legacy tail).
- ConvertNetworkTx: promote endomorphism (owner-authorized, folds old
  ConvertNetworkToL1Tx), gated by security.Mode.Manager.
- CreateNetworkTx: base (AddNet/SetNetOwner) + sovereign path registers own set.
- Deleted CreateSovereignL1Tx + ConvertNetworkToL1Tx.
- txs.UnmarshalOwner added: canonical owner marshal/unmarshal pair, no codec.

All ~13 txs.Visitor impls carry ConvertNetworkTx; gossip/block Parse(b) codec-free;
wallet/network/primary off txs.Codec.

KNOWN GAP (pending design decision, NOT silently green): CreateNetworkTx reads
tx.Chains() only to derive managerChainID — it does NOT create the genesis
chains (no AddChain). Atomic-spawn-with-chains vs decomplect-to-CreateChainTx is
the open call. 17 codec-era _test.go parked as .bak for follow-up rewrite.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 14:25:08 -07:00

1135 lines
32 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package p
import (
"errors"
"fmt"
"time"
stdcontext "context"
"github.com/luxfi/constants"
"github.com/luxfi/ids"
"github.com/luxfi/math"
"github.com/luxfi/math/set"
"github.com/luxfi/node/vms/platformvm/security"
"github.com/luxfi/node/vms/platformvm/signer"
"github.com/luxfi/node/vms/platformvm/stakeable"
"github.com/luxfi/node/vms/platformvm/txs"
"github.com/luxfi/node/wallet/chain/p/builder"
"github.com/luxfi/node/wallet/network/primary/common"
"github.com/luxfi/utils"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
)
var (
errNoChangeAddress = errors.New("no possible change address")
errWrongTxType = errors.New("wrong tx type")
errUnknownOwnerType = errors.New("unknown owner type")
errInsufficientAuthorization = errors.New("insufficient authorization")
errInsufficientFunds = errors.New("insufficient funds")
_ Builder = (*txBuilder)(nil)
)
// Builder provides a convenient interface for building unsigned P-chain
// transactions.
type Builder interface {
// GetBalance calculates the amount of each asset that this builder has
// control over.
GetBalance(
options ...common.Option,
) (map[ids.ID]uint64, error)
// GetImportableBalance calculates the amount of each 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)
// NewAddValidatorTx creates a new validator of the primary network.
//
// - [vdr] specifies all the details of the validation period such as the
// startTime, endTime, stake weight, and nodeID.
// - [rewardsOwner] specifies the owner of all the rewards this validator
// may accrue during its validation period.
// - [shares] specifies the fraction (out of 1,000,000) that this validator
// will take from delegation rewards. If 1,000,000 is provided, 100% of
// the delegation reward will be sent to the validator's [rewardsOwner].
NewAddValidatorTx(
vdr *txs.Validator,
rewardsOwner *secp256k1fx.OutputOwners,
shares uint32,
options ...common.Option,
) (*txs.AddValidatorTx, error)
// NewAddChainValidatorTx builds an unsigned legacy per-chain
// validator registration tx.
//
// - [vdr] specifies all the details of the validation period such as the
// startTime, endTime, sampling weight, nodeID, and netID.
//
// Deprecated: Use NewAddValidatorTx. Under LP-018 sovereign-L1,
// validators join a network — never a chain. A sovereign L1 IS a
// primary network at its own networkID. Retained for one release
// cycle for wire/codec compat with pre-LP-018 binaries.
NewAddChainValidatorTx(
vdr *txs.ChainValidator,
options ...common.Option,
) (*txs.AddChainValidatorTx, error)
// NewRemoveChainValidatorTx removes [nodeID] from the validator
// set [netID].
NewRemoveChainValidatorTx(
nodeID ids.NodeID,
netID ids.ID,
options ...common.Option,
) (*txs.RemoveChainValidatorTx, error)
// NewAddDelegatorTx creates a new delegator to a validator on the primary
// network.
//
// - [vdr] specifies all the details of the delegation period such as the
// startTime, endTime, stake weight, and validator's nodeID.
// - [rewardsOwner] specifies the owner of all the rewards this delegator
// may accrue at the end of its delegation period.
NewAddDelegatorTx(
vdr *txs.Validator,
rewardsOwner *secp256k1fx.OutputOwners,
options ...common.Option,
) (*txs.AddDelegatorTx, error)
// NewCreateChainTx creates a new chain in the named chain.
//
// - [netID] specifies the net to launch the chain in.
// - [genesis] specifies the initial state of the new chain.
// - [vmID] specifies the vm that the new chain will run.
// - [fxIDs] specifies all the feature extensions that the vm should be
// running with.
// - [chainName] specifies a human readable name for the chain.
NewCreateChainTx(
netID ids.ID,
genesis []byte,
vmID ids.ID,
fxIDs []ids.ID,
chainName string,
options ...common.Option,
) (*txs.CreateChainTx, error)
// NewCreateNetworkTx creates a new network with the specified owner.
//
// - [owner] specifies who has the ability to create new chains and add new
// validators to the network.
NewCreateNetworkTx(
owner *secp256k1fx.OutputOwners,
options ...common.Option,
) (*txs.CreateNetworkTx, 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)
// NewTransformChainTx creates a transform net transaction that attempts
// to convert the provided [netID] from a permissioned net to a
// permissionless chain. This transaction will convert
// [maxSupply] - [initialSupply] of [assetID] to staking rewards.
//
// - [netID] specifies the net to transform.
// - [assetID] specifies the asset to use to reward stakers on the chain.
// - [initialSupply] is the amount of [assetID] that will be in circulation
// after this transaction is accepted.
// - [maxSupply] is the maximum total amount of [assetID] that should ever
// exist.
// - [minConsumptionRate] is the rate that a staker will receive rewards
// if they stake with a duration of 0.
// - [maxConsumptionRate] is the maximum rate that staking rewards should be
// consumed from the reward pool per year.
// - [minValidatorStake] is the minimum amount of funds required to become a
// validator.
// - [maxValidatorStake] is the maximum amount of funds a single validator
// can be allocated, including delegated funds.
// - [minStakeDuration] is the minimum number of seconds a staker can stake
// for.
// - [maxStakeDuration] is the maximum number of seconds a staker can stake
// for.
// - [minValidatorStake] is the minimum amount of funds required to become a
// delegator.
// - [maxValidatorWeightFactor] is the factor which calculates the maximum
// amount of delegation a validator can receive. A value of 1 effectively
// disables delegation.
// - [uptimeRequirement] is the minimum percentage a validator must be
// online and responsive to receive a reward.
NewTransformChainTx(
netID ids.ID,
assetID ids.ID,
initialSupply uint64,
maxSupply uint64,
minConsumptionRate uint64,
maxConsumptionRate uint64,
minValidatorStake uint64,
maxValidatorStake uint64,
minStakeDuration time.Duration,
maxStakeDuration time.Duration,
minDelegationFee uint32,
minDelegatorStake uint64,
maxValidatorWeightFactor byte,
uptimeRequirement uint32,
options ...common.Option,
) (*txs.TransformChainTx, error)
// NewAddPermissionlessValidatorTx creates a new validator of the specified
// chain.
//
// - [vdr] specifies all the details of the validation period such as the
// netID, startTime, endTime, stake weight, and nodeID.
// - [signer] if the netID is the primary network, this is the BLS key
// for this validator. Otherwise, this value should be the empty signer.
// - [assetID] specifies the asset to stake.
// - [validationRewardsOwner] specifies the owner of all the rewards this
// validator earns for its validation period.
// - [delegationRewardsOwner] specifies the owner of all the rewards this
// validator earns for delegations during its validation period.
// - [shares] specifies the fraction (out of 1,000,000) that this validator
// will take from delegation rewards. If 1,000,000 is provided, 100% of
// the delegation reward will be sent to the validator's [rewardsOwner].
NewAddPermissionlessValidatorTx(
vdr *txs.ChainValidator,
signer signer.Signer,
assetID ids.ID,
validationRewardsOwner *secp256k1fx.OutputOwners,
delegationRewardsOwner *secp256k1fx.OutputOwners,
shares uint32,
options ...common.Option,
) (*txs.AddPermissionlessValidatorTx, error)
// NewAddPermissionlessDelegatorTx creates a new delegator of the specified
// net on the specified nodeID.
//
// - [vdr] specifies all the details of the delegation period such as the
// netID, startTime, endTime, stake weight, and nodeID.
// - [assetID] specifies the asset to stake.
// - [rewardsOwner] specifies the owner of all the rewards this delegator
// earns during its delegation period.
NewAddPermissionlessDelegatorTx(
vdr *txs.ChainValidator,
assetID ids.ID,
rewardsOwner *secp256k1fx.OutputOwners,
options ...common.Option,
) (*txs.AddPermissionlessDelegatorTx, error)
}
// BuilderBackend specifies the required information needed to build unsigned
// P-chain transactions.
type BuilderBackend interface {
builder.Backend
GetTx(ctx stdcontext.Context, txID ids.ID) (*txs.Tx, error)
}
type txBuilder struct {
addrs set.Set[ids.ShortID]
context *builder.Context
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.
// - [context] provides the chain's configuration.
// - [backend] provides the required access to the chain's state
// to build out the transactions.
func NewBuilder(addrs set.Set[ids.ShortID], context *builder.Context, backend BuilderBackend) Builder {
return &txBuilder{
addrs: addrs,
context: context,
backend: backend,
}
}
func (b *txBuilder) GetBalance(
options ...common.Option,
) (map[ids.ID]uint64, error) {
ops := common.NewOptions(options)
return b.getBalance(constants.PlatformChainID, 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.context.UTXOAssetID: b.context.StaticFeeConfig.TxFee,
}
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
}
toStake := map[ids.ID]uint64{}
ops := common.NewOptions(options)
inputs, changeOutputs, _, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
outputs = append(outputs, changeOutputs...)
lux.SortTransferableOutputs(outputs) // sort the outputs
return txs.NewBaseTx(&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: outputs,
Memo: ops.Memo(),
})
}
func (b *txBuilder) NewAddValidatorTx(
vdr *txs.Validator,
rewardsOwner *secp256k1fx.OutputOwners,
shares uint32,
options ...common.Option,
) (*txs.AddValidatorTx, error) {
utxoAssetID := b.context.UTXOAssetID
toBurn := map[ids.ID]uint64{
utxoAssetID: b.context.StaticFeeConfig.AddNetworkValidatorFee,
}
toStake := map[ids.ID]uint64{
utxoAssetID: vdr.Wght,
}
ops := common.NewOptions(options)
inputs, baseOutputs, stakeOutputs, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
utils.Sort(rewardsOwner.Addrs)
return txs.NewAddValidatorTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: baseOutputs,
Memo: ops.Memo(),
},
*vdr,
stakeOutputs,
rewardsOwner,
shares,
)
}
func (b *txBuilder) NewAddChainValidatorTx(
vdr *txs.ChainValidator,
options ...common.Option,
) (*txs.AddChainValidatorTx, error) {
toBurn := map[ids.ID]uint64{
b.context.UTXOAssetID: b.context.StaticFeeConfig.AddChainValidatorFee,
}
toStake := map[ids.ID]uint64{}
ops := common.NewOptions(options)
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
chainAuth, err := b.authorizeNet(vdr.Chain, ops)
if err != nil {
return nil, err
}
return txs.NewAddChainValidatorTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: outputs,
Memo: ops.Memo(),
},
vdr.Validator,
vdr.Chain,
chainAuth,
)
}
func (b *txBuilder) NewRemoveChainValidatorTx(
nodeID ids.NodeID,
netID ids.ID,
options ...common.Option,
) (*txs.RemoveChainValidatorTx, error) {
toBurn := map[ids.ID]uint64{
b.context.UTXOAssetID: b.context.StaticFeeConfig.TxFee,
}
toStake := map[ids.ID]uint64{}
ops := common.NewOptions(options)
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
chainAuth, err := b.authorizeNet(netID, ops)
if err != nil {
return nil, err
}
return txs.NewRemoveChainValidatorTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: outputs,
Memo: ops.Memo(),
},
nodeID,
netID,
chainAuth,
)
}
func (b *txBuilder) NewAddDelegatorTx(
vdr *txs.Validator,
rewardsOwner *secp256k1fx.OutputOwners,
options ...common.Option,
) (*txs.AddDelegatorTx, error) {
utxoAssetID := b.context.UTXOAssetID
toBurn := map[ids.ID]uint64{
utxoAssetID: b.context.StaticFeeConfig.AddNetworkDelegatorFee,
}
toStake := map[ids.ID]uint64{
b.context.UTXOAssetID: vdr.Wght,
}
ops := common.NewOptions(options)
inputs, baseOutputs, stakeOutputs, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
utils.Sort(rewardsOwner.Addrs)
return txs.NewAddDelegatorTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: baseOutputs,
Memo: ops.Memo(),
},
*vdr,
stakeOutputs,
rewardsOwner,
)
}
func (b *txBuilder) NewCreateChainTx(
netID ids.ID,
genesis []byte,
vmID ids.ID,
fxIDs []ids.ID,
chainName string,
options ...common.Option,
) (*txs.CreateChainTx, error) {
toBurn := map[ids.ID]uint64{
b.context.UTXOAssetID: b.context.StaticFeeConfig.CreateChainTxFee,
}
toStake := map[ids.ID]uint64{}
ops := common.NewOptions(options)
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
chainAuth, err := b.authorizeNet(netID, ops)
if err != nil {
return nil, err
}
utils.Sort(fxIDs)
return txs.NewCreateChainTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: outputs,
Memo: ops.Memo(),
},
netID,
chainName,
vmID,
fxIDs,
genesis,
chainAuth,
)
}
func (b *txBuilder) NewCreateNetworkTx(
owner *secp256k1fx.OutputOwners,
options ...common.Option,
) (*txs.CreateNetworkTx, error) {
toBurn := map[ids.ID]uint64{
b.context.UTXOAssetID: b.context.StaticFeeConfig.CreateNetworkTxFee,
}
toStake := map[ids.ID]uint64{}
ops := common.NewOptions(options)
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
utils.Sort(owner.Addrs)
// Plain "create an empty network under the primary network": no own
// validators or chains, no manager. Parent = primary network;
// security inherited (restaked from parent).
return txs.NewCreateNetworkTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: outputs,
Memo: ops.Memo(),
},
constants.PrimaryNetworkID, // parent
owner,
security.Mode{RestakeParent: true, Manager: security.PChain}, // restaked L2 under primary
nil, // validators
nil, // chains
0, // managerChainIdx
nil, // managerAddress
)
}
func (b *txBuilder) NewImportTx(
sourceChainID ids.ID,
to *secp256k1fx.OutputOwners,
options ...common.Option,
) (*txs.ImportTx, error) {
ops := common.NewOptions(options)
utxos, err := b.backend.UTXOs(ops.Context(), sourceChainID)
if err != nil {
return nil, err
}
var (
addrs = ops.Addresses(b.addrs)
minIssuanceTime = ops.MinIssuanceTime()
utxoAssetID = b.context.UTXOAssetID
txFee = b.context.StaticFeeConfig.TxFee
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 {
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(importedInputs) == 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,
}
toStake := map[ids.ID]uint64{}
var err error
inputs, outputs, _, err = b.spend(toBurn, toStake, 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) // sort imported outputs
return txs.NewImportTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: outputs,
Memo: ops.Memo(),
},
sourceChainID,
importedInputs,
)
}
func (b *txBuilder) NewExportTx(
chainID ids.ID,
outputs []*lux.TransferableOutput,
options ...common.Option,
) (*txs.ExportTx, error) {
toBurn := map[ids.ID]uint64{
b.context.UTXOAssetID: b.context.StaticFeeConfig.TxFee,
}
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
}
toStake := map[ids.ID]uint64{}
ops := common.NewOptions(options)
inputs, changeOutputs, _, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
lux.SortTransferableOutputs(outputs) // sort exported outputs
return txs.NewExportTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: changeOutputs,
Memo: ops.Memo(),
},
chainID,
outputs,
)
}
func (b *txBuilder) NewTransformChainTx(
netID ids.ID,
assetID ids.ID,
initialSupply uint64,
maxSupply uint64,
minConsumptionRate uint64,
maxConsumptionRate uint64,
minValidatorStake uint64,
maxValidatorStake uint64,
minStakeDuration time.Duration,
maxStakeDuration time.Duration,
minDelegationFee uint32,
minDelegatorStake uint64,
maxValidatorWeightFactor byte,
uptimeRequirement uint32,
options ...common.Option,
) (*txs.TransformChainTx, error) {
toBurn := map[ids.ID]uint64{
b.context.UTXOAssetID: b.context.StaticFeeConfig.TransformChainTxFee,
assetID: maxSupply - initialSupply,
}
toStake := map[ids.ID]uint64{}
ops := common.NewOptions(options)
inputs, outputs, _, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
chainAuth, err := b.authorizeNet(netID, ops)
if err != nil {
return nil, err
}
return txs.NewTransformChainTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: outputs,
Memo: ops.Memo(),
},
netID,
assetID,
initialSupply,
maxSupply,
minConsumptionRate,
maxConsumptionRate,
minValidatorStake,
maxValidatorStake,
uint32(minStakeDuration/time.Second),
uint32(maxStakeDuration/time.Second),
minDelegationFee,
minDelegatorStake,
maxValidatorWeightFactor,
uptimeRequirement,
chainAuth,
)
}
func (b *txBuilder) NewAddPermissionlessValidatorTx(
vdr *txs.ChainValidator,
signer signer.Signer,
assetID ids.ID,
validationRewardsOwner *secp256k1fx.OutputOwners,
delegationRewardsOwner *secp256k1fx.OutputOwners,
shares uint32,
options ...common.Option,
) (*txs.AddPermissionlessValidatorTx, error) {
utxoAssetID := b.context.UTXOAssetID
toBurn := map[ids.ID]uint64{}
if vdr.Chain == constants.PrimaryNetworkID {
toBurn[utxoAssetID] = b.context.StaticFeeConfig.AddNetworkValidatorFee
} else {
toBurn[utxoAssetID] = b.context.StaticFeeConfig.AddChainValidatorFee
}
toStake := map[ids.ID]uint64{
assetID: vdr.Wght,
}
ops := common.NewOptions(options)
inputs, baseOutputs, stakeOutputs, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
utils.Sort(validationRewardsOwner.Addrs)
utils.Sort(delegationRewardsOwner.Addrs)
return txs.NewAddPermissionlessValidatorTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: baseOutputs,
Memo: ops.Memo(),
},
vdr.Validator,
vdr.Chain,
signer,
stakeOutputs,
validationRewardsOwner,
delegationRewardsOwner,
shares,
)
}
func (b *txBuilder) NewAddPermissionlessDelegatorTx(
vdr *txs.ChainValidator,
assetID ids.ID,
rewardsOwner *secp256k1fx.OutputOwners,
options ...common.Option,
) (*txs.AddPermissionlessDelegatorTx, error) {
utxoAssetID := b.context.UTXOAssetID
toBurn := map[ids.ID]uint64{}
if vdr.Chain == constants.PrimaryNetworkID {
toBurn[utxoAssetID] = b.context.StaticFeeConfig.AddNetworkDelegatorFee
} else {
toBurn[utxoAssetID] = b.context.StaticFeeConfig.AddChainDelegatorFee
}
toStake := map[ids.ID]uint64{
assetID: vdr.Wght,
}
ops := common.NewOptions(options)
inputs, baseOutputs, stakeOutputs, err := b.spend(toBurn, toStake, ops)
if err != nil {
return nil, err
}
utils.Sort(rewardsOwner.Addrs)
return txs.NewAddPermissionlessDelegatorTx(
&lux.BaseTx{
NetworkID: b.context.NetworkID,
BlockchainID: constants.PlatformChainID,
Ins: inputs,
Outs: baseOutputs,
Memo: ops.Memo(),
},
vdr.Validator,
vdr.Chain,
stakeOutputs,
rewardsOwner,
)
}
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
if lockedOut, ok := outIntf.(*stakeable.LockOut); ok {
if !options.AllowStakeableLocked() && lockedOut.Locktime > minIssuanceTime {
// This output is currently locked, so this output can't be
// burned.
continue
}
outIntf = lockedOut.TransferableOut
}
out, ok := outIntf.(*secp256k1fx.TransferOutput)
if !ok {
return nil, errUnknownOutputType
}
_, 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
}
// spend takes in the requested burn amounts and the requested stake amounts.
//
// - [amountsToBurn] maps assetID to the amount of the asset to spend without
// producing an output. This is typically used for fees. However, it can
// also be used to consume some of an asset that will be produced in
// separate outputs, such as ExportedOutputs. Only unlocked UTXOs are able
// to be burned here.
// - [amountsToStake] maps assetID to the amount of the asset to spend and
// place into the staked outputs. First locked UTXOs are attempted to be
// used for these funds, and then unlocked UTXOs will be attempted to be
// used. There is no preferential ordering on the unlock times.
func (b *txBuilder) spend(
amountsToBurn map[ids.ID]uint64,
amountsToStake map[ids.ID]uint64,
options *common.Options,
) (
inputs []*lux.TransferableInput,
changeOutputs []*lux.TransferableOutput,
stakeOutputs []*lux.TransferableOutput,
err error,
) {
utxos, err := b.backend.UTXOs(options.Context(), constants.PlatformChainID)
if err != nil {
return nil, nil, nil, err
}
addrs := options.Addresses(b.addrs)
minIssuanceTime := options.MinIssuanceTime()
addr, ok := addrs.Peek()
if !ok {
return nil, nil, nil, errNoChangeAddress
}
changeOwner := options.ChangeOwner(&secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{addr},
})
// Iterate over the locked UTXOs
for _, utxo := range utxos {
assetID := utxo.AssetID()
remainingAmountToStake := amountsToStake[assetID]
// If we have staked enough of the asset, then we have no need burn
// more.
if remainingAmountToStake == 0 {
continue
}
outIntf := utxo.Out
lockedOut, ok := outIntf.(*stakeable.LockOut)
if !ok {
// This output isn't locked, so it will be handled during the next
// iteration of the UTXO set
continue
}
if minIssuanceTime >= lockedOut.Locktime {
// This output isn't locked, so it will be handled during the next
// iteration of the UTXO set
continue
}
out, ok := lockedOut.TransferableOut.(*secp256k1fx.TransferOutput)
if !ok {
return nil, nil, nil, errUnknownOutputType
}
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: &stakeable.LockIn{
Locktime: lockedOut.Locktime,
TransferableIn: &secp256k1fx.TransferInput{
Amt: out.Amt,
Input: secp256k1fx.Input{
SigIndices: inputSigIndices,
},
},
},
})
// Stake any value that should be staked
amountToStake := min(
remainingAmountToStake, // Amount we still need to stake
out.Amt, // Amount available to stake
)
// Add the output to the staked outputs
stakeOutputs = append(stakeOutputs, &lux.TransferableOutput{
Asset: utxo.Asset,
Out: &stakeable.LockOut{
Locktime: lockedOut.Locktime,
TransferableOut: &secp256k1fx.TransferOutput{
Amt: amountToStake,
OutputOwners: out.OutputOwners,
},
},
})
amountsToStake[assetID] -= amountToStake
if remainingAmount := out.Amt - amountToStake; remainingAmount > 0 {
// This input had extra value, so some of it must be returned
changeOutputs = append(changeOutputs, &lux.TransferableOutput{
Asset: utxo.Asset,
Out: &stakeable.LockOut{
Locktime: lockedOut.Locktime,
TransferableOut: &secp256k1fx.TransferOutput{
Amt: remainingAmount,
OutputOwners: out.OutputOwners,
},
},
})
}
}
// Iterate over the unlocked UTXOs
for _, utxo := range utxos {
assetID := utxo.AssetID()
remainingAmountToStake := amountsToStake[assetID]
remainingAmountToBurn := amountsToBurn[assetID]
// If we have consumed enough of the asset, then we have no need burn
// more.
if remainingAmountToStake == 0 && remainingAmountToBurn == 0 {
continue
}
outIntf := utxo.Out
if lockedOut, ok := outIntf.(*stakeable.LockOut); ok {
if lockedOut.Locktime > minIssuanceTime {
// This output is currently locked, so this output can't be
// burned.
continue
}
outIntf = lockedOut.TransferableOut
}
out, ok := outIntf.(*secp256k1fx.TransferOutput)
if !ok {
return nil, nil, nil, errUnknownOutputType
}
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 := min(
remainingAmountToBurn, // Amount we still need to burn
out.Amt, // Amount available to burn
)
amountsToBurn[assetID] -= amountToBurn
amountAvalibleToStake := out.Amt - amountToBurn
// Burn any value that should be burned
amountToStake := min(
remainingAmountToStake, // Amount we still need to stake
amountAvalibleToStake, // Amount available to stake
)
amountsToStake[assetID] -= amountToStake
if amountToStake > 0 {
// Some of this input was put for staking
stakeOutputs = append(stakeOutputs, &lux.TransferableOutput{
Asset: utxo.Asset,
Out: &secp256k1fx.TransferOutput{
Amt: amountToStake,
OutputOwners: *changeOwner,
},
})
}
if remainingAmount := amountAvalibleToStake - amountToStake; remainingAmount > 0 {
// This input had extra value, so some of it must be returned
changeOutputs = append(changeOutputs, &lux.TransferableOutput{
Asset: utxo.Asset,
Out: &secp256k1fx.TransferOutput{
Amt: remainingAmount,
OutputOwners: *changeOwner,
},
})
}
}
for assetID, amount := range amountsToStake {
if amount != 0 {
return nil, nil, nil, fmt.Errorf(
"%w: provided UTXOs need %d more units of asset %q to stake",
errInsufficientFunds,
amount,
assetID,
)
}
}
for assetID, amount := range amountsToBurn {
if amount != 0 {
return nil, nil, nil, fmt.Errorf(
"%w: provided UTXOs need %d more units of asset %q",
errInsufficientFunds,
amount,
assetID,
)
}
}
utils.Sort(inputs) // sort inputs
lux.SortTransferableOutputs(changeOutputs) // sort the change outputs
lux.SortTransferableOutputs(stakeOutputs) // sort stake outputs
return inputs, changeOutputs, stakeOutputs, nil
}
func (b *txBuilder) authorizeNet(netID ids.ID, options *common.Options) (*secp256k1fx.Input, error) {
netTx, err := b.backend.GetTx(options.Context(), netID)
if err != nil {
return nil, fmt.Errorf(
"failed to fetch net %q: %w",
netID,
err,
)
}
network, ok := netTx.Unsigned.(*txs.CreateNetworkTx)
if !ok {
return nil, errWrongTxType
}
owner, ok := network.Owner().(*secp256k1fx.OutputOwners)
if !ok {
return nil, errUnknownOwnerType
}
addrs := options.Addresses(b.addrs)
minIssuanceTime := options.MinIssuanceTime()
inputSigIndices, ok := common.MatchOwners(owner, addrs, minIssuanceTime)
if !ok {
// We can't authorize the chain
return nil, errInsufficientAuthorization
}
return &secp256k1fx.Input{
SigIndices: inputSigIndices,
}, nil
}