Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ verify_ssl = true
name = "pypi"

[packages]
aiohttp = "*"
bech32 = "*"
coincurve = "*"
cryptography = "*"
Expand All @@ -19,6 +20,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
1,840 changes: 1,195 additions & 645 deletions Pipfile.lock

Large diffs are not rendered by default.

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: # pyre-ignore [16]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is the pyre error

uma/uma.py:152:19 Undefined attribute [16]: `aiohttp.client.ClientSession` has no attribute `get`.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we might need to pin to 3.10 on aiohttp due to pyre incompatibility aio-libs/aiohttp#8463

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that fixed it, thanks!

response.raise_for_status()
return await response.text()


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

Expand Down
Loading