Just to elaborate on this, since I've been digging into it and asking around...
We can use the following formula to calculate the partial_fee
for an extrinsic (ie the total/final fee
minus any tip).
partial_fee = base_fee + len_fee + ((adjusted_weight_fee/estimated_weight)*actual_weight)
Where:
base_fee
is a fixed base fee to include some transaction in a block. It accounts
for the work needed to verify the signature and such and is constant for any tx.
len_fee
is a fee paid based on the size (length in bytes) of the transaction. Longer
transactions require more storage.
adjusted_weight_fee
is a fee that is itself estimated_weight * targeted_fee_adjustment
.
targeted_fee_adjustment
is some adjustment made based on the network load and such, and is
an opaque internal value we have no access to.
estimated_weight
is the "pre-dispatch" weight of the transaction. It's set based on the cost
of processing the transaction on reference hardware.
actual_weight
is the weight that is found in the ExtrinsicSuccess
event for the extrinsic in
a block (it's just called weight
in the event), and is often the same as estimated_weight
,
but the node has the opportunity to change it to whatever it likes, I think.
The RPC endpoint payment_queryFeeDetails
returns base_fee
, len_fee
and adjusted_weight_fee
.
The RPC endpoint payment_queryInfo
returns estimated_weight
(called weight
in the response), and
a partialFee
value, which is our best guess at the inclusion fee for the tx without actually submitting
it and seeing whether the node changes the weight/decides not to take a fee at all.
To get the correct values for some extrinsic from those endpoints, provide the extrinsic bytes, and the
block number one before the block it made it into (eg if the extrinsic was in block 100, you'd use
block 99 as an argument). Using the correct block number is very important.
Once you've called these endpoints, access the ExtrinsicSuccess
event to find the actual_weight
(it's just called weight
in the event props), but also a paysFee
value which signals whether the
extrinsic actually incurred a fee at all or not (a node has the opportunity to refund the fee entirely
if it likes by setting this to No
).
So, the steps to calculate the fee are basically:
- if
paysFee
is No, the fee paid is 0
- else, gather the values needed above and plus them into the equation above to return the final
partial fee that was paid.
Why the above equation?!
Well, the basic way to calculate a pre-dispatch fee is:
partial_fee = base_fee + len_fee + adjusted_weight_fee
(see above for explanations of each value).
We can obtain this result just from the payment
RPC endpoints.
Once the extrinsic is in a block though, the ExtrinsicSuccess
event signals whether the weight used to
calculate the fee changed (and whether we need to pay at all via paysFee
). So we need to recalculate the
adjusted_weight_fee
by swapping out the original weight
it was based on with the correct weight from the
ExtrinsicSuccess
event. That's all there really is to it.
Note that doing this calculation may incur some precision loss, and so it's recommended to use high precision
maths libraries to get the most accurate values back.
Caveats
Substrate based chains can use whatever custom fee logic they like, and so this logic/calculation only applies to chains that copy the Polkadot fee calculation logic.
There will soon be a transactionPayment::TransactionFeePaid
event, which will include the final fee paid for some transaction (ie the inclusion/partial fee + any tip). This will become a more reliable way to calculate fees going forwards (and is on Kusama already), but of course there is always a need to calculate fees for historic blocks too :)