Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uma data visibility #44

Merged
merged 4 commits into from
Jun 28, 2024
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
40 changes: 40 additions & 0 deletions lightspark/__tests__/test_uma_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import datetime
import logging
from unittest.mock import patch
from lightspark import LightsparkSyncClient

logger = logging.getLogger("lightspark")
logger.setLevel(logging.DEBUG)


class TestUmaUtils:
@patch("lightspark.lightspark_client.datetime")
def test_hash_uma_identifier_same_month(self, mock_datetime):
client = LightsparkSyncClient("", "")
priv_key_bytes = b"xyz"
mock_datetime.now.return_value = datetime.datetime(2021, 1, 1, 0, 0, 0)

hashed_uma = client.hash_uma_identifier("[email protected]", priv_key_bytes)
hashed_uma_same_month = client.hash_uma_identifier(
"[email protected]", priv_key_bytes
)

logger.debug(hashed_uma)
assert hashed_uma_same_month == hashed_uma

@patch("lightspark.lightspark_client.datetime")
def test_hash_uma_identifier_different_month(self, mock_datetime):
client = LightsparkSyncClient("", "")
priv_key_bytes = b"xyz"

mock_datetime.now.return_value = datetime.datetime(2021, 1, 1, 0, 0, 0)
hashed_uma = client.hash_uma_identifier("[email protected]", priv_key_bytes)

mock_datetime.now.return_value = datetime.datetime(2021, 2, 1, 0, 0, 0)
hashed_uma_diff_month = client.hash_uma_identifier(
"[email protected]", priv_key_bytes
)

logger.debug(hashed_uma)
logger.debug(hashed_uma_diff_month)
assert hashed_uma_diff_month != hashed_uma
79 changes: 73 additions & 6 deletions lightspark/lightspark_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,47 @@ def create_uma_invoice(
amount_msats: int,
metadata: str,
expiry_secs: Optional[int] = None,
signing_private_key: Optional[bytes] = None,
receiver_identifier: Optional[str] = None,
) -> Invoice:
"""Creates a new invoice for the UMA protocol. The metadata is hashed and included in the invoice. This API
generates a Lightning Invoice (follows the Bolt 11 specification) to request a payment from another Lightning Node.
This should only be used for generating invoices for UMA, with `create_invoice` preferred in the general case.

Args:
node_id: The node ID for which to create an invoice.
amount_msats: The amount of the invoice in msats. You can create a zero-amount invoice to accept any payment amount.
metadata: The LNURL metadata payload field in the initial payreq response. This wil be hashed and present in the
h-tag (SHA256 purpose of payment) of the resulting Bolt 11 invoice. See
[this spec](https://github.com/lnurl/luds/blob/luds/06.md#pay-to-static-qrnfclink) for details.
expiry_secs: The number of seconds until the invoice expires. Defaults to 600.
signing_private_key: The receiver's signing private key. Used to hash the receiver identifier.
receiver_identifier: Optional identifier of the receiver. If provided, this will be hashed using a monthly-rotated
seed and used for anonymized analysis.
"""
receiver_hash = None
if receiver_identifier is not None:
if signing_private_key is None:
raise LightsparkException(
"CreateUmaInvoiceError",
"Receiver identifier provided without signing private key",
)
receiver_hash = self.hash_uma_identifier(
receiver_identifier, signing_private_key
)

variables = {
"amount_msats": amount_msats,
"node_id": node_id,
"metadata_hash": sha256(metadata.encode("utf-8")).hexdigest(),
"expiry_secs": expiry_secs if expiry_secs is not None else 600,
}
if receiver_hash is not None:
variables["receiver_hash"] = receiver_hash
logger.info("Creating an uma invoice for node %s.", node_id)
json = self._requester.execute_graphql(
CREATE_UMA_INVOICE_MUTATION,
{
"amount_msats": amount_msats,
"node_id": node_id,
"metadata_hash": sha256(metadata.encode("utf-8")).hexdigest(),
"expiry_secs": expiry_secs if expiry_secs is not None else 600,
},
variables,
)

return Invoice_from_json(self._requester, json["create_uma_invoice"]["invoice"])
Expand Down Expand Up @@ -530,7 +561,36 @@ def pay_uma_invoice(
maximum_fees_msats: int,
amount_msats: Optional[int] = None,
idempotency_key: Optional[str] = None,
signing_private_key: Optional[bytes] = None,
sender_identifier: Optional[str] = None,
) -> OutgoingPayment:
"""Sends an UMA payment to a node on the Lightning Network, based on the invoice (as defined by the BOLT11
specification) that you provide. This should only be used for paying UMA invoices, with `pay_invoice` preferred
in the general case.

Args:
node_id: The ID of the node that will pay the invoice.
encoded_invoice: The encoded invoice to pay.
timeout_secs: A timeout for the payment in seconds.
maximum_fees_msats: Maximum fees (in msats) to pay for the payment.
amount_msats: The amount to pay in msats for a zero-amount invoice. Defaults to the full amount of the
invoice. Note, this parameter can only be passed for a zero-amount invoice. Otherwise, the call will fail.
idempotency_key: An optional key to ensure idempotency of the payment.
signing_private_key: The sender's signing private key. Used to hash the sender identifier.
sender_identifier: Optional identifier of the sender. If provided, this will be hashed using a monthly-rotated
seed and used for anonymized analysis.
"""
sender_hash = None
if sender_identifier is not None:
if signing_private_key is None:
raise LightsparkException(
"PayUmaInvoiceError",
"Sender identifier provided without signing private key",
)
sender_hash = self.hash_uma_identifier(
sender_identifier, signing_private_key
)

variables = {
"node_id": node_id,
"encoded_invoice": encoded_invoice,
Expand All @@ -541,6 +601,8 @@ def pay_uma_invoice(
variables["amount_msats"] = amount_msats
if idempotency_key is not None:
variables["idempotency_key"] = idempotency_key
if sender_hash is not None:
variables["sender_hash"] = sender_hash
json = self._requester.execute_graphql(
PAY_UMA_INVOICE_MUTATION,
variables,
Expand Down Expand Up @@ -933,6 +995,11 @@ def _hash_phone_number(self, phone_number_e164_format: str) -> str:
)
return sha256(phone_number_e164_format.encode()).hexdigest()

def hash_uma_identifier(self, identifier: str, signing_private_key: bytes) -> str:
now = datetime.now(timezone.utc)
input_data = identifier + f"{now.month}-{now.year}" + signing_private_key.hex()
return sha256(input_data.encode()).hexdigest()

def fail_htlcs(self, invoice_id: str, cancel_invoice: bool = True) -> str:
"""
Fails all pending HTLCs associated with an invoice.
Expand Down
2 changes: 2 additions & 0 deletions lightspark/scripts/create_uma_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
$amount_msats: Long!
$metadata_hash: String!
$expiry_secs: Int
$receiver_hash: String = null
) {{
create_uma_invoice(input: {{
node_id: $node_id
amount_msats: $amount_msats
metadata_hash: $metadata_hash
expiry_secs: $expiry_secs
receiver_hash: $receiver_hash
}}) {{
invoice {{
...InvoiceFragment
Expand Down
2 changes: 2 additions & 0 deletions lightspark/scripts/pay_uma_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
$maximum_fees_msats: Long!
$amount_msats: Long
$idempotency_key: String
$sender_hash: String = null
) {{
pay_uma_invoice(input: {{
node_id: $node_id
Expand All @@ -18,6 +19,7 @@
maximum_fees_msats: $maximum_fees_msats
amount_msats: $amount_msats
idempotency_key: $idempotency_key
sender_hash: $sender_hash
}}) {{
payment {{
...OutgoingPaymentFragment
Expand Down
Loading