def _calculate_fees(self) -> Optional[List[FeeAmount]]: """Calcluates fees backwards for this path. Returns ``None``, if the fee calculation cannot be done. """ total = PaymentWithFeeAmount(self.value) fees: List[FeeAmount] = [] for prev_node, mediator, next_node in reversed( list(window(self.nodes, 3))): view_in: ChannelView = self.G[prev_node][mediator]["view"] view_out: ChannelView = self.G[mediator][next_node]["view"] log.debug( "Fee calculation", amount=total, view_out=view_out, view_in=view_in, amount_without_fees=total, balance_in=view_in.capacity_partner, balance_out=view_out.capacity, schedule_in=view_in.fee_schedule_receiver, schedule_out=view_out.fee_schedule_sender, receivable_amount=view_in.capacity, ) amount_with_fees = get_amount_with_fees( amount_without_fees=total, balance_in=Balance(view_in.capacity_partner), balance_out=Balance(view_out.capacity), schedule_in=view_in.fee_schedule_receiver, schedule_out=view_out.fee_schedule_sender, receivable_amount=view_in.capacity, ) if amount_with_fees is None: log.warning( "Invalid path because of invalid fee calculation", amount=total, view_out=view_out, view_in=view_in, amount_without_fees=total, balance_in=view_in.capacity_partner, balance_out=view_out.capacity, schedule_in=view_in.fee_schedule_receiver, schedule_out=view_out.fee_schedule_sender, receivable_amount=view_in.capacity, ) return None fee = PaymentWithFeeAmount(amount_with_fees - total) total += fee # type: ignore fees.append(FeeAmount(fee)) # The hop to the target does not incur mediation fees fees.append(FeeAmount(0)) return fees
def test_fee_add_remove_invariant(flat_fee, prop_fee, imbalance_fee, amount, balance1, balance2): """ First adding and then removing fees must yield the original value """ total_balance = TokenAmount(100_000_000_000_000_000_000) prop_fee_per_channel = ppm_fee_per_channel(ProportionalFeeAmount(prop_fee)) imbalance_fee = calculate_imbalance_fees( channel_capacity=total_balance, proportional_imbalance_fee=ProportionalFeeAmount(imbalance_fee), ) fee_schedule = FeeScheduleState( cap_fees=False, flat=FeeAmount(flat_fee), proportional=prop_fee_per_channel, imbalance_penalty=imbalance_fee, ) channel_in = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=total_balance - balance1), partner_state=NettingChannelEndStateProperties(balance=balance1), fee_schedule=fee_schedule, ) ) channel_out = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=balance2), partner_state=NettingChannelEndStateProperties(balance=total_balance - balance2), fee_schedule=fee_schedule, ) ) amount_with_fees = get_amount_with_fees( amount_without_fees=amount, schedule_in=channel_in.fee_schedule, schedule_out=channel_out.fee_schedule, receivable_amount=balance1, balance_in=total_balance - balance1, balance_out=balance2, ) assume(amount_with_fees) assert amount_with_fees amount_without_fees = get_amount_without_fees( amount_with_fees=amount_with_fees, channel_in=channel_in, channel_out=channel_out ) assume(amount_without_fees) assert amount - 1 <= amount_without_fees <= amount + 1
def edge_weight( visited: Dict[ChannelID, float], view: ChannelView, view_from_partner: ChannelView, amount: PaymentAmount, fee_penalty: float, ) -> float: diversity_weight = visited.get(view.channel_id, 0) # Fees for initiator and target are included here. This promotes routes # that are nice to the initiator's and target's capacities, but it's # inconsistent with the estimated total fee. # Enable fee apping for both fee schedules schedule_in = copy(view.fee_schedule_receiver) schedule_in.cap_fees = True schedule_out = copy(view.fee_schedule_sender) schedule_out.cap_fees = True amount_with_fees = get_amount_with_fees( amount_without_fees=PaymentWithFeeAmount(amount), balance_in=Balance(view.capacity), balance_out=Balance(view.capacity), schedule_in=schedule_in, schedule_out=schedule_out, receivable_amount=view.capacity, ) if amount_with_fees is None: return float("inf") fee = FeeAmount(amount_with_fees - amount) fee_weight = fee / 1e18 * fee_penalty no_refund_weight = 0 if view_from_partner.capacity < int(float(amount) * 1.1): no_refund_weight = 1 return 1 + diversity_weight + fee_weight + no_refund_weight
def _calculate_fees(self) -> None: total = PaymentWithFeeAmount(self.value) for prev_node, mediator, next_node in reversed( list(window(self.nodes, 3))): view_in: ChannelView = self.G[prev_node][mediator]["view"] view_out: ChannelView = self.G[mediator][next_node]["view"] amount_with_fees = get_amount_with_fees( amount_without_fees=total, balance_in=Balance(view_in.capacity_partner), balance_out=Balance(view_out.capacity), schedule_in=view_in.fee_schedule_receiver, schedule_out=view_out.fee_schedule_sender, receivable_amount=view_in.capacity, ) if amount_with_fees is None: log.warning( "Invalid fee calculation", amount=total, view_out=view_out, view_in=view_in, amount_without_fees=total, balance_in=view_in.capacity_partner, balance_out=view_out.capacity, schedule_in=view_in.fee_schedule_receiver, schedule_out=view_out.fee_schedule_sender, receivable_amount=view_in.capacity, ) self._is_valid = False break fee = PaymentWithFeeAmount(amount_with_fees - total) total += fee # type: ignore self.fees.append(FeeAmount(fee))
def test_imbalance_penalty(): r"""Test an imbalance penalty by moving back and forth The imbalance fee looks like 20 | / | / 10 |\. / | \. / 0 | \/ --------------- 0 50 100 For each input, we first assume the channel is used to forward tokens to a payee, which moves the capacity from x1 to x2. The we assume the same amount is mediated in the opposite direction (moving from x2 to x1) and check that the calculated fee is the same as before just with the opposite sign. """ v_schedule = FeeScheduleState( imbalance_penalty=[ (TokenAmount(0), FeeAmount(10)), (TokenAmount(50), FeeAmount(0)), (TokenAmount(100), FeeAmount(20)), ] ) reverse_schedule = FeeScheduleState( imbalance_penalty=[ (TokenAmount(0), FeeAmount(20)), (TokenAmount(50), FeeAmount(0)), (TokenAmount(100), FeeAmount(10)), ] ) for cap_fees, x1, amount, expected_fee_in, expected_fee_out in [ # Uncapped fees (False, 0, 50, -8, -10), (False, 50, 30, 20, 12), (False, 0, 10, -2, -2), (False, 10, 10, -2, -2), (False, 0, 20, -3, -4), (False, 40, 15, 0, 0), (False, 50, 31, None, 12), (False, 100, 1, None, None), # Capped fees (True, 0, 50, 0, 0), (True, 50, 30, 20, 12), (True, 0, 10, 0, 0), (True, 10, 10, 0, 0), (True, 0, 20, 0, 0), (True, 40, 15, 0, 0), ]: v_schedule.cap_fees = cap_fees amount_with_fees = get_amount_with_fees( amount_without_fees=PaymentWithFeeAmount(amount), balance_in=Balance(x1), balance_out=Balance(100), schedule_in=v_schedule, schedule_out=FeeScheduleState(cap_fees=cap_fees), receivable_amount=TokenAmount(100 - x1), ) if expected_fee_in is None: assert amount_with_fees is None else: assert amount_with_fees is not None assert amount_with_fees - amount == FeeAmount(expected_fee_in) reverse_schedule.cap_fees = cap_fees amount_with_fees = get_amount_with_fees( amount_without_fees=PaymentWithFeeAmount(amount), balance_in=Balance(0), balance_out=Balance(100 - x1), schedule_in=FeeScheduleState(cap_fees=cap_fees), schedule_out=reverse_schedule, receivable_amount=TokenAmount(100), ) if expected_fee_out is None: assert amount_with_fees is None else: assert amount_with_fees is not None assert amount_with_fees - amount == FeeAmount(expected_fee_out)