= 'base-mainnet' chain
payment clients
Payment clients are the interfaces and implementations that allow users to pay for L402 offers.
PaymentRequest
PaymentRequest (offer_id:str, payment_context_token:str, payment_method:str, chain:str='', asset:str='')
Represents a payment request to get payment details from a payment provider
PaymentProvider
PaymentProvider ()
Base class for all payment providers
# from l402.utils import *
# run_l402_server(PaymentRequest, port=9000)
Coinbase Provider
CoinbaseProvider
CoinbaseProvider (wallet:cdp.wallet.Wallet, asset:str='usdc')
Base class for objects needing a basic __repr__
= CoinbaseProvider(wallet=create_test_wallet(fund=False, chain=chain))
c c
CoinbaseProvider(wallet=Wallet: (id: ef60974b-2571-44ca-ac25-ff99d2e3c88f, network_id: base-mainnet, server_signer_status: None), asset='usdc', supported_methods=['onchain'], chain='base-mainnet')
= httpx.get('https://l402-offers.replit.app')
r # r = httpx.get('http://localhost:9000/offers')
r.status_code, r.text
(402,
'{"offers":[{"offer_id":"ce23eefd-1156-4aa4-85c1-361918a24485","amount":1,"currency":"USD","description":"Purchase 1 credit for API access","title":"1 Credit Package","type":"one-time","payment_methods":["onchain","lightning"]}],"payment_context_token":"8ee0082b-97c8-4ac6-baa4-f9f544dc977c","payment_request_url":"https://hub-5n97k.ondigitalocean.app/v0/l402/payment-request","version":"0.2.2"}')
= r.json()
o o
{'offers': [{'offer_id': 'ce23eefd-1156-4aa4-85c1-361918a24485',
'amount': 1,
'currency': 'USD',
'description': 'Purchase 1 credit for API access',
'title': '1 Credit Package',
'type': 'one-time',
'payment_methods': ['onchain', 'lightning']}],
'payment_context_token': '8ee0082b-97c8-4ac6-baa4-f9f544dc977c',
'payment_request_url': 'https://hub-5n97k.ondigitalocean.app/v0/l402/payment-request',
'version': '0.2.2'}
= {
data "offer_id": first(o['offers'])['offer_id'],
"payment_method": 'onchain',
"chain": chain,
"asset": 'usdc',
"payment_context_token": o['payment_context_token']
}= httpx.post(o['payment_request_url'], json=data)
r r.status_code, r.json()
(200,
{'expires_at': '2025-01-28T02:06:17.637705+00:00',
'offer_id': 'ce23eefd-1156-4aa4-85c1-361918a24485',
'payment_request': {'checkout_url': 'https://commerce.coinbase.com/pay/be427ef1-153a-495e-93eb-1b10550731d0',
'address': '0x03059433BCdB6144624cC2443159D9445C32b7a8',
'chain': 'base-mainnet',
'asset': 'usdc'},
'version': '0.2.2'})
= {
data "offer_id": first(o['offers'])['offer_id'],
"payment_method": 'lightning',
"payment_context_token": o['payment_context_token']
}= httpx.post(o['payment_request_url'], json=data, timeout=15)
r r.status_code, r.text
(200,
'{"expires_at": "2025-01-28T04:14:16.328280+00:00", "offer_id": "ce23eefd-1156-4aa4-85c1-361918a24485", "payment_request": {"lightning_invoice": "lnbc90n1pnes5txpp5zxhg46zr2lycmqg9km930h9mmefeyanw3jm8c2cxpqxlhlvevkgsdq6xysyxun9v35hggzsv93kkct8v5cqzpgxqrzpjrzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqz99gpz55yqqqqqqqqqqqqqq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqz99gpz55yqqqqqqqqqqqqqq9qsp5y9unj8j6czfjy274dj7war7k7xey8jvgegsxcjenct8ntah4eyvs9qxpqysgqq60qg26s74k4jsetcz954dlpghg63a8qrprqytr0ml0f2hxtfm0zujk4t54x2n4hxaxaz6d75tm7xjalv287mn559uq8tnmyfs92gdqpdutj9l"}, "version": "0.2.2"}')
= {
data "offer_id": 'test-lightning-1',
"payment_method": 'lightning',
"payment_context_token": '550cdc77-bdae-410b-bd7a-091b14be72bb'
}= httpx.post('http://localhost:8000/v0/l402/payment-request', json=data, timeout=15)
r r.status_code, r.text
(200,
'{"expires_at": "2025-01-28T02:46:35.422815+00:00", "offer_id": "test-lightning-1", "payment_request": {"lightning_invoice": "lnbc90n1pnes0xhpp5lsp46vm2j47f8z3rm3lmj2lh54gvc4jn3hql0qhp9ryxef9fz0lsdpy23jhxapqf35kw6r5de5kueeq2pshjmt9de6qcqzpgxqrzpnrzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqz99gpz55yqqqqqqqqqqqqqq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqz99gpz55yqqqqqqqqqqqqqq9qsp5ykrsqs0x9mjt22937a0hn5wz0z0fxd9yxv97sud2kkrswuakh2js9qxpqysgqeq8g08w8n3t0wlhj3h3zkjsnpa8gznzncf98nqk92lyd96a30gsy8fxzk5q9z47fsy59ljn548xlck84qeta88wygppq3zed8qsz9dgqxn7rtq"}, "version": "0.2.2"}')
get_payment_request
get_payment_request (payment_request_url:str, payment_context_token:str, offer_id:str, payment_method:str, chain:str='', asset:str='')
= get_payment_request(o['payment_request_url'], o['payment_context_token'], o['offers'][0]['offer_id'], 'onchain', chain, 'usdc')
r r
--------------------------------------------------------------------------- ReadTimeout Traceback (most recent call last) File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_transports/default.py:101, in map_httpcore_exceptions() 100 try: --> 101 yield 102 except Exception as exc: File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_transports/default.py:250, in HTTPTransport.handle_request(self, request) 249 with map_httpcore_exceptions(): --> 250 resp = self._pool.handle_request(req) 252 assert isinstance(resp.stream, typing.Iterable) File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py:256, in ConnectionPool.handle_request(self, request) 255 self._close_connections(closing) --> 256 raise exc from None 258 # Return the response. Note that in this case we still have to manage 259 # the point at which the response is closed. File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py:236, in ConnectionPool.handle_request(self, request) 234 try: 235 # Send the request on the assigned connection. --> 236 response = connection.handle_request( 237 pool_request.request 238 ) 239 except ConnectionNotAvailable: 240 # In some cases a connection may initially be available to 241 # handle a request, but then become unavailable. 242 # 243 # In this case we clear the connection and try again. File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/connection.py:103, in HTTPConnection.handle_request(self, request) 101 raise exc --> 103 return self._connection.handle_request(request) File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:136, in HTTP11Connection.handle_request(self, request) 135 self._response_closed() --> 136 raise exc File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:106, in HTTP11Connection.handle_request(self, request) 97 with Trace( 98 "receive_response_headers", logger, request, kwargs 99 ) as trace: 100 ( 101 http_version, 102 status, 103 reason_phrase, 104 headers, 105 trailing_data, --> 106 ) = self._receive_response_headers(**kwargs) 107 trace.return_value = ( 108 http_version, 109 status, 110 reason_phrase, 111 headers, 112 ) File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:177, in HTTP11Connection._receive_response_headers(self, request) 176 while True: --> 177 event = self._receive_event(timeout=timeout) 178 if isinstance(event, h11.Response): File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:217, in HTTP11Connection._receive_event(self, timeout) 216 if event is h11.NEED_DATA: --> 217 data = self._network_stream.read( 218 self.READ_NUM_BYTES, timeout=timeout 219 ) 221 # If we feed this case through h11 we'll raise an exception like: 222 # 223 # httpcore.RemoteProtocolError: can't handle event type (...) 227 # perspective. Instead we handle this case distinctly and treat 228 # it as a ConnectError. File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_backends/sync.py:126, in SyncStream.read(self, max_bytes, timeout) 125 exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError} --> 126 with map_exceptions(exc_map): 127 self._sock.settimeout(timeout) File /Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py:158, in _GeneratorContextManager.__exit__(self, typ, value, traceback) 157 try: --> 158 self.gen.throw(value) 159 except StopIteration as exc: 160 # Suppress StopIteration *unless* it's the same exception that 161 # was passed to throw(). This prevents a StopIteration 162 # raised inside the "with" statement from being suppressed. File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_exceptions.py:14, in map_exceptions(map) 13 if isinstance(exc, from_exc): ---> 14 raise to_exc(exc) from exc 15 raise ReadTimeout: The read operation timed out The above exception was the direct cause of the following exception: ReadTimeout Traceback (most recent call last) Cell In[13], line 1 ----> 1 r = get_payment_request(o['payment_request_url'], o['payment_context_token'], o['offers'][0]['offer_id'], 'onchain', chain, 'usdc') 2 r Cell In[12], line 16, in get_payment_request(payment_request_url, payment_context_token, offer_id, payment_method, chain, asset) 3 def get_payment_request(payment_request_url: str, 4 payment_context_token: str, 5 offer_id: str, 6 payment_method: str, 7 chain: str = "", 8 asset: str = ""): 9 data = { 10 "offer_id": offer_id, 11 "payment_method": payment_method, (...) 14 "payment_context_token": payment_context_token 15 } ---> 16 r = httpx.post(payment_request_url, json=data) 17 r.raise_for_status() 18 return r.json() File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_api.py:304, in post(url, content, data, files, json, params, headers, cookies, auth, proxy, follow_redirects, verify, timeout, trust_env) 282 def post( 283 url: URL | str, 284 *, (...) 297 trust_env: bool = True, 298 ) -> Response: 299 """ 300 Sends a `POST` request. 301 302 **Parameters**: See `httpx.request`. 303 """ --> 304 return request( 305 "POST", 306 url, 307 content=content, 308 data=data, 309 files=files, 310 json=json, 311 params=params, 312 headers=headers, 313 cookies=cookies, 314 auth=auth, 315 proxy=proxy, 316 follow_redirects=follow_redirects, 317 verify=verify, 318 timeout=timeout, 319 trust_env=trust_env, 320 ) File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_api.py:109, in request(method, url, params, content, data, files, json, headers, cookies, auth, proxy, timeout, follow_redirects, verify, trust_env) 57 """ 58 Sends an HTTP request. 59 (...) 100 ``` 101 """ 102 with Client( 103 cookies=cookies, 104 proxy=proxy, (...) 107 trust_env=trust_env, 108 ) as client: --> 109 return client.request( 110 method=method, 111 url=url, 112 content=content, 113 data=data, 114 files=files, 115 json=json, 116 params=params, 117 headers=headers, 118 auth=auth, 119 follow_redirects=follow_redirects, 120 ) File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:825, in Client.request(self, method, url, content, data, files, json, params, headers, cookies, auth, follow_redirects, timeout, extensions) 810 warnings.warn(message, DeprecationWarning, stacklevel=2) 812 request = self.build_request( 813 method=method, 814 url=url, (...) 823 extensions=extensions, 824 ) --> 825 return self.send(request, auth=auth, follow_redirects=follow_redirects) File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:914, in Client.send(self, request, stream, auth, follow_redirects) 910 self._set_timeout(request) 912 auth = self._build_request_auth(request, auth) --> 914 response = self._send_handling_auth( 915 request, 916 auth=auth, 917 follow_redirects=follow_redirects, 918 history=[], 919 ) 920 try: 921 if not stream: File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:942, in Client._send_handling_auth(self, request, auth, follow_redirects, history) 939 request = next(auth_flow) 941 while True: --> 942 response = self._send_handling_redirects( 943 request, 944 follow_redirects=follow_redirects, 945 history=history, 946 ) 947 try: 948 try: File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:979, in Client._send_handling_redirects(self, request, follow_redirects, history) 976 for hook in self._event_hooks["request"]: 977 hook(request) --> 979 response = self._send_single_request(request) 980 try: 981 for hook in self._event_hooks["response"]: File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:1014, in Client._send_single_request(self, request) 1009 raise RuntimeError( 1010 "Attempted to send an async request with a sync Client instance." 1011 ) 1013 with request_context(request=request): -> 1014 response = transport.handle_request(request) 1016 assert isinstance(response.stream, SyncByteStream) 1018 response.request = request File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_transports/default.py:249, in HTTPTransport.handle_request(self, request) 235 import httpcore 237 req = httpcore.Request( 238 method=request.method, 239 url=httpcore.URL( (...) 247 extensions=request.extensions, 248 ) --> 249 with map_httpcore_exceptions(): 250 resp = self._pool.handle_request(req) 252 assert isinstance(resp.stream, typing.Iterable) File /Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py:158, in _GeneratorContextManager.__exit__(self, typ, value, traceback) 156 value = typ() 157 try: --> 158 self.gen.throw(value) 159 except StopIteration as exc: 160 # Suppress StopIteration *unless* it's the same exception that 161 # was passed to throw(). This prevents a StopIteration 162 # raised inside the "with" statement from being suppressed. 163 return exc is not value File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_transports/default.py:118, in map_httpcore_exceptions() 115 raise 117 message = str(exc) --> 118 raise mapped_exc(message) from exc ReadTimeout: The read operation timed out
= get_payment_request(o['payment_request_url'], o['payment_context_token'], o['offers'][0]['offer_id'], 'lightning')
r r
--------------------------------------------------------------------------- HTTPStatusError Traceback (most recent call last) Cell In[13], line 1 ----> 1 r = get_payment_request(o['payment_request_url'], o['payment_context_token'], o['offers'][0]['offer_id'], 'lightning') 2 r Cell In[11], line 17, in get_payment_request(payment_request_url, payment_context_token, offer_id, payment_method, chain, asset) 9 data = { 10 "offer_id": offer_id, 11 "payment_method": payment_method, (...) 14 "payment_context_token": payment_context_token 15 } 16 r = httpx.post(payment_request_url, json=data) ---> 17 r.raise_for_status() 18 return r.json() File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_models.py:829, in Response.raise_for_status(self) 827 error_type = error_types.get(status_class, "Invalid status code") 828 message = message.format(self, error_type=error_type) --> 829 raise HTTPStatusError(message, request=request, response=self) HTTPStatusError: Server error '500 Internal Server Error' for url 'https://hub-5n97k.ondigitalocean.app/v0/l402/payment-request' For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500
Client
Client (lightning_provider=None, credit_card_provider=None, onchain_provider=None, fewsats_provider=None)
Base class for objects needing a basic __repr__
= create_test_wallet(fund=False, chain=chain)
w = Client(onchain_provider=CoinbaseProvider(wallet=w, asset='usdc'))
c try:
c.pay(o)except Exception as e:
print(e)
Insufficient funds: have 0, need 1.
Fewsats Client
Fewsats
Fewsats (api_key:str=None, base_url:str='https://hub-5n97k.ondigitalocean.app')
Initialize self. See help(type(self)) for accurate signature.
= Fewsats()
f = Fewsats(base_url='http://localhost:8000', api_key='yQbJPudW-nqiTyx880t8G83oLGyoR-RlnrLDmEe0D58')
f f.get_payment_methods()
[{'id': 1,
'last4': '4242',
'brand': 'visa',
'exp_month': 12,
'exp_year': 2034,
'is_default': False},
{'id': 4,
'last4': '4242',
'brand': 'Visa',
'exp_month': 12,
'exp_year': 2034,
'is_default': True}]
= Client(fewsats_provider=Fewsats(base_url='http://localhost:8000', api_key='yQbJPudW-nqiTyx880t8G83oLGyoR-RlnrLDmEe0D58'))
c = None
err try:
c.pay(o)except Exception as e:
print(e.response.text)
{'payment_request_url': 'https://hub-5n97k.ondigitalocean.app/v0/l402/payment-request', 'payment_context_token': 'a1700b1a-8325-47a9-902a-f90e82d21427', 'payment_method': 'onchain', 'offer': {'offer_id': 'f52bcccd-1057-4204-b1ae-01707bfe29af', 'amount': 1, 'currency': 'USD', 'description': 'Purchase 1 credit for API access', 'title': '1 Credit Package', 'type': 'one-time', 'payment_methods': ['onchain']}}
http://localhost:8000
yQbJPudW-nqiTyx880t8G83oLGyoR-RlnrLDmEe0D58
Traceback (most recent call last):
File "/usr/local/lib/python3.12/site-packages/ninja/operation.py", line 341, in run
result = await self.view_func(request, **values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hub_api/l402/api/common/decorators.py", line 17, in wrapper
return await func(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hub_api/l402/api/v0/views.py", line 32, in l402_create_purchase_from_offer_v0
return await purchase_from_offer(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/asgiref/sync.py", line 518, in thread_handler
raise exc_info[1]
File "/opt/hub_api/common/decorators/aatomic.py", line 48, in wrapper
return await fun(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hub_api/l402/workflow.py", line 424, in purchase_from_offer
return await purchase_with_onchain(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/asgiref/sync.py", line 518, in thread_handler
raise exc_info[1]
File "/opt/hub_api/common/decorators/aatomic.py", line 48, in wrapper
return await fun(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hub_api/l402/workflow.py", line 496, in purchase_with_onchain
transfer = wallet.transfer(
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/cdp/wallet.py", line 466, in transfer
return self.default_address.transfer(amount, asset_id, destination, gasless, skip_batching)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/cdp/wallet_address.py", line 122, in transfer
self._ensure_sufficient_balance(normalized_amount, asset_id)
File "/usr/local/lib/python3.12/site-packages/cdp/wallet_address.py", line 463, in _ensure_sufficient_balance
raise InsufficientFundsError(expected=amount, exact=current_balance)
cdp.errors.InsufficientFundsError: Insufficient funds: have 0.499997, need 1.
err.response.json()
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[18], line 1 ----> 1 err.response.json() AttributeError: 'NoneType' object has no attribute 'response'
try:
f.pay(o)except Exception as e:
print(e)
name 'f' is not defined
= Fewsats()
f try:
=r['payment_request']['address'], amount="0.000001", chain='base-mainnet', asset='usdc')
f._pay_onchain(addressexcept Exception as e:
print(e)
{'address': '0x114113186ca748Ea629A08B762df34a93f4C7e64', 'amount': '0.000001', 'chain': 'base-mainnet', 'asset': 'usdc'}
The read operation timed out
o, r
({'offers': [{'amount': 1,
'currency': 'USD',
'description': 'Purchase 1 credit for API access',
'offer_id': '33d08b4a-1589-4519-a24f-ba67e2166f0b',
'payment_methods': ['onchain'],
'title': '1 Credit Package',
'type': 'one-time'}],
'payment_context_token': 'd5607f07-3ecd-4f84-a9f9-74db7f922760',
'payment_request_url': 'http://localhost:9000/payment_request',
'version': '0.2.2'},
{'expires_at': '2025-01-25T18:21:42.339161+00:00',
'offer_id': '33d08b4a-1589-4519-a24f-ba67e2166f0b',
'payment_request': {'address': '0xAa4b26Ca04692E6cAA310Dc05Feaf1dE75943d62',
'chain': 'base-mainnet',
'asset': 'usdc'},
'version': '0.2.2'})