Cannot calculate suggested fees for inner transactions

I have an application call which contains inner transactions, written in PyTEAL.

I set the inner fee parameter to 0 so that the sender must pay for fees of the inner transaction and not use the funds held by the application.

InnerTxnBuilder.Begin(),
InnerTxnBuilder.SetFields(
    {
        TxnField.type_enum: TxnType.Payment,
        TxnField.amount: amount,
        TxnField.receiver: reciever,
        # fee set to 0 to not use application funds
        TxnField.fee: Int(0)
    }
),
InnerTxnBuilder.Submit(),

But then it throws a fee too small error, because I still need to modify the suggested parameters when I construct the transaction so that the user pays for the fees for the additional transactions.

I figured that I need to increase suggusted_params.fee to include the fees of the inner transaction.

suggusted_params = algod.suggested_params()
# pay additional fee for one inner transaction
suggusted_params.fee = "???"
txn = transaction.ApplicationNoOpTxn(
    sender=sender.address,
    sp=suggusted_params,
    index=application_id
)

But the question is, how do I calculate the fee for the transaction?

Unless I am mistaken:

suggusted_params.fee is the fee per byte. However, the suggested params returns the value "fee": 0 on all networks, so we cannot calculate the required fees. (My intention was to multiply it by 2 since I have 1 inner transaction, but obviously cannot since the value is 0).

suggusted_params.min_fee is the minimum fee that you must pay when the bytes*fee is low. However, it is not useful either because I need the fee per byte, not the minimum network fee.

I have found that in the dev sandbox network, I need the fee to be at least 10. But I do not wish to use a static value because the fees might increase in the future.

Side note

Also, is this a good way to set an acceptable fee in PyTEAL? (Txn.fee is total fee, not fee per byte)

has_acceptable_fee = Txn.fee() <= Global.min_txn_fee() * Int(100)
Assert(has_acceptable_fee)

On another note, I think the interchangability of “suggested fee” (per byte) and “fee” might cause confusion in some cases.

I think I might be confusing myself a bit.

The real question might be:

How do I get suggested fee per byte, and why does the API return fee-per-byte of 0?

Fees without inner transactions

The fees are computed as indicated there:

There is a minimum fee (currently 1,000 microAlgos) and a fee per byte.
The fee per byte is 0 when no congestion.
A node will only accept transactions with fee at least:

max(min_fee, size_of_transaction * fee_per_byte)

Fee pooling

An important feature of Algorand is that fees are pooled in a group of transactions.
This means you just need that the sum of fees of all transactions in the group is sufficient.

Fees with inner transactions

Without congestion, you need to pay 1,000 microAlgos per transaction (including inner transactions) over all the transactions.

So in your case, what you need is to ensure that your application call has at least 1,000 microAlgos of fees.
The simplest solution for that is to set:

suggested_params.fee = suggested_params.min_fee * 2
suggested_params.flat_fee = True

or

from algosdk import constants

suggested_params.fee = constants.MIN_TXN_FEE * 2
suggested_params.flat_fee = True

EDIT: I wrote something that does not work before (suggested_params.min_fee = suggested_params.min_fee * 2, which is incorrect because the code does not use suggested_params.min_fee but constants.min_txn_fee)

Modifying the fee per byte is too complicated and not useful in that case.

(Advanced note: the node will first check this information without looking at inner transactions, which means you cannot use fees from inner transactions to pay for fees for outer transactions.)

Checking the fee via side note

This looks acceptable is you’re ready to accept paying 100 times the min fee.
I would say that in general I would just either force Txn.fee() == 0 or Txn.fee() <= Global.min_txn_fee() and expect the user to pay the required extra fees via fee pooling in the outer transaction in case of congestion.

Thanks for the reply. :slight_smile:

The simplest solution for that is to set:

suggested_params.min_fee = suggested_params.min_fee * 2

I have been trying this, but the min_fee has no effect on whether the transaction is accepted or rejected.

For example, I have tried:

# Delete Application Txn which contains one Inner Transaction

suggusted_params = algod.suggested_params()
suggusted_params.min_fee *= 2
txn = transaction.ApplicationDeleteTxn(
    sender=sender.address,
    sp=suggusted_params,
    index=application_id
)

# ...
# Application Call Txn which contains one Inner Transactions

suggusted_params = algod.suggested_params()
txn_1 = transaction.PaymentTxn(
    sender=sender.address,
    sp=suggusted_params,
    receiver=utils.get_application_address(application_id),
    amt=amount
)

suggusted_params = algod.suggested_params()
suggusted_params.min_fee *= 2
print(suggusted_params.min_fee, '!!!') # 2000 !!!
txn_2 = transaction.ApplicationNoOpTxn(
    sender=sender.address,
    sp=suggusted_params,
    index=application_id,
    app_args=['return_to_sender']
)

# ...

But they still throw: logic eval error: fee too small. Even multiplying min_fee by 1000 shows the same result.

However, setting suggusted_params.fee = 10 does work.

I am testing on the Sandbox Release network currently.

Unless I am mistaken, the issue is still remains:

  • The only way to pay for inner transaction fees is to set suggusted_params.fee
  • Which is problematic because there is no way to obtain the fee per byte
    • algod.suggested_params() (and REST API) only returns fee=0 on all networks

Checking the fee via side note

This looks acceptable is you’re ready to accept paying 100 times the min fee.

To clarify, I set the max fee on the application call to 100x because I assumed:

  • The fees will still be quite low
  • It will definitely pay for all inner transactions
  • The user can decide how much they want to pay for transaction speed
  • It is future-proof

So I was just wondering if this intuition was accurate.

I would say that in general I would just either force Txn.fee() == 0 or Txn.fee() <= Global.min_txn_fee() and expect the user to pay the required extra fees via fee pooling in the outer transaction in case of congestion.

If there is only one outer transaction (the application call), I need the at least: Txn.fee() <= Global.min_txn_fee() * (1 + NUM_INNER_TXNS).

My intention was to pay for inner transactions using the fees on a standalone Application Call Transaction.

When I have multiple outer transactions, and there are congestion benefits to pooling, I’ll keep that in mind. :slight_smile:

Sorry my bad, the code I gave you was wrong.
Here is the correct one:

from algosdk import constants

suggested_params.fee = constants.MIN_TXN_FEE * 2
suggested_params.flat_fee = True

or

suggested_params.fee = suggested_params.min_fee * 2
suggested_params.flat_fee = True
1 Like
suggested_params.fee = suggested_params.min_fee * 2
suggested_params.flat_fee = True

Looks good. I did not know about the flat fee.

I can see that if flat_fee = True, then fee is the total fee instead of the fee per byte (as I can see in py algosdk docs for the SuggestedParams).

I think that will work in 99% of cases. However, I believe that the issues I mentioned still remain.

The solution above only works as long as:

  1. You know how many inner transactions there are

    • (Because this is the factor to multiply min_fee by)
    • (This not a huge problem since you would normally know the number of inner transactions when constructing the transaction)
  2. You know how many bytes your transactions write

    • (Because if the inner transactions write a lot of bytes or , the required fee will exceed the flat fee)
    • (If any of your transactions write bytes which cause it to exceed the min fee, then the only way to know what fee to pay is to guess, as far as I know)
  3. The current network fees does not change due to congestion

    • (Same as above. Too many bytes + too high network fees = failed transaction)

If the fee-per-byte was available then I it would easier for development and more future-proof since then you do not need any fee calculation logic and your application is adaptive to changes in the fees if unexpected network congestion occurs.

But I think the only way to get the fee-per-byte is to fix the algod /v2/transactions/params endpoint so that fee is not 0 - unless I am mistaken.

Actually, there might be a way to get the exact total fee for a transaction using dryruns. You would need to construct and sign the transaction twice though.

suggest_params.fee (before changed above) is actually the fee per byte.
But you are right: you would need to manually check the size of the transaction to know whether the fee-per-byte computation needs to be used or the min fee computation should be used.
This depends on the actual congestion.

You can indeed use dry-run to know the number of inner transactions.

I have made an gh issue and I think it clarified a few things for me as well.

The fee parameter (fee-per-byte) returned by the API is indeed correct (it is zero until network congestion occurs). The fee-per-byte is NOT a problem.

Is this a bug?

As stated above, suggested_params.min_fee has no effect when passed into a transaction contructor (sp=suggested_params).

The reason for this is because py-algorand-sdk uses a constant (constants.min_txn_fee) instead of the min-fee which is stored in the object (sp.min_fee).

if not sp.flat_fee:
    self.fee = max(
        self.estimate_size() * self.fee, constants.min_txn_fee
    )

I do not know if this is intentional or not, but it does make it so that the user cannot specify the minimum fee - and you need to use a flat fee instead.

How to calculate fees for inner transactions

For the time-being, if:

  • You have inner transactions (with fees paid by the user)
  • You want to make sure that your transactions adapt to increased fee-per-byte (due to network congestion)
  • You want it to work with specific number of inner transactions

Then you need to do something like the following:

# Get txn info

num_bytes = transaction.ApplicationNoOpTxn(
    sender=sender.address,
    sp=client.suggested_params(),
    index=application_id,
    app_args=['return_to_sender']
).estimate_size()
num_txns = 5 # hard coded (1 outer + 4 inner)

# Calculate fee & get suggested params

suggusted_params = client.suggested_params()
suggusted_params.fee = max(
    num_bytes * suggested_params.fee,     # fee-per-byte
    num_txns * suggusted_params.min_fee  # min-fee
)
suggusted_params.flat_fee = True

# Construct transaction

txn = transaction.ApplicationNoOpTxn(
    sender=sender.address,
    sp=suggusted_params,
    index=application_id,
    app_args=['return_to_sender']
)

# ... 
# sign and send

(I have not tested)

Otherwise, for 99% of cases, the simple answers will work fine.

I would be convenient to use suggested_parameters.min_fee though.

1 Like

Another thing, I do not think it is possible to calculate the num_bytes for inner transactions. (need to look into dryruns)

That may complicate things a little bit since you need to pay for the bytes of inner transactions too. And there is no way to get it easily/conveniently. Then again, transactions do not usually change in size and it is probably not too inconvenient to hard-code the number of bytes and transactions.

Yes, your computation looks good for transactions that are signed with an ed25519 key (as opposed to multisig / smart signatures, as estimated_size assumes that the transaction is signed with an ed25519 key - but this is an issue with the SDK too).

Thanks for this detailed code!

Inner transactions currently do not count in the size you need to consider. So your code should also work with inner transactions, as long as num_txns also includes the inner transactions.
That is if you have a group of 2 transaction calls, one doing 3 inner transactions and one doing 1 inner transaction, num_txns = 2 + 3 + 1 = 6.