def maybe_fulfill_htlc(
            self, *,
            chan: Channel,
            htlc: UpdateAddHtlc,
            processed_onion: ProcessedOnionPacket,
            onion_packet_bytes: bytes,
            already_forwarded: bool = False,
    ) -> Tuple[Optional[bytes], Optional[Tuple[str, Callable[[], Awaitable[Optional[str]]]]]]:
        """
        Decide what to do with an HTLC: return preimage if it can be fulfilled, forwarding callback if it can be forwarded.
        Return (preimage, (payment_key, callback)) with at most a single element not None.
        """
        if not processed_onion.are_we_final:
            if not self.lnworker.enable_htlc_forwarding:
                return None, None
            # use the htlc key if we are forwarding
            payment_key = serialize_htlc_key(chan.get_scid_or_local_alias(), htlc.htlc_id)
            callback = lambda: self.maybe_forward_htlc(
                incoming_chan=chan,
                htlc=htlc,
                processed_onion=processed_onion)
            return None, (payment_key, callback)

        def log_fail_reason(reason: str):
            self.logger.info(
                f"maybe_fulfill_htlc. will FAIL HTLC: chan {chan.short_channel_id}. "
                f"{reason}. htlc={str(htlc)}. onion_payload={processed_onion.hop_data.payload}")

        chain = self.network.blockchain()
        # Check that our blockchain tip is sufficiently recent so that we have an approx idea of the height.
        # We should not release the preimage for an HTLC that its sender could already time out as
        # then they might try to force-close and it becomes a race.
        if chain.is_tip_stale() and not already_forwarded:
            log_fail_reason(f"our chain tip is stale")
            raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_NODE_FAILURE, data=b'')
        local_height = chain.height()

        # parse parameters and perform checks that are invariant
        payment_secret_from_onion, total_msat, channel_opening_fee, exc_incorrect_or_unknown_pd = self.check_accepted_htlc(
            chan=chan,
            htlc=htlc,
            processed_onion=processed_onion,
            log_fail_reason=log_fail_reason)

        # payment key for final onions
        payment_hash = htlc.payment_hash
        payment_key = (payment_hash + payment_secret_from_onion).hex()

        if self.check_mpp_is_waiting(
                payment_secret=payment_secret_from_onion,
                short_channel_id=chan.get_scid_or_local_alias(),
                htlc=htlc,
                expected_msat=total_msat,
                exc_incorrect_or_unknown_pd=exc_incorrect_or_unknown_pd,
                log_fail_reason=log_fail_reason,
        ):
            return None, None

        # TODO check against actual min_final_cltv_expiry_delta from invoice (and give 2-3 blocks of leeway?)
        if local_height + MIN_FINAL_CLTV_DELTA_ACCEPTED > htlc.cltv_abs and not already_forwarded:
            log_fail_reason(f"htlc.cltv_abs is unreasonably close")
            raise exc_incorrect_or_unknown_pd

        # detect callback
        # if there is a trampoline_onion, maybe_fulfill_htlc will be called again
        # order is important: if we receive a trampoline onion for a hold invoice, we need to peel the onion first.

        if processed_onion.trampoline_onion_packet:
            # TODO: we should check that all trampoline_onions are the same
            trampoline_onion = self.process_onion_packet(
                processed_onion.trampoline_onion_packet,
                payment_hash=payment_hash,
                onion_packet_bytes=onion_packet_bytes,
                is_trampoline=True)
            if trampoline_onion.are_we_final:
                # trampoline- we are final recipient of HTLC
                # note: the returned payment_key will contain the inner payment_secret
                return self.maybe_fulfill_htlc(
                    chan=chan,
                    htlc=htlc,
                    processed_onion=trampoline_onion,
                    onion_packet_bytes=onion_packet_bytes,
                    already_forwarded=already_forwarded,
                )
            else:
                callback = lambda: self.maybe_forward_trampoline(
                    payment_hash=payment_hash,
                    inc_cltv_abs=htlc.cltv_abs, # TODO: use max or enforce same value across mpp parts
                    outer_onion=processed_onion,
                    trampoline_onion=trampoline_onion,
                    fw_payment_key=payment_key)
                return None, (payment_key, callback)

        # TODO don't accept payments twice for same invoice
        # TODO check invoice expiry
        info = self.lnworker.get_payment_info(payment_hash)
        if info is None:
            log_fail_reason(f"no payment_info found for RHASH {htlc.payment_hash.hex()}")
            raise exc_incorrect_or_unknown_pd

        preimage = self.lnworker.get_preimage(payment_hash)
        expected_payment_secrets = [self.lnworker.get_payment_secret(htlc.payment_hash)]
        if preimage:
            expected_payment_secrets.append(derive_payment_secret_from_payment_preimage(preimage)) # legacy secret for old invoices
        if payment_secret_from_onion not in expected_payment_secrets:
            log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {expected_payment_secrets[0].hex()}')
            raise exc_incorrect_or_unknown_pd
        invoice_msat = info.amount_msat
        if channel_opening_fee:
            invoice_msat -= channel_opening_fee

        if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat):
            log_fail_reason(f"total_msat={total_msat} too different from invoice_msat={invoice_msat}")
            raise exc_incorrect_or_unknown_pd

        hold_invoice_callback = self.lnworker.hold_invoice_callbacks.get(payment_hash)
        if hold_invoice_callback and not preimage:
            callback = lambda: hold_invoice_callback(payment_hash)
            return None, (payment_key, callback)

        if not preimage:
            if not already_forwarded:
                log_fail_reason(f"missing preimage and no hold invoice callback {payment_hash.hex()}")
                raise exc_incorrect_or_unknown_pd
            else:
                return None, None

        if payment_hash.hex() in self.lnworker.dont_settle_htlcs:
            return None, None

        chan.opening_fee = None
        self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}")
        return preimage, None

        Decide what to do with an HTLC: return preimage if it can be fulfilled, forwarding callback if it can be forwarded.
        Return (preimage, (payment_key, callback)) with at most a single element not None.
        
electrum/lnpeer.py