33# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.
44
55# %% auto 0
6- __all__ = ['Fewsats' , 'Offer' , 'L402Offers' ]
6+ __all__ = ['Fewsats' , 'FewsatsWebhookEvent' , ' Offer' , 'L402Offers' ]
77
88# %% ../nbs/00_core.ipynb 3
99from fastcore .utils import *
1010import os
11+ import hashlib
12+ import hmac
1113import httpx
12- from typing import Dict , Any , List
14+ import time
1315import json
16+ from dataclasses import dataclass
1417from fastcore .basics import BasicRepr
1518from fastcore .utils import store_attr
1619from typing import List , Dict , Any
1720
1821# %% ../nbs/00_core.ipynb 8
1922class Fewsats :
23+
24+ WEBHOOK_VERSION = "v1"
25+ WEBHOOK_SIGNATURE_HEADER = "Fewsats-Signature"
26+
2027 "Client for interacting with the Fewsats API"
2128 def __init__ (self ,
2229 api_key : str = None , # The API key for the Fewsats account
@@ -114,6 +121,63 @@ def add_webhook(self:Fewsats,
114121 return self ._request ("POST" , f"v0/users/webhook/add" , json = {"webhook_url" : webhook_url })
115122
116123# %% ../nbs/00_core.ipynb 39
124+ @dataclass
125+ class FewsatsWebhookEvent :
126+ offer_id : str
127+ payment_context_token : str
128+ amount : int
129+ currency : str
130+ status : str
131+ timestamp : str
132+
133+ @patch (cls_method = True )
134+ def verify_webhook (cls :Fewsats ,
135+ data : bytes ,
136+ signature : str ,
137+ webhook_secret : str ,
138+ ) -> dict :
139+ """
140+ Verify and parse a webhook that comes from Fewsats
141+ Args:
142+ data: bytes
143+ signature: str
144+ webhook_secret: str
145+ Returns:
146+ FewsatsWebhookEvent
147+ """
148+ if not isinstance (data , bytes ):
149+ raise TypeError (f"'data' should be bytes, got { type (data )} " )
150+
151+ timestamp_str , signature_str = signature .split ("," )
152+ timestamp = timestamp_str .split ("=" )[1 ]
153+ signature_version , signature = signature_str .split ("=" )
154+
155+ payload_str = data .decode ("utf-8" )
156+
157+ if signature_version != cls .WEBHOOK_VERSION :
158+ raise ValueError ("Unsupported signature version" )
159+
160+ if timestamp .isdigit ():
161+ timestamp = int (timestamp )
162+ else :
163+ raise ValueError ("Invalid timestamp" )
164+
165+ if timestamp - time .time () > 300 :
166+ raise ValueError ("Timestamp is older than 5 minutes" )
167+
168+ signed_payload = f"{ timestamp } .{ payload_str } "
169+
170+ # Generate the signature
171+ expected_signature = hmac .new (webhook_secret .encode (), signed_payload .encode (), hashlib .sha256 ).hexdigest ()
172+
173+ if signature .lower () != expected_signature .lower ():
174+ raise ValueError ("Webhook message hash does not match signature" )
175+
176+ event = json .loads (payload_str )
177+ return FewsatsWebhookEvent (** event )
178+
179+
180+ # %% ../nbs/00_core.ipynb 42
117181@patch
118182def pay_lightning (self : Fewsats ,
119183 invoice : str , # lightning invoice
@@ -129,7 +193,7 @@ def pay_lightning(self: Fewsats,
129193 }
130194 return self ._request ("POST" , "v0/l402/purchases/lightning" , json = data )
131195
132- # %% ../nbs/00_core.ipynb 42
196+ # %% ../nbs/00_core.ipynb 45
133197class Offer (BasicRepr ):
134198 "Represents a single L402 offer"
135199 def __init__ (self ,
@@ -184,7 +248,7 @@ def from_dict(cls, d: Dict[str, Any]) -> 'L402Offers':
184248 )
185249
186250
187- # %% ../nbs/00_core.ipynb 46
251+ # %% ../nbs/00_core.ipynb 49
188252@patch
189253def pay_offer (self :Fewsats ,
190254 offer_id : str , # the offer id to pay for
@@ -213,7 +277,7 @@ def pay_offer(self:Fewsats,
213277 return self ._request ("POST" , "v0/l402/purchases/from-offer" , json = data )
214278
215279
216- # %% ../nbs/00_core.ipynb 49
280+ # %% ../nbs/00_core.ipynb 52
217281@patch
218282def pay_offer_str (self :Fewsats ,
219283 offer_id : str , # the offer id to pay for
@@ -250,7 +314,7 @@ def pay_offer_str(self:Fewsats,
250314
251315 return self ._request ("POST" , "v0/l402/purchases/from-offer" , timeout = 20 , json = data )
252316
253- # %% ../nbs/00_core.ipynb 52
317+ # %% ../nbs/00_core.ipynb 55
254318@patch
255319def pay_link (self :Fewsats ,
256320 url : str , # URL to purchase from
@@ -280,14 +344,14 @@ def pay_link(self:Fewsats,
280344 }
281345 return self ._request ("POST" , "v0/l402/purchases/from-link" , json = data )
282346
283- # %% ../nbs/00_core.ipynb 55
347+ # %% ../nbs/00_core.ipynb 58
284348@patch
285349def payment_info (self :Fewsats ,
286350 pid :str ): # purchase id
287351 "Retrieve the details of a payment."
288352 return self ._request ("GET" , f"v0/l402/outgoing-payments/{ pid } " )
289353
290- # %% ../nbs/00_core.ipynb 58
354+ # %% ../nbs/00_core.ipynb 61
291355@patch
292356def as_tools (self :Fewsats ):
293357 "Return list of available tools for AI agents"
0 commit comments