Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AllotAllMana to txBuilder #569

Merged
merged 3 commits into from
Oct 18, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 88 additions & 60 deletions builder/transaction_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,42 +146,62 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ
return b
}

minManalockedSlot := b.transaction.CreationSlot + 2*b.api.ProtocolParameters().MaxCommittableAge()
if storedManaOutputIndex >= len(b.transaction.Outputs) {
return setBuildError(ierrors.Errorf("given storedManaOutputIndex does not exist: %d", storedManaOutputIndex))
}

// check if the output is locked for a certain time to an account.
hasManalockCondition := func(output iotago.Output) (iotago.AccountID, bool) {
if !output.UnlockConditionSet().HasTimelockUntil(minManalockedSlot) {
return iotago.EmptyAccountID, false
}
// calculate the minimum required mana to issue the block
minRequiredMana, err := b.MinRequiredAllotedMana(b.api.ProtocolParameters().WorkScoreStructure(), rmc, blockIssuerAccountID)
if err != nil {
return setBuildError(ierrors.Wrap(err, "failed to calculate the minimum required mana to issue the block"))
}

unlockAddress := output.UnlockConditionSet().Address()
if unlockAddress == nil {
return iotago.EmptyAccountID, false
}
unboundManaInputsLeftoverBalance, err := b.calculateAvailableManaLeftover(targetSlot, minRequiredMana, blockIssuerAccountID)
if err != nil {
return setBuildError(err)
}

if unlockAddress.Address.Type() != iotago.AddressAccount {
return iotago.EmptyAccountID, false
}
//nolint:forcetypeassert // we can safely assume that this is an AccountAddress
accountAddress := unlockAddress.Address.(*iotago.AccountAddress)
// allot the mana to the block issuer account (we increase the value, so we don't interfere with the already alloted value)
b.IncreaseAllotment(blockIssuerAccountID, minRequiredMana)

// move the remaining mana to stored mana on the specified output index
switch output := b.transaction.Outputs[storedManaOutputIndex].(type) {
case *iotago.BasicOutput:
output.Mana += unboundManaInputsLeftoverBalance
case *iotago.AccountOutput:
output.Mana += unboundManaInputsLeftoverBalance
case *iotago.NFTOutput:
output.Mana += unboundManaInputsLeftoverBalance
default:
return setBuildError(ierrors.Wrapf(iotago.ErrUnknownOutputType, "output type %T does not support stored mana", output))
}

return b
}

return accountAddress.AccountID(), true
// AllotAllMana allots all available mana to the provided account, even if the alloted value is less than the minimum required mana value to issue the block.
func (b *TransactionBuilder) AllotAllMana(targetSlot iotago.SlotIndex, blockIssuerAccountID iotago.AccountID) *TransactionBuilder {
setBuildError := func(err error) *TransactionBuilder {
b.occurredBuildErr = err
return b
}

// calculate the available mana on input side
_, unboundManaInputs, accountBoundManaInputs, err := CalculateAvailableMana(b.api.ProtocolParameters(), b.inputs, targetSlot)
unboundManaInputsLeftoverBalance, err := b.calculateAvailableManaLeftover(targetSlot, 0, blockIssuerAccountID)
if err != nil {
return setBuildError(ierrors.Wrap(err, "failed to calculate the available mana on input side"))
return setBuildError(err)
}

// update the unbound mana balance
updateUnboundManaBalance := func(manaOut iotago.Mana) error {
if unboundManaInputs < manaOut {
return ierrors.New("not enough unbound mana available on the input side")
}
unboundManaInputs -= manaOut
// allot the mana to the block issuer account (we increase the value, so we don't interfere with the already alloted value)
b.IncreaseAllotment(blockIssuerAccountID, unboundManaInputsLeftoverBalance)

return nil
return b
}

func (b *TransactionBuilder) calculateAvailableManaLeftover(targetSlot iotago.SlotIndex, minRequiredMana iotago.Mana, blockIssuerAccountID iotago.AccountID) (iotago.Mana, error) {
// calculate the available mana on input side
_, unboundManaInputs, accountBoundManaInputs, err := CalculateAvailableMana(b.api.ProtocolParameters(), b.inputs, targetSlot)
if err != nil {
return 0, ierrors.Wrap(err, "failed to calculate the available mana on input side")
}

// update the account bound mana balances if they exist and/or the onbound mana balance
Expand All @@ -195,7 +215,12 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ
accountBoundManaInputs[accountID] = 0

// subtract the remainder from the unbound mana
return updateUnboundManaBalance(accountBoundManaOut - accountBalance)
unboundManaInputs, err = safemath.SafeSub(unboundManaInputs, accountBoundManaOut-accountBalance)
if err != nil {
return ierrors.Wrapf(err, "not enough unbound mana on the input side for account %s while subtracting remainder", accountID.String())
}

return nil
}

// there is enough account bound mana for this account, subtract it from there
Expand All @@ -205,70 +230,73 @@ func (b *TransactionBuilder) AllotRequiredManaAndStoreRemainingManaInOutput(targ
}

// no account bound mana available for the given account, subtract it from the unbounded mana
return updateUnboundManaBalance(accountBoundManaOut)
}
unboundManaInputs, err = safemath.SafeSub(unboundManaInputs, accountBoundManaOut)
if err != nil {
return ierrors.Wrapf(err, "not enough unbound mana on the input side for account %s", accountID.String())
}

if storedManaOutputIndex >= len(b.transaction.Outputs) {
return setBuildError(ierrors.Errorf("given storedManaOutputIndex does not exist: %d", storedManaOutputIndex))
return nil
}

// subtract the stored mana on the outputs side
for _, o := range b.transaction.Outputs {
switch output := o.(type) {
case *iotago.AccountOutput:
// mana on account outputs is locked to this account
if err := updateUnboundAndAccountBoundManaBalances(output.AccountID, output.StoredMana()); err != nil {
return setBuildError(ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side"))
if err = updateUnboundAndAccountBoundManaBalances(output.AccountID, output.StoredMana()); err != nil {
return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side for account output")
}

default:
// check if the output locked mana to a certain account
if accountID, isManaLocked := hasManalockCondition(output); isManaLocked {
if err := updateUnboundAndAccountBoundManaBalances(accountID, output.StoredMana()); err != nil {
return setBuildError(ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side"))
if accountID, isManaLocked := b.hasManalockCondition(output); isManaLocked {
if err = updateUnboundAndAccountBoundManaBalances(accountID, output.StoredMana()); err != nil {
return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side, while checking locked mana")
}
} else {
if err := updateUnboundManaBalance(output.StoredMana()); err != nil {
return setBuildError(ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side"))
unboundManaInputs, err = safemath.SafeSub(unboundManaInputs, output.StoredMana())
if err != nil {
return 0, ierrors.Wrap(err, "failed to subtract the stored mana on the outputs side")
}
}
}
}

// subtract the already alloted mana
for _, allotment := range b.transaction.Allotments {
if err := updateUnboundAndAccountBoundManaBalances(allotment.AccountID, allotment.Value); err != nil {
return setBuildError(ierrors.Wrap(err, "failed to subtract the already alloted mana"))
if err = updateUnboundAndAccountBoundManaBalances(allotment.AccountID, allotment.Value); err != nil {
return 0, ierrors.Wrap(err, "failed to subtract the already alloted mana")
}
}

// calculate the minimum required mana to issue the block
minRequiredMana, err := b.MinRequiredAllotedMana(b.api.ProtocolParameters().WorkScoreStructure(), rmc, blockIssuerAccountID)
if err != nil {
return setBuildError(ierrors.Wrap(err, "failed to calculate the minimum required mana to issue the block"))
// subtract the minimum required mana to issue the block
if err = updateUnboundAndAccountBoundManaBalances(blockIssuerAccountID, minRequiredMana); err != nil {
return 0, ierrors.Wrap(err, "failed to subtract the minimum required mana to issue the block")
}

// subtract the minimum required mana to issue the block
if err := updateUnboundAndAccountBoundManaBalances(blockIssuerAccountID, minRequiredMana); err != nil {
return setBuildError(ierrors.Wrap(err, "failed to subtract the minimum required mana to issue the block"))
return unboundManaInputs, nil
}

// hasManalockCondition checks if the output is locked for a certain time to an account.
func (b *TransactionBuilder) hasManalockCondition(output iotago.Output) (iotago.AccountID, bool) {
minManalockedSlot := b.transaction.CreationSlot + 2*b.api.ProtocolParameters().MaxCommittableAge()

if !output.UnlockConditionSet().HasTimelockUntil(minManalockedSlot) {
return iotago.EmptyAccountID, false
}

// allot the mana to the block issuer account (we increase the value, so we don't interfere with the already alloted value)
b.IncreaseAllotment(blockIssuerAccountID, minRequiredMana)
unlockAddress := output.UnlockConditionSet().Address()
if unlockAddress == nil {
return iotago.EmptyAccountID, false
}

// move the remaining mana to stored mana on the specified output index
switch output := b.transaction.Outputs[storedManaOutputIndex].(type) {
case *iotago.BasicOutput:
output.Mana += unboundManaInputs
case *iotago.AccountOutput:
output.Mana += unboundManaInputs
case *iotago.NFTOutput:
output.Mana += unboundManaInputs
default:
return setBuildError(ierrors.Wrapf(iotago.ErrUnknownOutputType, "output type %T does not support stored mana", output))
if unlockAddress.Address.Type() != iotago.AddressAccount {
return iotago.EmptyAccountID, false
}
//nolint:forcetypeassert // we can safely assume that this is an AccountAddress
accountAddress := unlockAddress.Address.(*iotago.AccountAddress)

return b
return accountAddress.AccountID(), true
}

// BuildAndSwapToBlockBuilder builds the transaction and then swaps to a BasicBlockBuilder with
Expand Down
Loading