mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
The module already called itself github.com/luxfi/node while containing no node in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and the Go column of the matrix measured a VM plugin. A repo that fetches the thing it claims to be has not replaced it, so the Go sources move in — 1515 files, the daemon and everything under it — and the build reaches outside this module for nothing. Rust and C++ sit beside it unchanged and are still measured against it by the differential. That is the point of one repo: the reference implementation and the two ports are the same checkout, so parity is a thing you can run rather than a thing you assert. Verified: builds, and the closure carries no lux-private — checked at build time rather than believed. The daemon starts twelve chains on a local network (P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
2010 lines
50 KiB
Go
2010 lines
50 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package builder
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/node/utils"
|
|
"github.com/luxfi/node/utils/math"
|
|
"github.com/luxfi/node/vms/components/gas"
|
|
"github.com/luxfi/node/vms/components/verify"
|
|
"github.com/luxfi/node/vms/platformvm/fx"
|
|
"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/vms/platformvm/txs/fee"
|
|
"github.com/luxfi/node/wallet/network/primary/common"
|
|
lux "github.com/luxfi/utxo"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
)
|
|
|
|
var (
|
|
ErrNoChangeAddress = errors.New("no possible change address")
|
|
ErrUnknownOutputType = errors.New("unknown output type")
|
|
ErrUnknownOwnerType = errors.New("unknown owner type")
|
|
ErrInsufficientAuthorization = errors.New("insufficient authorization")
|
|
ErrInsufficientFunds = errors.New("insufficient funds")
|
|
|
|
_ Builder = (*builder)(nil)
|
|
)
|
|
|
|
// Builder provides a convenient interface for building unsigned P-chain
|
|
// transactions.
|
|
type Builder interface {
|
|
// Context returns the configuration of the chain that this builder uses to
|
|
// create transactions.
|
|
Context() *Context
|
|
|
|
// 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 network.
|
|
//
|
|
// - [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)
|
|
|
|
// NewTransferChainOwnershipTx changes the owner of the named chain.
|
|
//
|
|
// - [chainID] specifies the chain to be modified
|
|
// - [owner] specifies who has the ability to create new chains and add new
|
|
// validators to the chain.
|
|
NewTransferChainOwnershipTx(
|
|
chainID ids.ID,
|
|
owner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.TransferChainOwnershipTx, error)
|
|
|
|
// NewRegisterL1ValidatorTx adds a validator to an L1.
|
|
//
|
|
// - [balance] that the validator should allocate to continuous fees
|
|
// - [proofOfPossession] is the BLS PoP for the key included in the Warp
|
|
// message
|
|
// - [message] is the Warp message that authorizes this validator to be
|
|
// added
|
|
NewRegisterL1ValidatorTx(
|
|
balance uint64,
|
|
proofOfPossession [bls.SignatureLen]byte,
|
|
message []byte,
|
|
options ...common.Option,
|
|
) (*txs.RegisterL1ValidatorTx, error)
|
|
|
|
// NewSetL1ValidatorWeightTx sets the weight of a validator on an L1.
|
|
//
|
|
// - [message] is the Warp message that authorizes this validator's weight
|
|
// to be changed
|
|
NewSetL1ValidatorWeightTx(
|
|
message []byte,
|
|
options ...common.Option,
|
|
) (*txs.SetL1ValidatorWeightTx, error)
|
|
|
|
// NewIncreaseL1ValidatorBalanceTx increases the balance of a validator on
|
|
// an L1 for the continuous fee.
|
|
// the continuous fee.
|
|
//
|
|
// - [validationID] of the validator
|
|
// - [balance] amount to increase the validator's balance by
|
|
NewIncreaseL1ValidatorBalanceTx(
|
|
validationID ids.ID,
|
|
balance uint64,
|
|
options ...common.Option,
|
|
) (*txs.IncreaseL1ValidatorBalanceTx, error)
|
|
|
|
// NewDisableL1ValidatorTx disables an L1 validator and returns the
|
|
// remaining funds allocated to the continuous fee to the remaining balance
|
|
// owner.
|
|
//
|
|
// - [validationID] of the validator to disable
|
|
NewDisableL1ValidatorTx(
|
|
validationID ids.ID,
|
|
options ...common.Option,
|
|
) (*txs.DisableL1ValidatorTx, 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)
|
|
|
|
// Removed in regenesis
|
|
// // 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)
|
|
|
|
// Note: NewRegisterL1ValidatorTx, NewSetL1ValidatorWeightTx,
|
|
// NewIncreaseL1ValidatorBalanceTx, and NewDisableL1ValidatorTx
|
|
// are already defined above in this interface
|
|
}
|
|
|
|
type Backend interface {
|
|
UTXOs(ctx context.Context, sourceChainID ids.ID) ([]*lux.UTXO, error)
|
|
GetOwner(ctx context.Context, ownerID ids.ID) (fx.Owner, error)
|
|
}
|
|
|
|
type builder struct {
|
|
addrs set.Set[ids.ShortID]
|
|
context *Context
|
|
backend Backend
|
|
}
|
|
|
|
// New 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 chain's state.
|
|
func New(
|
|
addrs set.Set[ids.ShortID],
|
|
context *Context,
|
|
backend Backend,
|
|
) Builder {
|
|
return &builder{
|
|
addrs: addrs,
|
|
context: context,
|
|
backend: backend,
|
|
}
|
|
}
|
|
|
|
// getBlockchainID returns the blockchain ID to use for transactions
|
|
// Returns context.ChainID if set (for tests), otherwise PlatformChainID
|
|
func (b *builder) getBlockchainID() ids.ID {
|
|
if b.context.ChainID != ids.Empty {
|
|
return b.context.ChainID
|
|
}
|
|
return constants.PlatformChainID
|
|
}
|
|
|
|
func (b *builder) Context() *Context {
|
|
return b.context
|
|
}
|
|
|
|
func (b *builder) GetBalance(
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error) {
|
|
ops := common.NewOptions(options)
|
|
return b.getBalance(constants.PlatformChainID, ops)
|
|
}
|
|
|
|
func (b *builder) GetImportableBalance(
|
|
chainID ids.ID,
|
|
options ...common.Option,
|
|
) (map[ids.ID]uint64, error) {
|
|
ops := common.NewOptions(options)
|
|
return b.getBalance(chainID, ops)
|
|
}
|
|
|
|
func (b *builder) NewBaseTx(
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.BaseTx, error) {
|
|
toBurn := map[ids.ID]uint64{}
|
|
for _, out := range outputs {
|
|
assetID := out.AssetID()
|
|
amountToBurn, err := math.Add(toBurn[assetID], out.Out.Amount())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
toBurn[assetID] = amountToBurn
|
|
}
|
|
toStake := map[ids.ID]uint64{}
|
|
|
|
ops := common.NewOptions(options)
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
outputComplexity, err := fee.OutputComplexity(outputs...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicBaseTxComplexities.Add(
|
|
&memoComplexity,
|
|
&outputComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, changeOutputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
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: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
})
|
|
}
|
|
|
|
func (b *builder) NewAddValidatorTx(
|
|
vdr *txs.Validator,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
shares uint32,
|
|
options ...common.Option,
|
|
) (*txs.AddValidatorTx, error) {
|
|
utxoAssetID := b.context.UTXOAssetID
|
|
toBurn := map[ids.ID]uint64{}
|
|
toStake := map[ids.ID]uint64{
|
|
utxoAssetID: vdr.Wght,
|
|
}
|
|
ops := common.NewOptions(options)
|
|
inputs, baseOutputs, stakeOutputs, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
gas.Dimensions{},
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(rewardsOwner.Addrs)
|
|
|
|
return txs.NewAddValidatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: baseOutputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
*vdr,
|
|
stakeOutputs,
|
|
rewardsOwner,
|
|
shares,
|
|
)
|
|
}
|
|
|
|
// Removed in regenesis
|
|
func (b *builder) NewAddChainValidatorTx(
|
|
vdr *txs.ChainValidator,
|
|
options ...common.Option,
|
|
) (*txs.AddChainValidatorTx, error) {
|
|
toBurn := map[ids.ID]uint64{}
|
|
toStake := map[ids.ID]uint64{}
|
|
//
|
|
ops := common.NewOptions(options)
|
|
chainAuth, err := b.authorize(vdr.Chain, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
authComplexity, err := fee.AuthComplexity(chainAuth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicAddChainValidatorTxComplexities.Add(
|
|
&memoComplexity,
|
|
&authComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
return txs.NewAddChainValidatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
},
|
|
vdr.Validator,
|
|
vdr.Chain,
|
|
chainAuth,
|
|
)
|
|
}
|
|
|
|
// Removed in regenesis
|
|
func (b *builder) NewRemoveChainValidatorTx(
|
|
nodeID ids.NodeID,
|
|
netID ids.ID,
|
|
options ...common.Option,
|
|
) (*txs.RemoveChainValidatorTx, error) {
|
|
toBurn := map[ids.ID]uint64{}
|
|
toStake := map[ids.ID]uint64{}
|
|
//
|
|
ops := common.NewOptions(options)
|
|
chainAuth, err := b.authorize(netID, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
authComplexity, err := fee.AuthComplexity(chainAuth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicRemoveChainValidatorTxComplexities.Add(
|
|
&memoComplexity,
|
|
&authComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
return txs.NewRemoveChainValidatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
nodeID,
|
|
netID,
|
|
chainAuth,
|
|
)
|
|
}
|
|
|
|
func (b *builder) NewAddDelegatorTx(
|
|
vdr *txs.Validator,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.AddDelegatorTx, error) {
|
|
utxoAssetID := b.context.UTXOAssetID
|
|
toBurn := map[ids.ID]uint64{}
|
|
toStake := map[ids.ID]uint64{
|
|
utxoAssetID: vdr.Wght,
|
|
}
|
|
ops := common.NewOptions(options)
|
|
inputs, baseOutputs, stakeOutputs, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
gas.Dimensions{},
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(rewardsOwner.Addrs)
|
|
return txs.NewAddDelegatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: baseOutputs,
|
|
Memo: ops.Memo(),
|
|
},
|
|
*vdr,
|
|
stakeOutputs,
|
|
rewardsOwner,
|
|
)
|
|
}
|
|
|
|
func (b *builder) 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{}
|
|
toStake := map[ids.ID]uint64{}
|
|
|
|
ops := common.NewOptions(options)
|
|
chainAuth, err := b.authorize(netID, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
memo := ops.Memo()
|
|
bandwidth, err := math.Mul(uint64(len(fxIDs)), ids.IDLen)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bandwidth, err = math.Add(bandwidth, uint64(len(chainName)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bandwidth, err = math.Add(bandwidth, uint64(len(genesis)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bandwidth, err = math.Add(bandwidth, uint64(len(memo)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dynamicComplexity := gas.Dimensions{
|
|
gas.Bandwidth: bandwidth,
|
|
}
|
|
authComplexity, err := fee.AuthComplexity(chainAuth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicCreateChainTxComplexities.Add(
|
|
&dynamicComplexity,
|
|
&authComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(fxIDs)
|
|
return txs.NewCreateChainTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
},
|
|
netID,
|
|
chainName,
|
|
vmID,
|
|
fxIDs,
|
|
genesis,
|
|
chainAuth,
|
|
)
|
|
}
|
|
|
|
func (b *builder) NewCreateNetworkTx(
|
|
owner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.CreateNetworkTx, error) {
|
|
toBurn := map[ids.ID]uint64{}
|
|
toStake := map[ids.ID]uint64{}
|
|
|
|
ops := common.NewOptions(options)
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
ownerComplexity, err := fee.OwnerComplexity(owner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicCreateNetworkTxComplexities.Add(
|
|
&memoComplexity,
|
|
&ownerComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
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: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
},
|
|
constants.PrimaryNetworkID, // parent
|
|
owner,
|
|
security.Mode{RestakeParent: true, Manager: security.PChain}, // restaked L2 under primary
|
|
nil, // validators
|
|
ids.Empty, // managerChainID (P-Chain-governed)
|
|
nil, // managerAddress
|
|
)
|
|
}
|
|
|
|
// Removed in regenesis
|
|
func (b *builder) NewTransferChainOwnershipTx(
|
|
chainID ids.ID,
|
|
owner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.TransferChainOwnershipTx, error) {
|
|
toBurn := map[ids.ID]uint64{}
|
|
toStake := map[ids.ID]uint64{}
|
|
//
|
|
ops := common.NewOptions(options)
|
|
chainAuth, err := b.authorize(chainID, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
authComplexity, err := fee.AuthComplexity(chainAuth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ownerComplexity, err := fee.OwnerComplexity(owner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicTransferChainOwnershipTxComplexities.Add(
|
|
&memoComplexity,
|
|
&authComplexity,
|
|
&ownerComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
utils.Sort(owner.Addrs)
|
|
return txs.NewTransferChainOwnershipTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
},
|
|
chainID,
|
|
chainAuth,
|
|
owner,
|
|
)
|
|
}
|
|
|
|
func (b *builder) NewRegisterL1ValidatorTx(
|
|
balance uint64,
|
|
proofOfPossession [bls.SignatureLen]byte,
|
|
message []byte,
|
|
options ...common.Option,
|
|
) (*txs.RegisterL1ValidatorTx, error) {
|
|
var (
|
|
toBurn = map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: balance,
|
|
}
|
|
toStake = map[ids.ID]uint64{}
|
|
|
|
ops = common.NewOptions(options)
|
|
memo = ops.Memo()
|
|
memoComplexity = gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
)
|
|
warpComplexity, err := fee.WarpComplexity(message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicRegisterL1ValidatorTxComplexities.Add(
|
|
&memoComplexity,
|
|
&warpComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return txs.NewRegisterL1ValidatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
},
|
|
balance,
|
|
proofOfPossession,
|
|
message,
|
|
)
|
|
}
|
|
|
|
func (b *builder) NewSetL1ValidatorWeightTx(
|
|
message []byte,
|
|
options ...common.Option,
|
|
) (*txs.SetL1ValidatorWeightTx, error) {
|
|
var (
|
|
toBurn = map[ids.ID]uint64{}
|
|
toStake = map[ids.ID]uint64{}
|
|
ops = common.NewOptions(options)
|
|
memo = ops.Memo()
|
|
memoComplexity = gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
)
|
|
warpComplexity, err := fee.WarpComplexity(message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicSetL1ValidatorWeightTxComplexities.Add(
|
|
&memoComplexity,
|
|
&warpComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return txs.NewSetL1ValidatorWeightTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
},
|
|
message,
|
|
)
|
|
}
|
|
|
|
func (b *builder) NewIncreaseL1ValidatorBalanceTx(
|
|
validationID ids.ID,
|
|
balance uint64,
|
|
options ...common.Option,
|
|
) (*txs.IncreaseL1ValidatorBalanceTx, error) {
|
|
var (
|
|
toBurn = map[ids.ID]uint64{
|
|
b.context.UTXOAssetID: balance,
|
|
}
|
|
toStake = map[ids.ID]uint64{}
|
|
ops = common.NewOptions(options)
|
|
memo = ops.Memo()
|
|
memoComplexity = gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
)
|
|
complexity, err := fee.IntrinsicIncreaseL1ValidatorBalanceTxComplexities.Add(
|
|
&memoComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return txs.NewIncreaseL1ValidatorBalanceTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
},
|
|
validationID,
|
|
balance,
|
|
)
|
|
}
|
|
|
|
func (b *builder) NewDisableL1ValidatorTx(
|
|
validationID ids.ID,
|
|
options ...common.Option,
|
|
) (*txs.DisableL1ValidatorTx, error) {
|
|
var (
|
|
toBurn = map[ids.ID]uint64{}
|
|
toStake = map[ids.ID]uint64{}
|
|
ops = common.NewOptions(options)
|
|
)
|
|
disableAuth, err := b.authorize(validationID, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
authComplexity, err := fee.AuthComplexity(disableAuth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
complexity, err := fee.IntrinsicDisableL1ValidatorTxComplexities.Add(
|
|
&memoComplexity,
|
|
&authComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return txs.NewDisableL1ValidatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: constants.PlatformChainID,
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
},
|
|
validationID,
|
|
disableAuth,
|
|
)
|
|
}
|
|
|
|
func (b *builder) 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
|
|
|
|
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.Add(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,
|
|
)
|
|
}
|
|
|
|
outputs := make([]*lux.TransferableOutput, 0, len(importedAmounts))
|
|
for assetID, amount := range importedAmounts {
|
|
if assetID == utxoAssetID {
|
|
continue
|
|
}
|
|
|
|
outputs = append(outputs, &lux.TransferableOutput{
|
|
Asset: lux.Asset{ID: assetID},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amount,
|
|
OutputOwners: *to,
|
|
},
|
|
})
|
|
}
|
|
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
inputComplexity, err := fee.InputComplexity(importedInputs...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outputComplexity, err := fee.OutputComplexity(outputs...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicImportTxComplexities.Add(
|
|
&memoComplexity,
|
|
&inputComplexity,
|
|
&outputComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
toBurn = map[ids.ID]uint64{}
|
|
toStake = map[ids.ID]uint64{}
|
|
)
|
|
excessLUX := importedAmounts[utxoAssetID]
|
|
|
|
inputs, changeOutputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
excessLUX,
|
|
complexity,
|
|
to,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
|
|
}
|
|
outputs = append(outputs, changeOutputs...)
|
|
|
|
lux.SortTransferableOutputs(outputs) // sort imported outputs
|
|
return txs.NewImportTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: outputs,
|
|
Memo: memo,
|
|
},
|
|
sourceChainID,
|
|
importedInputs,
|
|
)
|
|
}
|
|
|
|
func (b *builder) NewExportTx(
|
|
chainID ids.ID,
|
|
outputs []*lux.TransferableOutput,
|
|
options ...common.Option,
|
|
) (*txs.ExportTx, error) {
|
|
toBurn := map[ids.ID]uint64{}
|
|
for _, out := range outputs {
|
|
assetID := out.AssetID()
|
|
amountToBurn, err := math.Add(toBurn[assetID], out.Out.Amount())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
toBurn[assetID] = amountToBurn
|
|
}
|
|
|
|
toStake := map[ids.ID]uint64{}
|
|
ops := common.NewOptions(options)
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
outputComplexity, err := fee.OutputComplexity(outputs...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicExportTxComplexities.Add(
|
|
&memoComplexity,
|
|
&outputComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, changeOutputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lux.SortTransferableOutputs(outputs) // sort exported outputs
|
|
return txs.NewExportTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: changeOutputs,
|
|
Memo: memo,
|
|
},
|
|
chainID,
|
|
outputs,
|
|
)
|
|
}
|
|
|
|
// Removed in regenesis
|
|
func (b *builder) 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{
|
|
assetID: maxSupply - initialSupply,
|
|
}
|
|
toStake := map[ids.ID]uint64{}
|
|
//
|
|
ops := common.NewOptions(options)
|
|
chainAuth, err := b.authorize(netID, ops)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
inputs, outputs, _, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
gas.Dimensions{},
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
//
|
|
return txs.NewTransformChainTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
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 *builder) NewAddPermissionlessValidatorTx(
|
|
vdr *txs.ChainValidator,
|
|
signer signer.Signer,
|
|
assetID ids.ID,
|
|
validationRewardsOwner *secp256k1fx.OutputOwners,
|
|
delegationRewardsOwner *secp256k1fx.OutputOwners,
|
|
shares uint32,
|
|
options ...common.Option,
|
|
) (*txs.AddPermissionlessValidatorTx, error) {
|
|
toBurn := map[ids.ID]uint64{}
|
|
toStake := map[ids.ID]uint64{
|
|
assetID: vdr.Wght,
|
|
}
|
|
|
|
ops := common.NewOptions(options)
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
signerComplexity, err := fee.SignerComplexity(signer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
validatorOwnerComplexity, err := fee.OwnerComplexity(validationRewardsOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
delegatorOwnerComplexity, err := fee.OwnerComplexity(delegationRewardsOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicAddPermissionlessValidatorTxComplexities.Add(
|
|
&memoComplexity,
|
|
&signerComplexity,
|
|
&validatorOwnerComplexity,
|
|
&delegatorOwnerComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, baseOutputs, stakeOutputs, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
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: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: baseOutputs,
|
|
Memo: memo,
|
|
},
|
|
vdr.Validator,
|
|
vdr.Chain,
|
|
signer,
|
|
stakeOutputs,
|
|
validationRewardsOwner,
|
|
delegationRewardsOwner,
|
|
shares,
|
|
)
|
|
}
|
|
|
|
func (b *builder) NewAddPermissionlessDelegatorTx(
|
|
vdr *txs.ChainValidator,
|
|
assetID ids.ID,
|
|
rewardsOwner *secp256k1fx.OutputOwners,
|
|
options ...common.Option,
|
|
) (*txs.AddPermissionlessDelegatorTx, error) {
|
|
toBurn := map[ids.ID]uint64{}
|
|
toStake := map[ids.ID]uint64{
|
|
assetID: vdr.Wght,
|
|
}
|
|
|
|
ops := common.NewOptions(options)
|
|
memo := ops.Memo()
|
|
memoComplexity := gas.Dimensions{
|
|
gas.Bandwidth: uint64(len(memo)),
|
|
}
|
|
ownerComplexity, err := fee.OwnerComplexity(rewardsOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
complexity, err := fee.IntrinsicAddPermissionlessDelegatorTxComplexities.Add(
|
|
&memoComplexity,
|
|
&ownerComplexity,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs, baseOutputs, stakeOutputs, err := b.spend(
|
|
toBurn,
|
|
toStake,
|
|
0,
|
|
complexity,
|
|
nil,
|
|
ops,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utils.Sort(rewardsOwner.Addrs)
|
|
return txs.NewAddPermissionlessDelegatorTx(
|
|
&lux.BaseTx{
|
|
NetworkID: b.context.NetworkID,
|
|
BlockchainID: b.getBlockchainID(),
|
|
Ins: inputs,
|
|
Outs: baseOutputs,
|
|
Memo: memo,
|
|
},
|
|
vdr.Validator,
|
|
vdr.Chain,
|
|
stakeOutputs,
|
|
rewardsOwner,
|
|
)
|
|
}
|
|
|
|
func (b *builder) 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.Add(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.
|
|
//
|
|
// - [toBurn] 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.
|
|
// - [toStake] 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.
|
|
// - [excessLUX] contains the amount of extra LUX that spend can produce in
|
|
// the change outputs in addition to the consumed and not burned LUX.
|
|
// - [complexity] contains the currently accrued transaction complexity that
|
|
// will be used to calculate the required fees to be burned.
|
|
// - [ownerOverride] optionally specifies the output owners to use for the
|
|
// unlocked LUX change output if no additional LUX was needed to be
|
|
// burned. If this value is nil, the default change owner is used.
|
|
func (b *builder) spend(
|
|
toBurn map[ids.ID]uint64,
|
|
toStake map[ids.ID]uint64,
|
|
excessLUX uint64,
|
|
complexity gas.Dimensions,
|
|
ownerOverride *secp256k1fx.OutputOwners,
|
|
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},
|
|
})
|
|
if ownerOverride == nil {
|
|
ownerOverride = changeOwner
|
|
}
|
|
|
|
s := spendHelper{
|
|
weights: b.context.ComplexityWeights,
|
|
gasPrice: b.context.GasPrice,
|
|
|
|
toBurn: toBurn,
|
|
toStake: toStake,
|
|
complexity: complexity,
|
|
|
|
// Initialize the return values with empty slices to preserve backward
|
|
// compatibility of the json representation of transactions with no
|
|
// inputs or outputs.
|
|
inputs: make([]*lux.TransferableInput, 0),
|
|
changeOutputs: make([]*lux.TransferableOutput, 0),
|
|
stakeOutputs: make([]*lux.TransferableOutput, 0),
|
|
}
|
|
|
|
utxosByLocktime := splitByLocktime(utxos, minIssuanceTime)
|
|
for _, utxo := range utxosByLocktime.locked {
|
|
assetID := utxo.AssetID()
|
|
if !s.shouldConsumeLockedAsset(assetID) {
|
|
continue
|
|
}
|
|
|
|
out, locktime, err := unwrapOutput(utxo.Out)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
err = s.addInput(&lux.TransferableInput{
|
|
UTXOID: utxo.UTXOID,
|
|
Asset: utxo.Asset,
|
|
In: &stakeable.LockIn{
|
|
Locktime: locktime,
|
|
TransferableIn: &secp256k1fx.TransferInput{
|
|
Amt: out.Amt,
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
excess := s.consumeLockedAsset(assetID, out.Amt)
|
|
err = s.addStakedOutput(&lux.TransferableOutput{
|
|
Asset: utxo.Asset,
|
|
Out: &stakeable.LockOut{
|
|
Locktime: locktime,
|
|
TransferableOut: &secp256k1fx.TransferOutput{
|
|
Amt: out.Amt - excess,
|
|
OutputOwners: out.OutputOwners,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
if excess == 0 {
|
|
continue
|
|
}
|
|
|
|
// This input had extra value, so some of it must be returned
|
|
err = s.addChangeOutput(&lux.TransferableOutput{
|
|
Asset: utxo.Asset,
|
|
Out: &stakeable.LockOut{
|
|
Locktime: locktime,
|
|
TransferableOut: &secp256k1fx.TransferOutput{
|
|
Amt: excess,
|
|
OutputOwners: out.OutputOwners,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
|
|
// Add all the remaining stake amounts assuming unlocked UTXOs.
|
|
for assetID, amount := range s.toStake {
|
|
if amount == 0 {
|
|
continue
|
|
}
|
|
|
|
err = s.addStakedOutput(&lux.TransferableOutput{
|
|
Asset: lux.Asset{
|
|
ID: assetID,
|
|
},
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: amount,
|
|
OutputOwners: *changeOwner,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
|
|
// LUX is handled last to account for fees.
|
|
utxosByUTXOAssetID := splitByAssetID(utxosByLocktime.unlocked, b.context.UTXOAssetID)
|
|
for _, utxo := range utxosByUTXOAssetID.other {
|
|
assetID := utxo.AssetID()
|
|
if !s.shouldConsumeAsset(assetID) {
|
|
continue
|
|
}
|
|
|
|
out, _, err := unwrapOutput(utxo.Out)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
err = s.addInput(&lux.TransferableInput{
|
|
UTXOID: utxo.UTXOID,
|
|
Asset: utxo.Asset,
|
|
In: &secp256k1fx.TransferInput{
|
|
Amt: out.Amt,
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
excess := s.consumeAsset(assetID, out.Amt)
|
|
if excess == 0 {
|
|
continue
|
|
}
|
|
|
|
// This input had extra value, so some of it must be returned
|
|
err = s.addChangeOutput(&lux.TransferableOutput{
|
|
Asset: utxo.Asset,
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: excess,
|
|
OutputOwners: *changeOwner,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
|
|
for _, utxo := range utxosByUTXOAssetID.requested {
|
|
// Check if we already have enough excessLUX to pay fees before adding more inputs.
|
|
// This early exit is crucial for ImportTx where imported inputs may already provide
|
|
// sufficient LUX to cover fees, and we don't want to add unnecessary platform chain inputs.
|
|
if !s.shouldConsumeAsset(b.context.UTXOAssetID) {
|
|
requiredFee, err := s.calculateFee()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
if excessLUX >= requiredFee {
|
|
break
|
|
}
|
|
}
|
|
|
|
out, _, err := unwrapOutput(utxo.Out)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
inputSigIndices, ok := common.MatchOwners(&out.OutputOwners, addrs, minIssuanceTime)
|
|
if !ok {
|
|
// We couldn't spend this UTXO, so we skip to the next one
|
|
continue
|
|
}
|
|
|
|
err = s.addInput(&lux.TransferableInput{
|
|
UTXOID: utxo.UTXOID,
|
|
Asset: utxo.Asset,
|
|
In: &secp256k1fx.TransferInput{
|
|
Amt: out.Amt,
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: inputSigIndices,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
excess := s.consumeAsset(b.context.UTXOAssetID, out.Amt)
|
|
excessLUX, err = math.Add(excessLUX, excess)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// Calculate fee AFTER adding the input to get accurate complexity
|
|
requiredFee, err := s.calculateFee()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// If we don't need to burn or stake additional LUX and we have
|
|
// consumed enough LUX to pay the required fee, we should stop
|
|
// consuming UTXOs.
|
|
if !s.shouldConsumeAsset(b.context.UTXOAssetID) && excessLUX >= requiredFee {
|
|
// If we need to consume additional LUX, we should be returning the
|
|
// change to the change address.
|
|
ownerOverride = changeOwner
|
|
break
|
|
}
|
|
|
|
// If we need to consume additional LUX, we should be returning the
|
|
// change to the change address.
|
|
ownerOverride = changeOwner
|
|
}
|
|
|
|
if err := s.verifyAssetsConsumed(); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
requiredFee, err := s.calculateFee()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
if excessLUX < requiredFee {
|
|
return nil, nil, nil, fmt.Errorf(
|
|
"%w: provided UTXOs needed %d more µLUX (%q)",
|
|
ErrInsufficientFunds,
|
|
requiredFee-excessLUX,
|
|
b.context.UTXOAssetID,
|
|
)
|
|
}
|
|
|
|
// Try to add a change output if we have excess LUX
|
|
secpExcessLUXOutput := &secp256k1fx.TransferOutput{
|
|
Amt: 0, // Populated later if used
|
|
OutputOwners: *ownerOverride,
|
|
}
|
|
excessLUXOutput := &lux.TransferableOutput{
|
|
Asset: lux.Asset{
|
|
ID: b.context.UTXOAssetID,
|
|
},
|
|
Out: secpExcessLUXOutput,
|
|
}
|
|
|
|
// Calculate the complexity of the potential change output
|
|
changeOutputComplexity, err := fee.OutputComplexity(excessLUXOutput)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// Temporarily add the change output complexity to calculate the fee
|
|
tempComplexity, err := s.complexity.Add(&changeOutputComplexity)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
requiredFeeWithChange, err := tempComplexity.ToGas(s.weights)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
feeWithChange, err := requiredFeeWithChange.Cost(s.gasPrice)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
if excessLUX > feeWithChange {
|
|
// It is worth adding the change output
|
|
// Permanently add the complexity
|
|
s.complexity = tempComplexity
|
|
secpExcessLUXOutput.Amt = excessLUX - feeWithChange
|
|
s.changeOutputs = append(s.changeOutputs, excessLUXOutput)
|
|
}
|
|
// If excessLUX <= feeWithChange, we don't add the change output
|
|
// and we don't modify s.complexity (it stays without the change output)
|
|
|
|
utils.Sort(s.inputs) // sort inputs
|
|
lux.SortTransferableOutputs(s.changeOutputs) // sort the change outputs
|
|
lux.SortTransferableOutputs(s.stakeOutputs) // sort stake outputs
|
|
return s.inputs, s.changeOutputs, s.stakeOutputs, nil
|
|
}
|
|
|
|
func (b *builder) authorize(ownerID ids.ID, options *common.Options) (*secp256k1fx.Input, error) {
|
|
ownerIntf, err := b.backend.GetOwner(options.Context(), ownerID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"failed to fetch owner for %q: %w",
|
|
ownerID,
|
|
err,
|
|
)
|
|
}
|
|
owner, ok := ownerIntf.(*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
|
|
}
|
|
|
|
type spendHelper struct {
|
|
weights gas.Dimensions
|
|
gasPrice gas.Price
|
|
|
|
toBurn map[ids.ID]uint64
|
|
toStake map[ids.ID]uint64
|
|
complexity gas.Dimensions
|
|
|
|
inputs []*lux.TransferableInput
|
|
changeOutputs []*lux.TransferableOutput
|
|
stakeOutputs []*lux.TransferableOutput
|
|
}
|
|
|
|
func (s *spendHelper) addInput(input *lux.TransferableInput) error {
|
|
newInputComplexity, err := fee.InputComplexity(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.complexity, err = s.complexity.Add(&newInputComplexity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.inputs = append(s.inputs, input)
|
|
return nil
|
|
}
|
|
|
|
func (s *spendHelper) addChangeOutput(output *lux.TransferableOutput) error {
|
|
s.changeOutputs = append(s.changeOutputs, output)
|
|
return s.addOutputComplexity(output)
|
|
}
|
|
|
|
func (s *spendHelper) addStakedOutput(output *lux.TransferableOutput) error {
|
|
s.stakeOutputs = append(s.stakeOutputs, output)
|
|
return s.addOutputComplexity(output)
|
|
}
|
|
|
|
func (s *spendHelper) addOutputComplexity(output *lux.TransferableOutput) error {
|
|
newOutputComplexity, err := fee.OutputComplexity(output)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.complexity, err = s.complexity.Add(&newOutputComplexity)
|
|
return err
|
|
}
|
|
|
|
func (s *spendHelper) shouldConsumeLockedAsset(assetID ids.ID) bool {
|
|
return s.toStake[assetID] != 0
|
|
}
|
|
|
|
func (s *spendHelper) shouldConsumeAsset(assetID ids.ID) bool {
|
|
return s.toBurn[assetID] != 0 || s.shouldConsumeLockedAsset(assetID)
|
|
}
|
|
|
|
func (s *spendHelper) consumeLockedAsset(assetID ids.ID, amount uint64) uint64 {
|
|
// Stake any value that should be staked
|
|
toStake := min(
|
|
s.toStake[assetID], // Amount we still need to stake
|
|
amount, // Amount available to stake
|
|
)
|
|
s.toStake[assetID] -= toStake
|
|
return amount - toStake
|
|
}
|
|
|
|
func (s *spendHelper) consumeAsset(assetID ids.ID, amount uint64) uint64 {
|
|
// Burn any value that should be burned
|
|
toBurn := min(
|
|
s.toBurn[assetID], // Amount we still need to burn
|
|
amount, // Amount available to burn
|
|
)
|
|
s.toBurn[assetID] -= toBurn
|
|
|
|
// Stake any remaining value that should be staked
|
|
return s.consumeLockedAsset(assetID, amount-toBurn)
|
|
}
|
|
|
|
func (s *spendHelper) calculateFee() (uint64, error) {
|
|
gas, err := s.complexity.ToGas(s.weights)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return gas.Cost(s.gasPrice)
|
|
}
|
|
|
|
func (s *spendHelper) verifyAssetsConsumed() error {
|
|
for assetID, amount := range s.toStake {
|
|
if amount == 0 {
|
|
continue
|
|
}
|
|
|
|
return fmt.Errorf(
|
|
"%w: provided UTXOs need %d more units of asset %q to stake",
|
|
ErrInsufficientFunds,
|
|
amount,
|
|
assetID,
|
|
)
|
|
}
|
|
for assetID, amount := range s.toBurn {
|
|
if amount == 0 {
|
|
continue
|
|
}
|
|
|
|
return fmt.Errorf(
|
|
"%w: provided UTXOs need %d more units of asset %q",
|
|
ErrInsufficientFunds,
|
|
amount,
|
|
assetID,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type utxosByLocktime struct {
|
|
unlocked []*lux.UTXO
|
|
locked []*lux.UTXO
|
|
}
|
|
|
|
// splitByLocktime separates the provided UTXOs into two slices:
|
|
// 1. UTXOs that are unlocked with the provided issuance time
|
|
// 2. UTXOs that are locked with the provided issuance time
|
|
func splitByLocktime(utxos []*lux.UTXO, minIssuanceTime uint64) utxosByLocktime {
|
|
split := utxosByLocktime{
|
|
unlocked: make([]*lux.UTXO, 0, len(utxos)),
|
|
locked: make([]*lux.UTXO, 0, len(utxos)),
|
|
}
|
|
for _, utxo := range utxos {
|
|
if lockedOut, ok := utxo.Out.(*stakeable.LockOut); ok && minIssuanceTime < lockedOut.Locktime {
|
|
split.locked = append(split.locked, utxo)
|
|
} else {
|
|
split.unlocked = append(split.unlocked, utxo)
|
|
}
|
|
}
|
|
return split
|
|
}
|
|
|
|
type utxosByAssetID struct {
|
|
requested []*lux.UTXO
|
|
other []*lux.UTXO
|
|
}
|
|
|
|
// splitByAssetID separates the provided UTXOs into two slices:
|
|
// 1. UTXOs with the provided assetID
|
|
// 2. UTXOs with a different assetID
|
|
func splitByAssetID(utxos []*lux.UTXO, assetID ids.ID) utxosByAssetID {
|
|
split := utxosByAssetID{
|
|
requested: make([]*lux.UTXO, 0, len(utxos)),
|
|
other: make([]*lux.UTXO, 0, len(utxos)),
|
|
}
|
|
for _, utxo := range utxos {
|
|
if utxo.AssetID() == assetID {
|
|
split.requested = append(split.requested, utxo)
|
|
} else {
|
|
split.other = append(split.other, utxo)
|
|
}
|
|
}
|
|
return split
|
|
}
|
|
|
|
// unwrapOutput returns the *secp256k1fx.TransferOutput that was, potentially,
|
|
// wrapped by a *stakeable.LockOut.
|
|
//
|
|
// If the output was stakeable and locked, the locktime is returned. Otherwise,
|
|
// the locktime returned will be 0.
|
|
//
|
|
// If the output is not a, potentially wrapped, *secp256k1fx.TransferOutput, an
|
|
// error is returned.
|
|
func unwrapOutput(output verify.State) (*secp256k1fx.TransferOutput, uint64, error) {
|
|
var locktime uint64
|
|
if lockedOut, ok := output.(*stakeable.LockOut); ok {
|
|
output = lockedOut.TransferableOut
|
|
locktime = lockedOut.Locktime
|
|
}
|
|
|
|
unwrappedOutput, ok := output.(*secp256k1fx.TransferOutput)
|
|
if !ok {
|
|
return nil, 0, ErrUnknownOutputType
|
|
}
|
|
return unwrappedOutput, locktime, nil
|
|
}
|