|
27 | 27 | import dns.exception |
28 | 28 | from aiorpcx import run_in_thread, NetAddress, ignore_after |
29 | 29 |
|
| 30 | +from .bolt12 import encode_invoice |
30 | 31 | from .logging import Logger |
31 | 32 | from .i18n import _ |
32 | 33 | from .json_db import stored_in |
|
55 | 56 | sha256, chacha20_encrypt, chacha20_decrypt, pw_encode_with_version_and_mac, pw_decode_with_version_and_mac |
56 | 57 | ) |
57 | 58 |
|
58 | | -from .onion_message import OnionMessageManager |
| 59 | +from .onion_message import OnionMessageManager, send_onion_message_to, encode_blinded_path |
59 | 60 | from .lntransport import ( |
60 | 61 | LNTransport, LNResponderTransport, LNTransportBase, LNPeerAddr, split_host_port, extract_nodeid, |
61 | 62 | ConnStringFormatError |
@@ -3782,3 +3783,77 @@ def get_forwarding_failure(self, payment_key: str) -> Tuple[Optional[bytes], Opt |
3782 | 3783 | error_bytes = bytes.fromhex(error_hex) if error_hex else None |
3783 | 3784 | failure_message = OnionRoutingFailure.from_bytes(bytes.fromhex(failure_hex)) if failure_hex else None |
3784 | 3785 | return error_bytes, failure_message |
| 3786 | + |
| 3787 | + def on_bolt12_invoice_request(self, recipient_data: dict, payload: dict): |
| 3788 | + # match to offer |
| 3789 | + self.logger.debug(f'on_bolt12_invoice_request: {recipient_data=} {payload=}') |
| 3790 | + offer_id = recipient_data['path_id']['data'] |
| 3791 | + offer = self.wallet.get_offer(offer_id) |
| 3792 | + if offer is None: |
| 3793 | + self.logger.warning('no matching offer for invoice_request') |
| 3794 | + return |
| 3795 | + |
| 3796 | + invreq_tlv = payload['invoice_request']['invoice_request'] |
| 3797 | + invreq = bolt12.decode_invoice_request(invreq_tlv) |
| 3798 | + self.logger.info(f'invoice_request: {invreq=}') |
| 3799 | + self.logger.debug(f'invoice_request for {offer=}') |
| 3800 | + |
| 3801 | + # two scenarios: |
| 3802 | + # 1) not in response to offer (no offer_issuer_id or offer_paths) |
| 3803 | + # MUST reject the invoice request if any of the following are present: |
| 3804 | + # offer_chains, offer_features or offer_quantity_max. |
| 3805 | + # MUST reject the invoice request if invreq_amount is not present. |
| 3806 | + # MAY use offer_amount (or offer_currency) for informational display to user. |
| 3807 | + # if it sends an invoice in response: |
| 3808 | + # MUST use invreq_paths if present, otherwise MUST use invreq_payer_id as the node id to send to. |
| 3809 | + |
| 3810 | + # 2) response to offer. |
| 3811 | + # |
| 3812 | + # MUST reject the invoice request if the offer fields do not exactly match a valid, unexpired offer. |
| 3813 | + # if offer_paths is present: |
| 3814 | + # MUST ignore the invoice_request if it did not arrive via one of those paths. |
| 3815 | + # otherwise: |
| 3816 | + # MUST ignore any invoice_request if it arrived via a blinded path. |
| 3817 | + # if offer_quantity_max is present: |
| 3818 | + # MUST reject the invoice request if there is no invreq_quantity field. |
| 3819 | + # if offer_quantity_max is non-zero: |
| 3820 | + # MUST reject the invoice request if invreq_quantity is zero, OR greater than offer_quantity_max. |
| 3821 | + # otherwise: |
| 3822 | + # MUST reject the invoice request if there is an invreq_quantity field. |
| 3823 | + # if offer_amount is present: |
| 3824 | + # MUST calculate the expected amount using the offer_amount: |
| 3825 | + # if offer_currency is not the invreq_chain currency, convert to the invreq_chain currency. |
| 3826 | + # if invreq_quantity is present, multiply by invreq_quantity.quantity. |
| 3827 | + # if invreq_amount is present: |
| 3828 | + # MUST reject the invoice request if invreq_amount.msat is less than the expected amount. |
| 3829 | + # MAY reject the invoice request if invreq_amount.msat greatly exceeds the expected amount. |
| 3830 | + # otherwise (no offer_amount): |
| 3831 | + # MUST reject the invoice request if it does not contain invreq_amount. |
| 3832 | + # SHOULD send an invoice in response using the onionmsg_tlv reply_path. |
| 3833 | + |
| 3834 | + is_response_to_offer = True # always assume scenario 2 for now |
| 3835 | + |
| 3836 | + if is_response_to_offer: |
| 3837 | + node_id_or_blinded_path = encode_blinded_path(payload['reply_path']['path']) |
| 3838 | + else: |
| 3839 | + # spec: MUST use invreq_paths if present, otherwise MUST use invreq_payer_id as the node id to send to. |
| 3840 | + if 'invreq_paths' in invreq: |
| 3841 | + node_id_or_blinded_path = invreq['invreq_paths']['paths'][0] # take first |
| 3842 | + else: |
| 3843 | + node_id_or_blinded_path = invreq['invreq_payer_id'] |
| 3844 | + |
| 3845 | + preimage = os.urandom(32) |
| 3846 | + invoice = bolt12.verify_request_and_create_invoice(self, offer, invreq, preimage) |
| 3847 | + if invoice is None: # TODO: use exception, not None |
| 3848 | + self.logger.error('could not generate invoice') |
| 3849 | + # TODO: send invoice_error if request not totally bogus |
| 3850 | + return |
| 3851 | + invoice_payment_hash = sha256(preimage) |
| 3852 | + self.save_preimage(invoice_payment_hash, preimage) |
| 3853 | + |
| 3854 | + destination_payload = { |
| 3855 | + 'invoice': {'invoice': encode_invoice(invoice, self.node_keypair.privkey)} |
| 3856 | + } |
| 3857 | + |
| 3858 | + send_onion_message_to(self, node_id_or_blinded_path, destination_payload) |
| 3859 | + |
0 commit comments