Skip to content

Commit

Permalink
Better fee calculation (#370)
Browse files Browse the repository at this point in the history
* Add better support for txn flags

* support Transaction.has_flag()

* support Transaction.has_flag()

* Add better support for txn flags.

* bump version; fix import TypedDict

* Update CHANGELOG.md

* All flags to upper case; Update Changelog; Update pyproject

* better fee calculation

* Edit docstring; Add inline-comments

* Fix typo

* Add referral to original formula

* Update settings.json

* add helper function; add tests

* fix linter; add option dynamic; change default back to open

* fix exception message

* Edit docstrings; Remove

* Add comments for calculations

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
  • Loading branch information
LimpidCrypto committed Apr 26, 2022
1 parent 811afa8 commit b82b004
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 13 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Expand Up @@ -3,5 +3,6 @@
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.linting.enabled": true,
"restructuredtext.confPath": "${workspaceFolder}/docs"
"restructuredtext.confPath": "${workspaceFolder}/docs",
"esbonio.sphinx.confDir": "${workspaceFolder}/docs"
}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [[Unreleased]]
### Added:
- Support for dynamic fee calculation

## [1.5.0] - 2022-04-25
### Added
Expand Down
Empty file.
74 changes: 74 additions & 0 deletions tests/unit/asyn/ledger/test_calculate_fee_dynamically.py
@@ -0,0 +1,74 @@
from unittest import TestCase

from xrpl.asyncio.ledger.main import calculate_fee_dynamically


class TestCalculateFeeDynamically(TestCase):
def test_queue_empty(self):
actual = {
"current_ledger_size": "46",
"current_queue_size": "0",
"drops": {
"base_fee": "10",
"median_fee": "5000",
"minimum_fee": "10",
"open_ledger_fee": "10",
},
"expected_ledger_size": "176",
"ledger_current_index": 70813866,
"levels": {
"median_level": "128000",
"minimum_level": "256",
"open_ledger_level": "256",
"reference_level": "256",
},
"max_queue_size": "3520",
}
expected = "15"
self.assertEqual(calculate_fee_dynamically(fee_data_set=actual), expected)

def test_queue_partially_filled(self):
actual = {
"current_ledger_size": "46",
"current_queue_size": "1760",
"drops": {
"base_fee": "10",
"median_fee": "5000",
"minimum_fee": "10",
"open_ledger_fee": "10",
},
"expected_ledger_size": "176",
"ledger_current_index": 70813866,
"levels": {
"median_level": "128000",
"minimum_level": "256",
"open_ledger_level": "256",
"reference_level": "256",
},
"max_queue_size": "3520",
}
expected = "225"
self.assertEqual(calculate_fee_dynamically(fee_data_set=actual), expected)

def test_queue_full(self):
actual = {
"current_ledger_size": "46",
"current_queue_size": "3520",
"drops": {
"base_fee": "10",
"median_fee": "5000",
"minimum_fee": "10",
"open_ledger_fee": "10",
},
"expected_ledger_size": "176",
"ledger_current_index": 70813866,
"levels": {
"median_level": "128000",
"minimum_level": "256",
"open_ledger_level": "256",
"reference_level": "256",
},
"max_queue_size": "3520",
}
expected = "5500"
self.assertEqual(calculate_fee_dynamically(fee_data_set=actual), expected)
21 changes: 14 additions & 7 deletions xrpl/asyncio/ledger/main.py
Expand Up @@ -3,6 +3,7 @@
from typing import Optional, cast

from xrpl.asyncio.clients import Client, XRPLRequestFailureException
from xrpl.asyncio.ledger.utils import calculate_fee_dynamically
from xrpl.constants import XRPLException
from xrpl.models.requests import Fee, Ledger
from xrpl.utils import xrp_to_drops
Expand Down Expand Up @@ -60,11 +61,14 @@ async def get_fee(
high, then the fees will not scale past the maximum fee. If None, there is
no ceiling for the fee. The default is 2 XRP.
fee_type: The type of fee to return. The options are "open" (the load-scaled
fee to get into the open ledger) or "minimum" (the minimum transaction
fee). The default is "open".
fee to get into the open ledger), "minimum" (the minimum transaction
fee) or "dynamic" (dynamic fee-calculation based on the queue size
of the node). The default is "open". The recommended option is
"dynamic".
Returns:
The transaction fee, in drops.
`Read more about drops <https://xrpl.org/currency-formats.html#xrp-amounts>`_
Raises:
XRPLException: if an incorrect option for `fee_type` is passed in.
Expand All @@ -74,15 +78,18 @@ async def get_fee(
if not response.is_successful():
raise XRPLRequestFailureException(response.result)

result = response.result["drops"]
result = response.result
drops = result["drops"]
if fee_type == "open":
fee = cast(str, result["open_ledger_fee"])
fee = cast(str, drops["open_ledger_fee"])
elif fee_type == "minimum":
fee = cast(str, result["minimum_fee"])
fee = cast(str, drops["minimum_fee"])
elif fee_type == "dynamic":
fee = calculate_fee_dynamically(fee_data_set=result)
else:
raise XRPLException(
f'`fee_type` param must be "open" or "minimum". {fee_type} is not a '
"valid option."
'`fee_type` param must be "open", "minimum" or "dynamic".'
f" {fee_type} is not a valid option."
)
if max_fee is not None:
max_fee_drops = int(xrp_to_drops(max_fee))
Expand Down
72 changes: 72 additions & 0 deletions xrpl/asyncio/ledger/utils.py
@@ -0,0 +1,72 @@
"""Helper functions for the ledger module."""

from typing import Any, Dict


def calculate_fee_dynamically(fee_data_set: Dict[str, Any]) -> str:
"""Calculate the transaction fee dynamically
based on the size of the queue of the node.
Args:
fee_data_set (Dict[str, Any]): The result of the `fee` method.
Returns:
str: The transaction fee, in drops.
`Read more about drops <https://xrpl.org/currency-formats.html#xrp-amounts>`_
Based on fee-calculation code here:
`<https://gist.github.com/WietseWind/3e9f9339f37a5881978a9661f49b0e52>`_
"""
current_queue_size = int(fee_data_set["current_queue_size"])
max_queue_size = int(fee_data_set["max_queue_size"])
queue_pct = current_queue_size / max_queue_size
drops = fee_data_set["drops"]
minimum_fee = int(drops["minimum_fee"])
median_fee = int(drops["median_fee"])
open_ledger_fee = int(drops["open_ledger_fee"])

# calculate the lowest fee the user is able to pay if the queue is empty
fee_low = round(
min(
max(minimum_fee * 1.5, round(max(median_fee, open_ledger_fee) / 500)),
1000,
),
)
if queue_pct > 0.1: # if 'current_queue_size' is >10 % of 'max_queue_size'
possible_fee_medium = round(
(minimum_fee + median_fee + open_ledger_fee) / 3,
)
elif queue_pct == 0: # if 'current_queue_size' is 0
possible_fee_medium = max(
10 * minimum_fee,
open_ledger_fee,
)
else:
possible_fee_medium = max(
10 * minimum_fee,
round((minimum_fee + median_fee) / 2),
)
# calculate the lowest fee the user is able to pay if there are txns in the queue
fee_medium = round(
min(
possible_fee_medium,
fee_low * 15,
10000,
),
)
# calculate the lowest fee the user is able to pay if the txn queue is full
fee_high = round(
min(
max(10 * minimum_fee, round(max(median_fee, open_ledger_fee) * 1.1)),
100000,
),
)

if queue_pct == 0: # if queue is empty
fee = str(fee_low)
elif 0 < queue_pct < 1: # queue has txns in it but is not full
fee = str(fee_medium)
else: # if queue is full
fee = str(fee_high)

return fee
17 changes: 12 additions & 5 deletions xrpl/ledger/main.py
Expand Up @@ -40,24 +40,31 @@ def get_latest_open_ledger_sequence(client: SyncClient) -> int:


def get_fee(
client: SyncClient, *, max_fee: Optional[float] = 2, fee_type: str = "open"
client: SyncClient,
*,
max_fee: Optional[float] = 2,
fee_type: str = "open",
) -> str:
"""
Query the ledger for the current minimum transaction fee.
Query the ledger for the current transaction fee.
Args:
client: the network client used to make network calls.
max_fee: The maximum fee in XRP that the user wants to pay. If load gets too
high, then the fees will not scale past the maximum fee. If None, there is
no ceiling for the fee. The default is 2 XRP.
fee_type: The type of fee to return. The options are "open" (the load-scaled
fee to get into the open ledger) or "minimum" (the minimum transaction
cost). The default is "open".
fee to get into the open ledger), "minimum" (the minimum transaction
fee) or "dynamic" (dynamic fee-calculation based on the queue size
of the node). The default is "open". The recommended option is
"dynamic".
Returns:
The minimum fee for transactions.
The transaction fee, in drops.
`Read more about drops <https://xrpl.org/currency-formats.html#xrp-amounts>`_
Raises:
XRPLException: if an incorrect option for `fee_type` is passed in.
XRPLRequestFailureException: if the rippled API call fails.
"""
return asyncio.run(main.get_fee(client, max_fee=max_fee, fee_type=fee_type))

0 comments on commit b82b004

Please sign in to comment.