Merge pull request #2038 from jerryfletcher21/rewards-support-routing-budget

add option for routing_budget_ppm in /api/rewards
This commit is contained in:
KoalaSat
2025-07-09 15:07:14 +00:00
committed by GitHub
7 changed files with 67 additions and 11 deletions

View File

@ -139,7 +139,6 @@ EXP_TAKER_BOND_INVOICE = 200
# Proportional routing fee limit (fraction of total payout: % / 100) # Proportional routing fee limit (fraction of total payout: % / 100)
PROPORTIONAL_ROUTING_FEE_LIMIT = 0.001 PROPORTIONAL_ROUTING_FEE_LIMIT = 0.001
# Base flat limit fee for routing in Sats (used only when proportional is lower than this) # Base flat limit fee for routing in Sats (used only when proportional is lower than this)
MIN_FLAT_ROUTING_FEE_LIMIT = 10
MIN_FLAT_ROUTING_FEE_LIMIT_REWARD = 2 MIN_FLAT_ROUTING_FEE_LIMIT_REWARD = 2
# Routing timeouts # Routing timeouts
REWARDS_TIMEOUT_SECONDS = 30 REWARDS_TIMEOUT_SECONDS = 30

View File

@ -501,7 +501,7 @@ class CLNNode:
* float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
) )
) # 200 ppm or 10 sats ) # 1000 ppm or 2 sats
timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS")) timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS"))
request = node_pb2.PayRequest( request = node_pb2.PayRequest(
bolt11=lnpayment.invoice, bolt11=lnpayment.invoice,

View File

@ -472,7 +472,7 @@ class LNDNode:
* float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
) )
) # 200 ppm or 10 sats ) # 1000 ppm or 2 sats
timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS")) timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS"))
request = router_pb2.SendPaymentRequest( request = router_pb2.SendPaymentRequest(
payment_request=lnpayment.invoice, payment_request=lnpayment.invoice,

View File

@ -1886,7 +1886,7 @@ class Logics:
return return
@classmethod @classmethod
def withdraw_rewards(cls, user, invoice): def withdraw_rewards(cls, user, invoice, routing_budget_ppm):
# only a user with positive withdraw balance can use this # only a user with positive withdraw balance can use this
if user.robot.earned_rewards < 1: if user.robot.earned_rewards < 1:
@ -1894,14 +1894,22 @@ class Logics:
num_satoshis = user.robot.earned_rewards num_satoshis = user.robot.earned_rewards
routing_budget_sats = int( if routing_budget_ppm is not None and routing_budget_ppm is not False:
max( routing_budget_sats = float(num_satoshis) * (
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), float(routing_budget_ppm) / 1_000_000
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
) )
) # 1000 ppm or 10 sats num_satoshis = int(num_satoshis - routing_budget_sats)
else:
# start deprecate in the future
routing_budget_sats = int(
max(
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
)
) # 1000 ppm or 2 sats
routing_budget_ppm = (routing_budget_sats / float(num_satoshis)) * 1_000_000
# end deprecate
routing_budget_ppm = (routing_budget_sats / float(num_satoshis)) * 1_000_000
reward_payout = LNNode.validate_ln_invoice( reward_payout = LNNode.validate_ln_invoice(
invoice, num_satoshis, routing_budget_ppm invoice, num_satoshis, routing_budget_ppm
) )

View File

@ -680,6 +680,14 @@ class ClaimRewardSerializer(serializers.Serializer):
default=None, default=None,
help_text="A valid LN invoice with the reward amount to withdraw", help_text="A valid LN invoice with the reward amount to withdraw",
) )
routing_budget_ppm = serializers.IntegerField(
default=0,
min_value=Decimal(0),
max_value=100_001,
allow_null=True,
required=False,
help_text="Max budget to allocate for routing in PPM",
)
class PriceSerializer(serializers.Serializer): class PriceSerializer(serializers.Serializer):

View File

@ -904,6 +904,7 @@ class RewardView(CreateAPIView):
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
pgp_invoice = serializer.data.get("invoice") pgp_invoice = serializer.data.get("invoice")
routing_budget_ppm = serializer.data.get("routing_budget_ppm", None)
valid_signature, invoice = verify_signed_message( valid_signature, invoice = verify_signed_message(
request.user.robot.public_key, pgp_invoice request.user.robot.public_key, pgp_invoice
@ -915,7 +916,7 @@ class RewardView(CreateAPIView):
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
valid, context = Logics.withdraw_rewards(request.user, invoice) valid, context = Logics.withdraw_rewards(request.user, invoice, routing_budget_ppm)
if not valid: if not valid:
context["successful_withdrawal"] = False context["successful_withdrawal"] = False

View File

@ -1732,6 +1732,45 @@ class TradeTest(BaseAPITestCase):
self.assertResponse(response) self.assertResponse(response)
self.assertIsInstance(response.json()["earned_rewards"], int) self.assertIsInstance(response.json()["earned_rewards"], int)
# Submit reward invoice
path = reverse("reward")
invoice = add_invoice("robot", response.json()["earned_rewards"])
signed_payout_invoice = sign_message(
invoice,
passphrase_path=f"tests/robots/{trade.taker_index}/token",
private_key_path=f"tests/robots/{trade.taker_index}/enc_priv_key",
)
body = {
"invoice": signed_payout_invoice
}
response = self.client.post(path, body, **taker_headers)
self.assertEqual(response.status_code, 200)
self.assertResponse(response)
self.assertTrue(response.json()["successful_withdrawal"])
def test_withdraw_reward_after_unilateral_cancel_routing_budget(self):
"""
Tests withdraw rewards specifying routing_budget_ppm as taker after maker
cancels order unilaterally
"""
trade = Trade(self.client)
trade.publish_order()
trade.take_order()
trade.take_order_third()
trade.lock_taker_bond()
trade.cancel_order(trade.maker_index)
# Fetch amount of rewards for taker
path = reverse("robot")
taker_headers = trade.get_robot_auth(trade.taker_index)
response = self.client.get(path, **taker_headers)
self.assertEqual(response.status_code, 200)
self.assertResponse(response)
self.assertIsInstance(response.json()["earned_rewards"], int)
# Submit reward invoice # Submit reward invoice
path = reverse("reward") path = reverse("reward")
invoice = add_invoice("robot", response.json()["earned_rewards"]) invoice = add_invoice("robot", response.json()["earned_rewards"])
@ -1742,6 +1781,7 @@ class TradeTest(BaseAPITestCase):
) )
body = { body = {
"invoice": signed_payout_invoice, "invoice": signed_payout_invoice,
"routing_budget_ppm": 0
} }
response = self.client.post(path, body, **taker_headers) response = self.client.post(path, body, **taker_headers)