Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: "ubuntu-22.04"
strategy:
matrix:
python_version: ["3.8", "3.10"]
python_version: ["3.9", "3.10"]
steps:
- name: "Checkout"
uses: "actions/checkout@v3"
Expand Down
4 changes: 4 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ verify_ssl = true
name = "pypi"

[packages]
# Pin to avoid pyre issues:
aiohttp = "<3.10"
async-timeout = "*"
bech32 = "*"
coincurve = "*"
cryptography = "*"
Expand All @@ -19,6 +22,7 @@ isort = "==5.11.4"
pylint = "*"
pyre-check = "*"
pytest = "*"
pytest-asyncio = "*"
tomli = "*"
wrapt = "*"
# Need to lock this version to avoid breaking older python versions:
Expand Down
534 changes: 516 additions & 18 deletions Pipfile.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@

import setuptools

setuptools.setup(include_package_data=True)
setuptools.setup(
include_package_data=True,
python_requires=">=3.9",
)
7 changes: 6 additions & 1 deletion uma/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
)
from uma.protocol.post_tx_callback import PostTransactionCallback, UtxoWithAmount
from uma.protocol.pubkey_response import PubkeyResponse
from uma.public_key_cache import InMemoryPublicKeyCache, IPublicKeyCache
from uma.public_key_cache import (
InMemoryPublicKeyCache,
IPublicKeyCache,
IAsyncPublicKeyCache,
)
from uma.signing_utils import sign_payload
from uma.type_utils import none_throws
from uma.uma import (
Expand All @@ -47,6 +51,7 @@
create_uma_lnurlp_request_url,
create_uma_lnurlp_response,
fetch_public_key_for_vasp,
fetch_public_key_for_vasp_async,
generate_nonce,
get_vasp_domain_from_uma_address,
is_uma_lnurlp_query,
Expand Down
48 changes: 47 additions & 1 deletion uma/__tests__/test_uma.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from uma.protocol.payer_data import compliance_from_payer_data
from uma.nonce_cache import InMemoryNonceCache
from uma.protocol.pubkey_response import PubkeyResponse
from uma.public_key_cache import InMemoryPublicKeyCache
from uma.public_key_cache import InMemoryPublicKeyCache, IAsyncPublicKeyCache
from uma.type_utils import none_throws
from uma.protocol.post_tx_callback import UtxoWithAmount
from uma.protocol.v0.payreq import PayRequest as V0PayRequest
Expand All @@ -33,6 +33,7 @@
create_pay_request,
create_post_transaction_callback,
fetch_public_key_for_vasp,
fetch_public_key_for_vasp_async,
is_uma_lnurlp_query,
parse_lnurlp_request,
parse_lnurlp_response,
Expand Down Expand Up @@ -77,6 +78,30 @@ def test_fetch_public_key() -> None:
assert cache.fetch_public_key_for_vasp(vasp_domain) == expected_pubkey


@pytest.mark.asyncio
async def test_fetch_public_key_async() -> None:
cache = TestAsyncPublicKeyCache()
vasp_domain = "vasp2.com"
timestamp = int((datetime.now(timezone.utc) + timedelta(hours=1)).timestamp())
expected_pubkey = PubkeyResponse(
signing_pubkey=secrets.token_bytes(16),
encryption_pubkey=secrets.token_bytes(16),
expiration_timestamp=datetime.fromtimestamp(timestamp, timezone.utc),
encryption_cert_chain=None,
signing_cert_chain=None,
)
url = "https://vasp2.com/.well-known/lnurlpubkey"

with patch(
"uma.uma._run_http_get_async",
return_value=json.dumps(expected_pubkey.to_dict()),
) as mock:
pubkey_response = await fetch_public_key_for_vasp_async(vasp_domain, cache)
mock.assert_called_once_with(url)
assert pubkey_response == expected_pubkey
assert await cache.fetch_public_key_for_vasp(vasp_domain) == expected_pubkey


def _create_pubkey_response(
signing_private_key: PrivateKey, encryption_private_key: PrivateKey
) -> PubkeyResponse:
Expand Down Expand Up @@ -1218,3 +1243,24 @@ def test_uma_invoice_signature() -> None:

assert verify_uma_invoice_signature(invoice, pubkey_response) is None
assert invoice.signature is not None


class TestAsyncPublicKeyCache(IAsyncPublicKeyCache):
def __init__(self) -> None:
self._cache = InMemoryPublicKeyCache()

async def fetch_public_key_for_vasp(
self, vasp_domain: str
) -> Optional[PubkeyResponse]:
return self._cache.fetch_public_key_for_vasp(vasp_domain)

async def add_public_key_for_vasp(
self, vasp_domain: str, public_key: PubkeyResponse
) -> None:
self._cache.add_public_key_for_vasp(vasp_domain, public_key)

async def remove_public_key_for_vasp(self, vasp_domain: str) -> None:
self._cache.remove_public_key_for_vasp(vasp_domain)

async def clear(self) -> None:
self._cache.clear()
6 changes: 3 additions & 3 deletions uma/protocol/payreq_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,9 @@ def append_backing_signature(
BackingSignature(domain=domain, signature=backing_signature)
)
compliance.backing_signatures = backing_signatures
if self.payee_data is None:
self.payee_data = {}
self.payee_data["compliance"] = compliance.to_dict()
payee_data = self.payee_data or {}
payee_data["compliance"] = compliance.to_dict()
self.payee_data = payee_data

def to_dict(self) -> Dict[str, Any]:
resp = super().to_dict()
Expand Down
22 changes: 22 additions & 0 deletions uma/public_key_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,25 @@ def remove_public_key_for_vasp(self, vasp_domain: str) -> None:

def clear(self) -> None:
self._cache.clear()


class IAsyncPublicKeyCache(ABC):
@abstractmethod
async def fetch_public_key_for_vasp(
self, vasp_domain: str
) -> Optional[PubkeyResponse]:
pass

@abstractmethod
async def add_public_key_for_vasp(
self, vasp_domain: str, public_key: PubkeyResponse
) -> None:
pass

@abstractmethod
async def remove_public_key_for_vasp(self, vasp_domain: str) -> None:
pass

@abstractmethod
async def clear(self) -> None:
pass
31 changes: 30 additions & 1 deletion uma/uma.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from uuid import uuid4

import requests
from aiohttp import ClientSession
from coincurve.ecdsa import cdata_to_der, der_to_cdata, signature_normalize
from coincurve.keys import PublicKey
from cryptography.exceptions import InvalidSignature
Expand Down Expand Up @@ -52,7 +53,7 @@
)
from uma.protocol.post_tx_callback import PostTransactionCallback, UtxoWithAmount
from uma.protocol.pubkey_response import PubkeyResponse
from uma.public_key_cache import IPublicKeyCache
from uma.public_key_cache import IPublicKeyCache, IAsyncPublicKeyCache
from uma.signing_utils import sign_payload
from uma.type_utils import none_throws
from uma.uma_invoice_creator import IUmaInvoiceCreator
Expand Down Expand Up @@ -86,6 +87,27 @@ def fetch_public_key_for_vasp(
return public_key


async def fetch_public_key_for_vasp_async(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care about using the gen_* convention here like we do internally? I don't have a strong preference, but was curious if anyone else does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yunyuyunyu do you have any thoughts?
from quick googling it doesn't seem like a common convention so I almost prefer not to, but also not a strong preference

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question. I actually don't see many external libraries using prefix gen for their async functions. Hmm, @vdurmont do you have a preference?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly for external stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks all -- going to leave as is then!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'd lean towards keeping our consistency and use gen_. I acknowledge it's not widely used in the outside world, but it doesn't hurt at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will update!

vasp_domain: str, cache: IAsyncPublicKeyCache
) -> PubkeyResponse:
public_key = await cache.fetch_public_key_for_vasp(vasp_domain)
if public_key:
return public_key

scheme = "http://" if is_domain_local(vasp_domain) else "https://"
url = scheme + vasp_domain + "/.well-known/lnurlpubkey"
try:
response_text = await _run_http_get_async(url)
except Exception as ex:
raise InvalidRequestException(
f"Unable to fetch pubkey from {vasp_domain}. Make sure the vasp domain is correct."
) from ex

public_key = PubkeyResponse.from_json(response_text)
await cache.add_public_key_for_vasp(vasp_domain, public_key)
return public_key


def create_pubkey_response(
signing_cert_chain: str,
encryption_cert_chain: str,
Expand Down Expand Up @@ -124,6 +146,13 @@ def _run_http_get(url: str) -> str:
return response.text


async def _run_http_get_async(url: str) -> str:
async with ClientSession() as session:
async with session.get(url) as response:
response.raise_for_status()
return await response.text()


def generate_nonce() -> str:
return str(random.randint(0, 0xFFFFFFFF))

Expand Down