Skip to content

Commit ab6acf8

Browse files
authored
Uma data visibility (#44)
1 parent f7d2a6b commit ab6acf8

File tree

4 files changed

+117
-6
lines changed

4 files changed

+117
-6
lines changed
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import datetime
2+
import logging
3+
from unittest.mock import patch
4+
from lightspark import LightsparkSyncClient
5+
6+
logger = logging.getLogger("lightspark")
7+
logger.setLevel(logging.DEBUG)
8+
9+
10+
class TestUmaUtils:
11+
@patch("lightspark.lightspark_client.datetime")
12+
def test_hash_uma_identifier_same_month(self, mock_datetime):
13+
client = LightsparkSyncClient("", "")
14+
priv_key_bytes = b"xyz"
15+
mock_datetime.now.return_value = datetime.datetime(2021, 1, 1, 0, 0, 0)
16+
17+
hashed_uma = client.hash_uma_identifier("[email protected]", priv_key_bytes)
18+
hashed_uma_same_month = client.hash_uma_identifier(
19+
"[email protected]", priv_key_bytes
20+
)
21+
22+
logger.debug(hashed_uma)
23+
assert hashed_uma_same_month == hashed_uma
24+
25+
@patch("lightspark.lightspark_client.datetime")
26+
def test_hash_uma_identifier_different_month(self, mock_datetime):
27+
client = LightsparkSyncClient("", "")
28+
priv_key_bytes = b"xyz"
29+
30+
mock_datetime.now.return_value = datetime.datetime(2021, 1, 1, 0, 0, 0)
31+
hashed_uma = client.hash_uma_identifier("[email protected]", priv_key_bytes)
32+
33+
mock_datetime.now.return_value = datetime.datetime(2021, 2, 1, 0, 0, 0)
34+
hashed_uma_diff_month = client.hash_uma_identifier(
35+
"[email protected]", priv_key_bytes
36+
)
37+
38+
logger.debug(hashed_uma)
39+
logger.debug(hashed_uma_diff_month)
40+
assert hashed_uma_diff_month != hashed_uma

lightspark/lightspark_client.py

+73-6
Original file line numberDiff line numberDiff line change
@@ -303,16 +303,47 @@ def create_uma_invoice(
303303
amount_msats: int,
304304
metadata: str,
305305
expiry_secs: Optional[int] = None,
306+
signing_private_key: Optional[bytes] = None,
307+
receiver_identifier: Optional[str] = None,
306308
) -> Invoice:
309+
"""Creates a new invoice for the UMA protocol. The metadata is hashed and included in the invoice. This API
310+
generates a Lightning Invoice (follows the Bolt 11 specification) to request a payment from another Lightning Node.
311+
This should only be used for generating invoices for UMA, with `create_invoice` preferred in the general case.
312+
313+
Args:
314+
node_id: The node ID for which to create an invoice.
315+
amount_msats: The amount of the invoice in msats. You can create a zero-amount invoice to accept any payment amount.
316+
metadata: The LNURL metadata payload field in the initial payreq response. This wil be hashed and present in the
317+
h-tag (SHA256 purpose of payment) of the resulting Bolt 11 invoice. See
318+
[this spec](https://github.com/lnurl/luds/blob/luds/06.md#pay-to-static-qrnfclink) for details.
319+
expiry_secs: The number of seconds until the invoice expires. Defaults to 600.
320+
signing_private_key: The receiver's signing private key. Used to hash the receiver identifier.
321+
receiver_identifier: Optional identifier of the receiver. If provided, this will be hashed using a monthly-rotated
322+
seed and used for anonymized analysis.
323+
"""
324+
receiver_hash = None
325+
if receiver_identifier is not None:
326+
if signing_private_key is None:
327+
raise LightsparkException(
328+
"CreateUmaInvoiceError",
329+
"Receiver identifier provided without signing private key",
330+
)
331+
receiver_hash = self.hash_uma_identifier(
332+
receiver_identifier, signing_private_key
333+
)
334+
335+
variables = {
336+
"amount_msats": amount_msats,
337+
"node_id": node_id,
338+
"metadata_hash": sha256(metadata.encode("utf-8")).hexdigest(),
339+
"expiry_secs": expiry_secs if expiry_secs is not None else 600,
340+
}
341+
if receiver_hash is not None:
342+
variables["receiver_hash"] = receiver_hash
307343
logger.info("Creating an uma invoice for node %s.", node_id)
308344
json = self._requester.execute_graphql(
309345
CREATE_UMA_INVOICE_MUTATION,
310-
{
311-
"amount_msats": amount_msats,
312-
"node_id": node_id,
313-
"metadata_hash": sha256(metadata.encode("utf-8")).hexdigest(),
314-
"expiry_secs": expiry_secs if expiry_secs is not None else 600,
315-
},
346+
variables,
316347
)
317348

318349
return Invoice_from_json(self._requester, json["create_uma_invoice"]["invoice"])
@@ -530,7 +561,36 @@ def pay_uma_invoice(
530561
maximum_fees_msats: int,
531562
amount_msats: Optional[int] = None,
532563
idempotency_key: Optional[str] = None,
564+
signing_private_key: Optional[bytes] = None,
565+
sender_identifier: Optional[str] = None,
533566
) -> OutgoingPayment:
567+
"""Sends an UMA payment to a node on the Lightning Network, based on the invoice (as defined by the BOLT11
568+
specification) that you provide. This should only be used for paying UMA invoices, with `pay_invoice` preferred
569+
in the general case.
570+
571+
Args:
572+
node_id: The ID of the node that will pay the invoice.
573+
encoded_invoice: The encoded invoice to pay.
574+
timeout_secs: A timeout for the payment in seconds.
575+
maximum_fees_msats: Maximum fees (in msats) to pay for the payment.
576+
amount_msats: The amount to pay in msats for a zero-amount invoice. Defaults to the full amount of the
577+
invoice. Note, this parameter can only be passed for a zero-amount invoice. Otherwise, the call will fail.
578+
idempotency_key: An optional key to ensure idempotency of the payment.
579+
signing_private_key: The sender's signing private key. Used to hash the sender identifier.
580+
sender_identifier: Optional identifier of the sender. If provided, this will be hashed using a monthly-rotated
581+
seed and used for anonymized analysis.
582+
"""
583+
sender_hash = None
584+
if sender_identifier is not None:
585+
if signing_private_key is None:
586+
raise LightsparkException(
587+
"PayUmaInvoiceError",
588+
"Sender identifier provided without signing private key",
589+
)
590+
sender_hash = self.hash_uma_identifier(
591+
sender_identifier, signing_private_key
592+
)
593+
534594
variables = {
535595
"node_id": node_id,
536596
"encoded_invoice": encoded_invoice,
@@ -541,6 +601,8 @@ def pay_uma_invoice(
541601
variables["amount_msats"] = amount_msats
542602
if idempotency_key is not None:
543603
variables["idempotency_key"] = idempotency_key
604+
if sender_hash is not None:
605+
variables["sender_hash"] = sender_hash
544606
json = self._requester.execute_graphql(
545607
PAY_UMA_INVOICE_MUTATION,
546608
variables,
@@ -933,6 +995,11 @@ def _hash_phone_number(self, phone_number_e164_format: str) -> str:
933995
)
934996
return sha256(phone_number_e164_format.encode()).hexdigest()
935997

998+
def hash_uma_identifier(self, identifier: str, signing_private_key: bytes) -> str:
999+
now = datetime.now(timezone.utc)
1000+
input_data = identifier + f"{now.month}-{now.year}" + signing_private_key.hex()
1001+
return sha256(input_data.encode()).hexdigest()
1002+
9361003
def fail_htlcs(self, invoice_id: str, cancel_invoice: bool = True) -> str:
9371004
"""
9381005
Fails all pending HTLCs associated with an invoice.

lightspark/scripts/create_uma_invoice.py

+2
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
$amount_msats: Long!
99
$metadata_hash: String!
1010
$expiry_secs: Int
11+
$receiver_hash: String = null
1112
) {{
1213
create_uma_invoice(input: {{
1314
node_id: $node_id
1415
amount_msats: $amount_msats
1516
metadata_hash: $metadata_hash
1617
expiry_secs: $expiry_secs
18+
receiver_hash: $receiver_hash
1719
}}) {{
1820
invoice {{
1921
...InvoiceFragment

lightspark/scripts/pay_uma_invoice.py

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
$maximum_fees_msats: Long!
1111
$amount_msats: Long
1212
$idempotency_key: String
13+
$sender_hash: String = null
1314
) {{
1415
pay_uma_invoice(input: {{
1516
node_id: $node_id
@@ -18,6 +19,7 @@
1819
maximum_fees_msats: $maximum_fees_msats
1920
amount_msats: $amount_msats
2021
idempotency_key: $idempotency_key
22+
sender_hash: $sender_hash
2123
}}) {{
2224
payment {{
2325
...OutgoingPaymentFragment

0 commit comments

Comments
 (0)