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

feat: make requests async #32

Merged
merged 3 commits into from
Apr 24, 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
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,26 +53,28 @@ The different types of responses defined in the [LNURL spec][lnurl-spec] have a
with different properties (see `models.py`):

```python
import requests
import httpx

from lnurl import Lnurl, LnurlResponse

lnurl = Lnurl('LNURL1DP68GURN8GHJ7MRWW4EXCTNZD9NHXATW9EU8J730D3H82UNV94MKJARGV3EXZAELWDJHXUMFDAHR6WFHXQERSVPCA649RV')
try:
async with httpx.AsyncClient() as client:
r = await client.get(lnurl.url)
res = LnurlResponse.from_dict(r.json()) # LnurlPayResponse
res.ok # bool
res.max_sendable # int
res.max_sats # int
res.callback.base # str
res.callback.query_params # dict
res.metadata # str
res.metadata.list() # list
res.metadata.text # str
res.metadata.images # list
r = requests.get(lnurl.url)

res = LnurlResponse.from_dict(r.json()) # LnurlPayResponse
res.ok # bool
res.max_sendable # int
res.max_sats # int
res.callback.base # str
res.callback.query_params # dict
res.metadata # str
res.metadata.list() # list
res.metadata.text # str
res.metadata.images # list
```

If you have already `requests` installed, you can also use the `.handle()` function directly.
If you have already `httpx` installed, you can also use the `.handle()` function directly.
It will return the appropriate response for a LNURL.

```python
Expand All @@ -81,6 +83,12 @@ It will return the appropriate response for a LNURL.
LnurlPayResponse(tag='payRequest', callback=WebUrl('https://lnurl.bigsun.xyz/lnurl-pay/callback/2169831', scheme='https', host='lnurl.bigsun.xyz', tld='xyz', host_type='domain', path='/lnurl-pay/callback/2169831'), min_sendable=10000, max_sendable=10000, metadata=LnurlPayMetadata('[["text/plain","NgHaEyaZNDnW iI DsFYdkI"],["image/png;base64","iVBOR...uQmCC"]]'))
```

You can execute and LNURL with either payRequest, withdrawRequest or login tag using the `execute` function.
```python
>>> import lnurl
>>> lnurl.execute('lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNZD9NHXATW9EU8J730D3H82UNV94CXZ7FLWDJHXUMFDAHR6V33XCUNSVE38QV6UF', 100000)
```

Building your own LNURL responses
---------------------------------

Expand Down Expand Up @@ -142,5 +150,5 @@ Commands:
decode decode a LNURL
encode encode a URL
handle handle a LNURL
payment-request make a payment_request
execute execute a LNURL
```
5 changes: 3 additions & 2 deletions lnurl/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""lnurl CLI"""

import asyncio
import sys

import click
Expand Down Expand Up @@ -46,7 +47,7 @@ def handle(lnurl):
"""
handle a LNURL
"""
decoded = handle_lnurl(lnurl)
decoded = asyncio.run(handle_lnurl(lnurl))
click.echo(decoded.json())


Expand All @@ -59,7 +60,7 @@ def execute(lnurl, msat_or_login):
"""
if not msat_or_login:
raise ValueError("You must provide either an amount_msat or a login_id.")
res = execute_lnurl(lnurl, msat_or_login)
res = asyncio.run(execute_lnurl(lnurl, msat_or_login))
click.echo(res.json())


Expand Down
99 changes: 51 additions & 48 deletions lnurl/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Optional, Union

import requests
import httpx
from bolt11 import Bolt11Exception, MilliSatoshi
from bolt11 import decode as bolt11_decode
from pydantic import ValidationError
Expand All @@ -25,89 +25,91 @@ def encode(url: str) -> Lnurl:
raise InvalidUrl


def get(url: str, *, response_class: Optional[Any] = None, verify: Union[str, bool] = True) -> LnurlResponseModel:
try:
req = requests.get(url, verify=verify)
req.raise_for_status()
except Exception as e:
raise LnurlResponseException(str(e))
async def get(url: str, *, response_class: Optional[Any] = None) -> LnurlResponseModel:
async with httpx.AsyncClient() as client:
try:
res = await client.get(url)
res.raise_for_status()
except Exception as e:
raise LnurlResponseException(str(e))

if response_class:
assert issubclass(response_class, LnurlResponseModel), "Use a valid `LnurlResponseModel` subclass."
return response_class(**req.json())
if response_class:
assert issubclass(response_class, LnurlResponseModel), "Use a valid `LnurlResponseModel` subclass."
return response_class(**res.json())

return LnurlResponse.from_dict(req.json())
return LnurlResponse.from_dict(res.json())


def handle(
async def handle(
bech32_lnurl: str,
response_class: Optional[LnurlResponseModel] = None,
verify: Union[str, bool] = True,
) -> LnurlResponseModel:
try:
if "@" in bech32_lnurl:
lnaddress = LnAddress(bech32_lnurl)
return get(lnaddress.url, response_class=response_class, verify=verify)
return await get(lnaddress.url, response_class=response_class)
lnurl = Lnurl(bech32_lnurl)
except (ValidationError, ValueError):
raise InvalidLnurl

if lnurl.is_login:
return LnurlAuthResponse(callback=lnurl.url, k1=lnurl.url.query_params["k1"])

return get(lnurl.url, response_class=response_class, verify=verify)
return await get(lnurl.url, response_class=response_class)


def execute(bech32_or_address: str, value: str) -> LnurlResponseModel:
async def execute(bech32_or_address: str, value: str) -> LnurlResponseModel:
try:
res = handle(bech32_or_address)
except Exception as exc:
raise LnurlResponseException(str(exc))

if isinstance(res, LnurlPayResponse) and res.tag == "payRequest":
return execute_pay_request(res, value)
return await execute_pay_request(res, value)
elif isinstance(res, LnurlAuthResponse) and res.tag == "login":
return execute_login(res, value)
return await execute_login(res, value)
elif isinstance(res, LnurlWithdrawResponse) and res.tag == "withdrawRequest":
return execute_withdraw(res, value)
return await execute_withdraw(res, value)

raise LnurlResponseException(f"{res.tag} not implemented") # type: ignore


def execute_pay_request(res: LnurlPayResponse, msat: str) -> LnurlResponseModel:
async def execute_pay_request(res: LnurlPayResponse, msat: str) -> LnurlResponseModel:
if not res.min_sendable <= MilliSatoshi(msat) <= res.max_sendable:
raise LnurlResponseException(f"Amount {msat} not in range {res.min_sendable} - {res.max_sendable}")
try:
req = requests.get(
res.callback,
params={
"amount": msat,
},
)
req.raise_for_status()
return LnurlResponse.from_dict(req.json())
async with httpx.AsyncClient() as client:
res2 = await client.get(
dni marked this conversation as resolved.
Show resolved Hide resolved
url=res.callback,
params={
"amount": msat,
},
)
res2.raise_for_status()
return LnurlResponse.from_dict(res2.json())
except Exception as exc:
raise LnurlResponseException(str(exc))


def execute_login(res: LnurlAuthResponse, secret: str) -> LnurlResponseModel:
async def execute_login(res: LnurlAuthResponse, secret: str) -> LnurlResponseModel:
try:
assert res.callback.host, "LNURLauth host does not exist"
key, sig = lnurlauth_signature(res.callback.host, secret, res.k1)
req = requests.get(
res.callback,
params={
"key": key,
"sig": sig,
},
)
req.raise_for_status()
return LnurlResponse.from_dict(req.json())
async with httpx.AsyncClient() as client:
res2 = await client.get(
url=res.callback,
params={
"key": key,
"sig": sig,
},
)
res2.raise_for_status()
return LnurlResponse.from_dict(res2.json())
except Exception as e:
raise LnurlResponseException(str(e))


def execute_withdraw(res: LnurlWithdrawResponse, pr: str) -> LnurlResponseModel:
async def execute_withdraw(res: LnurlWithdrawResponse, pr: str) -> LnurlResponseModel:
try:
invoice = bolt11_decode(pr)
except Bolt11Exception as exc:
Expand All @@ -117,14 +119,15 @@ def execute_withdraw(res: LnurlWithdrawResponse, pr: str) -> LnurlResponseModel:
if not res.min_withdrawable <= MilliSatoshi(amount) <= res.max_withdrawable:
raise LnurlResponseException(f"Amount {amount} not in range {res.min_withdrawable} - {res.max_withdrawable}")
try:
req = requests.get(
res.callback,
params={
"k1": res.k1,
"pr": pr,
},
)
req.raise_for_status()
return LnurlResponse.from_dict(req.json())
async with httpx.AsyncClient() as client:
res2 = await client.get(
url=res.callback,
params={
"k1": res.k1,
"pr": pr,
},
)
res2.raise_for_status()
return LnurlResponse.from_dict(res2.json())
except Exception as exc:
raise LnurlResponseException(str(exc))
Loading
Loading