Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 0 additions & 1 deletion .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ EXP_TAKER_BOND_INVOICE = 200
# Proportional routing fee limit (fraction of total payout: % / 100)
PROPORTIONAL_ROUTING_FEE_LIMIT = 0.001
# 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
# Routing timeouts
REWARDS_TIMEOUT_SECONDS = 30
Expand Down
2 changes: 1 addition & 1 deletion api/lightning/cln.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ def pay_invoice(cls, lnpayment):
* float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
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"))
request = node_pb2.PayRequest(
bolt11=lnpayment.invoice,
Expand Down
2 changes: 1 addition & 1 deletion api/lightning/lnd.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ def pay_invoice(cls, lnpayment):
* float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
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"))
request = router_pb2.SendPaymentRequest(
payment_request=lnpayment.invoice,
Expand Down
22 changes: 15 additions & 7 deletions api/logics.py
Original file line number Diff line number Diff line change
Expand Up @@ -1886,22 +1886,30 @@ def add_slashed_rewards(cls, order, slashed_bond, staked_bond):
return

@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

if user.robot.earned_rewards < 1:
return False, {"bad_invoice": "You have not earned rewards"}

num_satoshis = user.robot.earned_rewards

routing_budget_sats = int(
max(
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
if routing_budget_ppm is not None and routing_budget_ppm is not False:
routing_budget_sats = float(num_satoshis) * (
float(routing_budget_ppm) / 1_000_000
)
) # 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(
invoice, num_satoshis, routing_budget_ppm
)
Expand Down
8 changes: 8 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,14 @@ class ClaimRewardSerializer(serializers.Serializer):
default=None,
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):
Expand Down
3 changes: 2 additions & 1 deletion api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@ def post(self, request):
return Response(status=status.HTTP_400_BAD_REQUEST)

pgp_invoice = serializer.data.get("invoice")
routing_budget_ppm = serializer.data.get("routing_budget_ppm", None)

valid_signature, invoice = verify_signed_message(
request.user.robot.public_key, pgp_invoice
Expand All @@ -915,7 +916,7 @@ def post(self, 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:
context["successful_withdrawal"] = False
Expand Down
40 changes: 40 additions & 0 deletions tests/test_trade_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,45 @@ def test_withdraw_reward_after_unilateral_cancel(self):
self.assertResponse(response)
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
path = reverse("reward")
invoice = add_invoice("robot", response.json()["earned_rewards"])
Expand All @@ -1742,6 +1781,7 @@ def test_withdraw_reward_after_unilateral_cancel(self):
)
body = {
"invoice": signed_payout_invoice,
"routing_budget_ppm": 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth to leave this test as it is and create a new one specifically sending a custom number here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a new test

}

response = self.client.post(path, body, **taker_headers)
Expand Down
Loading