From 979cc8b3a04fda94fc6ece64b7d011b7bb4d9ba7 Mon Sep 17 00:00:00 2001 From: Pol Alvarez Date: Mon, 10 Feb 2025 20:05:43 +0900 Subject: [PATCH] added get_payment_status & set_webhook --- README.md | 74 +++++++++- fewsats/_modidx.py | 3 + fewsats/core.py | 34 ++++- nbs/00_core.ipynb | 248 ++++++++++++++++++++++++++-------- nbs/01_sherlock-example.ipynb | 8 +- nbs/01_utils.ipynb | 24 +--- nbs/index.ipynb | 144 ++++++++++++++++++-- 7 files changed, 437 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index da8e935..28b7405 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ The library provides a class to handle payments. You can use handle them manually or use the `as_tools()` method to create tools for autonomous agents. +### Making Payments + +Obtain information about your account and perform payments: + ``` python from fewsats.core import * ``` @@ -49,13 +53,14 @@ fs.payment_methods().json(), fs.balance().json(), fs.me().json() 'exp_month': 12, 'exp_year': 2034, 'is_default': True}], - [{'id': 1, 'balance': 4468, 'currency': 'usd'}], + [{'id': 1, 'balance': 4464, 'currency': 'usd'}], {'name': 'Pol', 'last_name': 'Alvarez Vecino', 'email': 'pol@fewsats.com', 'billing_info': None, 'id': 1, - 'created_at': '2024-08-20T16:13:01.255Z'}) + 'created_at': '2024-08-20T16:13:01.255Z', + 'webhook_url': 'https://example.com/webhook'}) The `pay` method uses the information returned by a [L402 Protocol](https://github.com/l402-protocol/l402?tab=readme-ov-file#402-response-format) @@ -92,8 +97,8 @@ l402_offer = { fs.pay_offer(l402_offer).json() ``` - {'id': 118, - 'created_at': '2025-02-06T13:57:59.586Z', + {'id': 121, + 'created_at': '2025-02-10T11:04:48.083Z', 'status': 'success', 'payment_method': 'lightning'} @@ -109,6 +114,67 @@ requires you to specify the amount you are expecting to pay in cents. This is done for accounting purposes and convenience, but the amount paid will be the sats in the invoice. +### Getting Paid + +Fewsats also provides methods for receiving payments. You can create +offers for receiving payments as follows. + +``` python +# Create offers for receiving payments +offers_data = [{ + "offer_id": "offer_example", + "amount": 1, + "currency": "USD", + "description": "Receive payment for your service", + "title": "1 Credit Package", + "payment_methods": ["lightning", "stripe"] +}] +r = fs.create_offers(offers_data) +offers = r.json() +offers +``` + + {'offers': [{'offer_id': 'offer_example', + 'amount': 1, + 'currency': 'USD', + 'description': 'Receive payment for your service', + 'title': '1 Credit Package', + 'payment_methods': ['lightning', 'stripe'], + 'type': 'one-off'}], + 'payment_context_token': 'a175fd73-cb68-4a22-8685-b236eff2f1a0', + 'payment_request_url': 'http://localhost:8000/v0/l402/payment-request', + 'version': '0.2.2'} + +You can check if an offer has been paid using the payment context token +as follows. + +``` python +fs.get_payment_status(payment_context_token=offers["payment_context_token"]).json() +``` + + {'payment_context_token': 'a175fd73-cb68-4a22-8685-b236eff2f1a0', + 'status': 'pending', + 'offer_id': None, + 'paid_at': None, + 'amount': None, + 'currency': None} + +If you prefer to be notified whenever an offer is paid, you can set up a +webhook as follows, and we will call it whenever a payment is made. + +``` python +r = fs.set_webhook(webhook_url="https://example.com/webhook") +r.json() +``` + + {'name': 'Pol', + 'last_name': 'Alvarez Vecino', + 'email': 'pol@fewsats.com', + 'billing_info': None, + 'id': 1, + 'created_at': '2024-08-20T16:13:01.255Z', + 'webhook_url': 'https://example.com/webhook'} + ### AI Agent Integration We will show how to enable your AI assistant to handle payments using diff --git a/fewsats/_modidx.py b/fewsats/_modidx.py index 8c047e6..105007f 100644 --- a/fewsats/_modidx.py +++ b/fewsats/_modidx.py @@ -12,11 +12,14 @@ 'fewsats.core.Fewsats.as_tools': ('core.html#fewsats.as_tools', 'fewsats/core.py'), 'fewsats.core.Fewsats.balance': ('core.html#fewsats.balance', 'fewsats/core.py'), 'fewsats.core.Fewsats.create_offers': ('core.html#fewsats.create_offers', 'fewsats/core.py'), + 'fewsats.core.Fewsats.get_payment_details': ('core.html#fewsats.get_payment_details', 'fewsats/core.py'), + 'fewsats.core.Fewsats.get_payment_status': ('core.html#fewsats.get_payment_status', 'fewsats/core.py'), 'fewsats.core.Fewsats.me': ('core.html#fewsats.me', 'fewsats/core.py'), 'fewsats.core.Fewsats.pay_lightning': ('core.html#fewsats.pay_lightning', 'fewsats/core.py'), 'fewsats.core.Fewsats.pay_offer': ('core.html#fewsats.pay_offer', 'fewsats/core.py'), 'fewsats.core.Fewsats.payment_info': ('core.html#fewsats.payment_info', 'fewsats/core.py'), 'fewsats.core.Fewsats.payment_methods': ('core.html#fewsats.payment_methods', 'fewsats/core.py'), + 'fewsats.core.Fewsats.set_webhook': ('core.html#fewsats.set_webhook', 'fewsats/core.py'), 'fewsats.core.Fewsats.wait_for_settlement': ('core.html#fewsats.wait_for_settlement', 'fewsats/core.py')}, 'fewsats.l402': { 'fewsats.l402.L.starstarmap': ('claudette.html#l.starstarmap', 'fewsats/l402.py'), 'fewsats.l402.Offer': ('claudette.html#offer', 'fewsats/l402.py'), diff --git a/fewsats/core.py b/fewsats/core.py index a6fe039..02c64d2 100644 --- a/fewsats/core.py +++ b/fewsats/core.py @@ -78,6 +78,32 @@ def create_offers(self:Fewsats, # %% ../nbs/00_core.ipynb 30 @patch +def get_payment_details(self:Fewsats, + payment_request_url:str, + offer_id:str, + payment_method:str, + payment_context_token:str, + ) -> dict: + data = {"offer_id": offer_id, "payment_method": payment_method, "payment_context_token": payment_context_token} + return httpx.post(payment_request_url, json=data) + + +# %% ../nbs/00_core.ipynb 33 +@patch +def get_payment_status(self:Fewsats, + payment_context_token:str, + ) -> dict: + return self._request("GET", f"v0/l402/payment-status?payment_context_token={payment_context_token}") + +# %% ../nbs/00_core.ipynb 35 +@patch +def set_webhook(self:Fewsats, + webhook_url:str, + ) -> dict: + return self._request("POST", f"v0/users/webhook/set", json={"webhook_url": webhook_url}) + +# %% ../nbs/00_core.ipynb 38 +@patch def pay_offer(self:Fewsats, l402_offer: Dict, # a dictionary containing the response of an L402 endpoint payment_method:str = '', # preferred payment method (optional) @@ -93,7 +119,7 @@ def pay_offer(self:Fewsats, data = {"payment_method": payment_method, **l402_offer} if payment_method else l402_offer return self._request("POST", "v0/l402/purchases/from-offer", json=data) -# %% ../nbs/00_core.ipynb 32 +# %% ../nbs/00_core.ipynb 40 @patch def pay_lightning(self: Fewsats, invoice: str, # lightning invoice @@ -109,14 +135,14 @@ def pay_lightning(self: Fewsats, } return self._request("POST", "v0/l402/purchases/lightning", json=data) -# %% ../nbs/00_core.ipynb 37 +# %% ../nbs/00_core.ipynb 45 @patch def payment_info(self:Fewsats, pid:str): # purchase id "Retrieve the details of a payment." return self._request("GET", f"v0/l402/purchases/{pid}") -# %% ../nbs/00_core.ipynb 40 +# %% ../nbs/00_core.ipynb 48 @patch def wait_for_settlement(self:Fewsats, pid:str, # purchase id @@ -134,7 +160,7 @@ def wait_for_settlement(self:Fewsats, wait *= 2 raise TimeoutError(f"Payment {pid} did not settle within {max_wait} seconds. Final status: {status}") -# %% ../nbs/00_core.ipynb 43 +# %% ../nbs/00_core.ipynb 51 @patch def as_tools(self:Fewsats): "Return list of available tools for AI agents" diff --git a/nbs/00_core.ipynb b/nbs/00_core.ipynb index 097f91f..7d2023f 100644 --- a/nbs/00_core.ipynb +++ b/nbs/00_core.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -47,7 +47,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -56,7 +56,7 @@ "True" ] }, - "execution_count": 4, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -75,7 +75,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -95,12 +95,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "k = os.getenv(\"FEWSATS_API_KEY\")\n", - "fs = Fewsats(api_key=k)\n", + "# k = os.getenv(\"FEWSATS_API_KEY\")\n", + "# fs = Fewsats(api_key=k)\n", + "k = os.getenv(\"FEWSATS_LOCAL_API_KEY\")\n", + "fs = Fewsats(api_key=k, base_url=\"http://localhost:8000\")\n", "\n", "test_eq(fs.api_key, k)\n", "test_eq(fs._httpx_client.headers[\"Authorization\"], f\"Token {k}\")" @@ -115,7 +117,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -132,7 +134,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -149,7 +151,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -179,7 +181,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -193,7 +195,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -205,10 +207,11 @@ " 'email': 'pol@fewsats.com',\n", " 'billing_info': None,\n", " 'id': 1,\n", - " 'created_at': '2024-08-20T16:13:01.255Z'})" + " 'created_at': '2024-08-20T16:13:01.255Z',\n", + " 'webhook_url': 'https://webhooks.fewsats.com/'})" ] }, - "execution_count": 11, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -227,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -241,16 +244,16 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(200, [{'id': 1, 'balance': 4468, 'currency': 'usd'}])" + "(200, [{'id': 1, 'balance': 4465, 'currency': 'usd'}])" ] }, - "execution_count": 13, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -271,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -284,7 +287,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -305,7 +308,7 @@ " 'is_default': True}])" ] }, - "execution_count": 15, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -318,7 +321,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -336,7 +339,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -352,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -372,7 +375,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -387,7 +390,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -401,12 +404,12 @@ " 'title': 'Test Package',\n", " 'payment_methods': ['lightning', 'credit_card'],\n", " 'type': 'one-off'}],\n", - " 'payment_context_token': 'd28a0611-e28a-44a7-bbd8-ea7dfe6fe8a2',\n", + " 'payment_context_token': '8eb6c2e1-eacf-4455-b686-06f3a4751304',\n", " 'payment_request_url': 'http://localhost:8000/v0/l402/payment-request',\n", " 'version': '0.2.2'})" ] }, - "execution_count": 20, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -427,6 +430,149 @@ "r.status_code, l402_offers" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get Payment Details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def get_payment_details(self:Fewsats,\n", + " payment_request_url:str,\n", + " offer_id:str,\n", + " payment_method:str,\n", + " payment_context_token:str,\n", + " ) -> dict:\n", + " data = {\"offer_id\": offer_id, \"payment_method\": payment_method, \"payment_context_token\": payment_context_token}\n", + " return httpx.post(payment_request_url, json=data)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(200,\n", + " {'expires_at': '2025-02-10T11:17:44.451276+00:00',\n", + " 'offer_id': 'test_offer_2',\n", + " 'payment_request': {'lightning_invoice': 'lnbc100n1pn6nkp9pp58ad4sxjxfryq2saercf0p6clcmwjpvtt7ywkt7aqljvx4uqxwfxsdq523jhxapq2pskx6mpvajscqzpgxqrzpjrzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqz99gpz55yqqqqqqqqqqqqqq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqz99gpz55yqqqqqqqqqqqqqq9qsp5w088nn2uq36ndw4m5c74eqwn9suaf3rxpa2aklrrmh3mvdhcpzms9qxpqysgqw6u0h50ak8dqy3zacq9mrxnaz22k033he7d0ex8zfkvg0vzydt7q9pe5mhk56x9lcd8ednykfsmt57rt6x9g3n6ej3g59z40j4t9gggquvn50a'},\n", + " 'version': '0.2.2'})" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "r = fs.get_payment_details(l402_offers[\"payment_request_url\"], l402_offers[\"offers\"][0][\"offer_id\"], \"lightning\", l402_offers[\"payment_context_token\"])\n", + "payment_details = r.json()\n", + "r.status_code, payment_details" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get Payment Status\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def get_payment_status(self:Fewsats,\n", + " payment_context_token:str,\n", + " ) -> dict:\n", + " return self._request(\"GET\", f\"v0/l402/payment-status?payment_context_token={payment_context_token}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(200,\n", + " {'payment_context_token': '8eb6c2e1-eacf-4455-b686-06f3a4751304',\n", + " 'status': 'pending',\n", + " 'offer_id': None,\n", + " 'paid_at': None,\n", + " 'amount': None,\n", + " 'currency': None})" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "r = fs.get_payment_status(l402_offers[\"payment_context_token\"])\n", + "r.status_code, r.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def set_webhook(self:Fewsats,\n", + " webhook_url:str,\n", + " ) -> dict:\n", + " return self._request(\"POST\", f\"v0/users/webhook/set\", json={\"webhook_url\": webhook_url})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(,\n", + " {'name': 'Pol',\n", + " 'last_name': 'Alvarez Vecino',\n", + " 'email': 'pol@fewsats.com',\n", + " 'billing_info': None,\n", + " 'id': 1,\n", + " 'created_at': '2024-08-20T16:13:01.255Z',\n", + " 'webhook_url': 'https://example.com/webhook'})" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "r = fs.set_webhook(\"https://example.com/webhook\")\n", + "r, r.json()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -438,7 +584,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -464,17 +610,17 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(400,\n", - " '{\"detail\": \"Invalid payment request received. Could not get payment details. Traceback (most recent call last):\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connection.py\\\\\", line 198, in _new_conn\\\\n sock = connection.create_connection(\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/util/connection.py\\\\\", line 60, in create_connection\\\\n for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/socket.py\\\\\", line 976, in getaddrinfo\\\\n for res in _socket.getaddrinfo(host, port, family, type, proto, flags):\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\nsocket.gaierror: [Errno -2] Name or service not known\\\\n\\\\nThe above exception was the direct cause of the following exception:\\\\n\\\\nTraceback (most recent call last):\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 787, in urlopen\\\\n response = self._make_request(\\\\n ^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 488, in _make_request\\\\n raise new_e\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 464, in _make_request\\\\n self._validate_conn(conn)\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 1093, in _validate_conn\\\\n conn.connect()\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connection.py\\\\\", line 704, in connect\\\\n self.sock = sock = self._new_conn()\\\\n ^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connection.py\\\\\", line 205, in _new_conn\\\\n raise NameResolutionError(self.host, self, e) from e\\\\nurllib3.exceptions.NameResolutionError: : Failed to resolve \\'api.lightspark.com\\' ([Errno -2] Name or service not known)\\\\n\\\\nThe above exception was the direct cause of the following exception:\\\\n\\\\nTraceback (most recent call last):\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/adapters.py\\\\\", line 667, in send\\\\n resp = conn.urlopen(\\\\n ^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 841, in urlopen\\\\n retries = retries.increment(\\\\n ^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/util/retry.py\\\\\", line 519, in increment\\\\n raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type]\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\nurllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host=\\'api.lightspark.com\\', port=443): Max retries exceeded with url: /graphql/server/2023-09-13 (Caused by NameResolutionError(\\\\\": Failed to resolve \\'api.lightspark.com\\' ([Errno -2] Name or service not known)\\\\\"))\\\\n\\\\nDuring handling of the above exception, another exception occurred:\\\\n\\\\nTraceback (most recent call last):\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/ninja/operation.py\\\\\", line 341, in run\\\\n result = await self.view_func(request, **values)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/api/common/decorators.py\\\\\", line 17, in wrapper\\\\n return await func(request, *args, **kwargs)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/api/v0/views.py\\\\\", line 127, in create_payment_request_v0\\\\n return await create_payment_request(\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/server.py\\\\\", line 68, in create_payment_request\\\\n provider = LightningProvider()\\\\n ^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/providers/lightning.py\\\\\", line 22, in __init__\\\\n self.client = LightsparkAPI()\\\\n ^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/utils/lightspark_api.py\\\\\", line 38, in __init__\\\\n signing_key = self.client.recover_node_signing_key(self.node_id, self.node_password)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/lightspark/lightspark_client.py\\\\\", line 502, in recover_node_signing_key\\\\n json = self._requester.execute_graphql(\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/lightspark/requests/requester.py\\\\\", line 96, in execute_graphql\\\\n r = self.graphql_session.post(\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/sessions.py\\\\\", line 637, in post\\\\n return self.request(\\\\\"POST\\\\\", url, data=data, json=json, **kwargs)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/sessions.py\\\\\", line 589, in request\\\\n resp = self.send(prep, **send_kwargs)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/sessions.py\\\\\", line 703, in send\\\\n r = adapter.send(request, **kwargs)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/adapters.py\\\\\", line 700, in send\\\\n raise ConnectionError(e, request=request)\\\\nrequests.exceptions.ConnectionError: HTTPSConnectionPool(host=\\'api.lightspark.com\\', port=443): Max retries exceeded with url: /graphql/server/2023-09-13 (Caused by NameResolutionError(\\\\\": Failed to resolve \\'api.lightspark.com\\' ([Errno -2] Name or service not known)\\\\\"))\\\\n\"}')" + " '{\"detail\": \"Invalid payment request received. Could not get payment details. Traceback (most recent call last):\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connection.py\\\\\", line 198, in _new_conn\\\\n sock = connection.create_connection(\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/util/connection.py\\\\\", line 60, in create_connection\\\\n for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/socket.py\\\\\", line 976, in getaddrinfo\\\\n for res in _socket.getaddrinfo(host, port, family, type, proto, flags):\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\nsocket.gaierror: [Errno -2] Name or service not known\\\\n\\\\nThe above exception was the direct cause of the following exception:\\\\n\\\\nTraceback (most recent call last):\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 787, in urlopen\\\\n response = self._make_request(\\\\n ^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 488, in _make_request\\\\n raise new_e\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 464, in _make_request\\\\n self._validate_conn(conn)\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 1093, in _validate_conn\\\\n conn.connect()\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connection.py\\\\\", line 704, in connect\\\\n self.sock = sock = self._new_conn()\\\\n ^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connection.py\\\\\", line 205, in _new_conn\\\\n raise NameResolutionError(self.host, self, e) from e\\\\nurllib3.exceptions.NameResolutionError: : Failed to resolve \\'api.lightspark.com\\' ([Errno -2] Name or service not known)\\\\n\\\\nThe above exception was the direct cause of the following exception:\\\\n\\\\nTraceback (most recent call last):\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/adapters.py\\\\\", line 667, in send\\\\n resp = conn.urlopen(\\\\n ^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/connectionpool.py\\\\\", line 841, in urlopen\\\\n retries = retries.increment(\\\\n ^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/urllib3/util/retry.py\\\\\", line 519, in increment\\\\n raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type]\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\nurllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host=\\'api.lightspark.com\\', port=443): Max retries exceeded with url: /graphql/server/2023-09-13 (Caused by NameResolutionError(\\\\\": Failed to resolve \\'api.lightspark.com\\' ([Errno -2] Name or service not known)\\\\\"))\\\\n\\\\nDuring handling of the above exception, another exception occurred:\\\\n\\\\nTraceback (most recent call last):\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/ninja/operation.py\\\\\", line 341, in run\\\\n result = await self.view_func(request, **values)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/api/common/decorators.py\\\\\", line 17, in wrapper\\\\n return await func(request, *args, **kwargs)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/api/v0/views.py\\\\\", line 127, in create_payment_request_v0\\\\n return await create_payment_request(\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/server.py\\\\\", line 68, in create_payment_request\\\\n provider = LightningProvider()\\\\n ^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/providers/lightning.py\\\\\", line 22, in __init__\\\\n self.client = LightsparkAPI()\\\\n ^^^^^^^^^^^^^^^\\\\n File \\\\\"/opt/hub_api/l402/utils/lightspark_api.py\\\\\", line 38, in __init__\\\\n signing_key = self.client.recover_node_signing_key(self.node_id, self.node_password)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/lightspark/lightspark_client.py\\\\\", line 502, in recover_node_signing_key\\\\n json = self._requester.execute_graphql(\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/lightspark/requests/requester.py\\\\\", line 96, in execute_graphql\\\\n r = self.graphql_session.post(\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/sessions.py\\\\\", line 637, in post\\\\n return self.request(\\\\\"POST\\\\\", url, data=data, json=json, **kwargs)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/sessions.py\\\\\", line 589, in request\\\\n resp = self.send(prep, **send_kwargs)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/sessions.py\\\\\", line 703, in send\\\\n r = adapter.send(request, **kwargs)\\\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\\\n File \\\\\"/usr/local/lib/python3.12/site-packages/requests/adapters.py\\\\\", line 700, in send\\\\n raise ConnectionError(e, request=request)\\\\nrequests.exceptions.ConnectionError: HTTPSConnectionPool(host=\\'api.lightspark.com\\', port=443): Max retries exceeded with url: /graphql/server/2023-09-13 (Caused by NameResolutionError(\\\\\": Failed to resolve \\'api.lightspark.com\\' ([Errno -2] Name or service not known)\\\\\"))\\\\n\"}')" ] }, - "execution_count": 22, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -487,7 +633,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -511,7 +657,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -520,7 +666,7 @@ "(500, {'detail': 'An error occurred while paying the L402 invoice.'})" ] }, - "execution_count": 24, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -534,7 +680,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -543,7 +689,7 @@ "'no id provided'" ] }, - "execution_count": 25, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -571,7 +717,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -585,7 +731,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -594,7 +740,7 @@ "" ] }, - "execution_count": 27, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -614,7 +760,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -639,7 +785,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -889,21 +1035,9 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": "python3", "language": "python", "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" } }, "nbformat": 4, diff --git a/nbs/01_sherlock-example.ipynb b/nbs/01_sherlock-example.ipynb index ea3b749..70e4e99 100644 --- a/nbs/01_sherlock-example.ipynb +++ b/nbs/01_sherlock-example.ipynb @@ -427,7 +427,7 @@ "" ] }, - "execution_count": 17, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -591,13 +591,9 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": "python3", "language": "python", "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.12.3" } }, "nbformat": 4, diff --git a/nbs/01_utils.ipynb b/nbs/01_utils.ipynb index c5c8c29..103c43c 100644 --- a/nbs/01_utils.ipynb +++ b/nbs/01_utils.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -49,7 +49,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -468,7 +468,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -479,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -575,21 +575,9 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": "python3", "language": "python", "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.3" } }, "nbformat": 4, diff --git a/nbs/index.ipynb b/nbs/index.ipynb index a59f18a..46b04c0 100644 --- a/nbs/index.ipynb +++ b/nbs/index.ipynb @@ -65,6 +65,15 @@ "The library provides a `Fewsats` class to handle payments. You can use handle them manually or use the `as_tools()` method to create tools for autonomous agents." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Making Payments\n", + "\n", + "Obtain information about your account and perform payments:\n" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -76,7 +85,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -94,16 +103,17 @@ " 'exp_month': 12,\n", " 'exp_year': 2034,\n", " 'is_default': True}],\n", - " [{'id': 1, 'balance': 4468, 'currency': 'usd'}],\n", + " [{'id': 1, 'balance': 4464, 'currency': 'usd'}],\n", " {'name': 'Pol',\n", " 'last_name': 'Alvarez Vecino',\n", " 'email': 'pol@fewsats.com',\n", " 'billing_info': None,\n", " 'id': 1,\n", - " 'created_at': '2024-08-20T16:13:01.255Z'})" + " 'created_at': '2024-08-20T16:13:01.255Z',\n", + " 'webhook_url': 'https://example.com/webhook'})" ] }, - "execution_count": 19, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -124,7 +134,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -153,19 +163,19 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'id': 118,\n", - " 'created_at': '2025-02-06T13:57:59.586Z',\n", + "{'id': 121,\n", + " 'created_at': '2025-02-10T11:04:48.083Z',\n", " 'status': 'success',\n", " 'payment_method': 'lightning'}" ] }, - "execution_count": 21, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -199,6 +209,122 @@ "The lightning invoice already contains a payment amount, but the method requires you to specify the amount you are expecting to pay in cents. This is done for accounting purposes and convenience, but the amount paid will be the sats in the invoice." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "### Getting Paid\n", + "\n", + "Fewsats also provides methods for receiving payments. You can create offers for receiving payments as follows." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'offers': [{'offer_id': 'offer_example',\n", + " 'amount': 1,\n", + " 'currency': 'USD',\n", + " 'description': 'Receive payment for your service',\n", + " 'title': '1 Credit Package',\n", + " 'payment_methods': ['lightning', 'stripe'],\n", + " 'type': 'one-off'}],\n", + " 'payment_context_token': 'a175fd73-cb68-4a22-8685-b236eff2f1a0',\n", + " 'payment_request_url': 'http://localhost:8000/v0/l402/payment-request',\n", + " 'version': '0.2.2'}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create offers for receiving payments\n", + "offers_data = [{\n", + " \"offer_id\": \"offer_example\",\n", + " \"amount\": 1,\n", + " \"currency\": \"USD\",\n", + " \"description\": \"Receive payment for your service\",\n", + " \"title\": \"1 Credit Package\",\n", + " \"payment_methods\": [\"lightning\", \"stripe\"]\n", + "}]\n", + "r = fs.create_offers(offers_data)\n", + "offers = r.json()\n", + "offers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can check if an offer has been paid using the payment context token as follows." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'payment_context_token': 'a175fd73-cb68-4a22-8685-b236eff2f1a0',\n", + " 'status': 'pending',\n", + " 'offer_id': None,\n", + " 'paid_at': None,\n", + " 'amount': None,\n", + " 'currency': None}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fs.get_payment_status(payment_context_token=offers[\"payment_context_token\"]).json()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you prefer to be notified whenever an offer is paid, you can set up a webhook as follows, and we will call it whenever a payment is made." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'Pol',\n", + " 'last_name': 'Alvarez Vecino',\n", + " 'email': 'pol@fewsats.com',\n", + " 'billing_info': None,\n", + " 'id': 1,\n", + " 'created_at': '2024-08-20T16:13:01.255Z',\n", + " 'webhook_url': 'https://example.com/webhook'}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "r = fs.set_webhook(webhook_url=\"https://example.com/webhook\")\n", + "r.json()" + ] + }, { "cell_type": "markdown", "metadata": {},