oneshot-python 0.8.1__tar.gz → 0.8.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/PKG-INFO +1 -1
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/oneshot/client.py +196 -5
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/oneshot/x402.py +36 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/pyproject.toml +1 -1
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/tests/test_x402.py +96 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/.gitignore +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/README.md +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/oneshot/__init__.py +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/oneshot/_errors.py +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/tests/__init__.py +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/tests/test_balance.py +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.3}/uv.lock +0 -0
|
@@ -23,9 +23,14 @@ from oneshot._errors import (
|
|
|
23
23
|
ToolError,
|
|
24
24
|
ValidationError,
|
|
25
25
|
)
|
|
26
|
-
from oneshot.x402 import
|
|
26
|
+
from oneshot.x402 import (
|
|
27
|
+
build_zero_cost_authorization,
|
|
28
|
+
encode_payment_header,
|
|
29
|
+
parse_payment_required,
|
|
30
|
+
sign_payment_authorization,
|
|
31
|
+
)
|
|
27
32
|
|
|
28
|
-
SDK_VERSION = "0.8.
|
|
33
|
+
SDK_VERSION = "0.8.3"
|
|
29
34
|
|
|
30
35
|
# ---------------------------------------------------------------------------
|
|
31
36
|
# Environment configuration
|
|
@@ -157,11 +162,21 @@ class OneShotClient:
|
|
|
157
162
|
|
|
158
163
|
self._log(f"Payment required: {payment_request['amount']} USDC")
|
|
159
164
|
|
|
160
|
-
# Step 3 — Sign x402 payment (
|
|
165
|
+
# Step 3 — Sign x402 payment (zero-cost auth if credits cover full cost)
|
|
161
166
|
amount = float(payment_request["amount"])
|
|
162
167
|
if amount == 0:
|
|
163
|
-
self._log("Credits cover full cost —
|
|
164
|
-
|
|
168
|
+
self._log("Credits cover full cost — sending zero-cost authorization")
|
|
169
|
+
auth = build_zero_cost_authorization(
|
|
170
|
+
from_address=self.address,
|
|
171
|
+
to_address=payment_request["recipient"],
|
|
172
|
+
accepted=accepted,
|
|
173
|
+
resource=parsed_req.get("resource"),
|
|
174
|
+
extensions=parsed_req.get("extensions"),
|
|
175
|
+
)
|
|
176
|
+
headers = {
|
|
177
|
+
**self._headers(),
|
|
178
|
+
"payment-signature": encode_payment_header(auth),
|
|
179
|
+
}
|
|
165
180
|
if quote_id:
|
|
166
181
|
headers["x-quote-id"] = quote_id
|
|
167
182
|
resp2 = await client.post(url, headers=headers, json=payload)
|
|
@@ -389,6 +404,182 @@ class OneShotClient:
|
|
|
389
404
|
"""Delete a browser profile. Async."""
|
|
390
405
|
return await self.acall_free_delete(f"/v1/tools/browser/profiles/{profile_id}")
|
|
391
406
|
|
|
407
|
+
# ------------------------------------------------------------------
|
|
408
|
+
# Paid tool convenience methods
|
|
409
|
+
# ------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
def research(self, topic: str, *, depth: str = "deep", **kwargs: Any) -> Any:
|
|
412
|
+
"""Run deep web research on a topic. Blocking."""
|
|
413
|
+
return self.call_tool("/v1/tools/research", {"topic": topic, "depth": depth, **kwargs})
|
|
414
|
+
|
|
415
|
+
async def aresearch(self, topic: str, *, depth: str = "deep", **kwargs: Any) -> Any:
|
|
416
|
+
"""Run deep web research on a topic. Async."""
|
|
417
|
+
return await self.acall_tool("/v1/tools/research", {"topic": topic, "depth": depth, **kwargs})
|
|
418
|
+
|
|
419
|
+
def web_search(self, query: str, *, max_results: int = 5, **kwargs: Any) -> Any:
|
|
420
|
+
"""Search the web. Blocking."""
|
|
421
|
+
return self.call_tool("/v1/tools/search", {"query": query, "max_results": max_results, **kwargs})
|
|
422
|
+
|
|
423
|
+
async def aweb_search(self, query: str, *, max_results: int = 5, **kwargs: Any) -> Any:
|
|
424
|
+
"""Search the web. Async."""
|
|
425
|
+
return await self.acall_tool("/v1/tools/search", {"query": query, "max_results": max_results, **kwargs})
|
|
426
|
+
|
|
427
|
+
def web_read(self, url: str, **kwargs: Any) -> Any:
|
|
428
|
+
"""Read a web page and extract content as markdown. Blocking."""
|
|
429
|
+
return self.call_tool("/v1/tools/web-read", {"url": url, **kwargs})
|
|
430
|
+
|
|
431
|
+
async def aweb_read(self, url: str, **kwargs: Any) -> Any:
|
|
432
|
+
"""Read a web page and extract content as markdown. Async."""
|
|
433
|
+
return await self.acall_tool("/v1/tools/web-read", {"url": url, **kwargs})
|
|
434
|
+
|
|
435
|
+
def email(self, to: str, subject: str, body: str, *, from_domain: Optional[str] = None, **kwargs: Any) -> Any:
|
|
436
|
+
"""Send an email. Blocking."""
|
|
437
|
+
payload: dict[str, Any] = {"to": to, "subject": subject, "body": body, **kwargs}
|
|
438
|
+
if from_domain:
|
|
439
|
+
payload["from_domain"] = from_domain
|
|
440
|
+
return self.call_tool("/v1/tools/email/send", payload)
|
|
441
|
+
|
|
442
|
+
async def aemail(self, to: str, subject: str, body: str, *, from_domain: Optional[str] = None, **kwargs: Any) -> Any:
|
|
443
|
+
"""Send an email. Async."""
|
|
444
|
+
payload: dict[str, Any] = {"to": to, "subject": subject, "body": body, **kwargs}
|
|
445
|
+
if from_domain:
|
|
446
|
+
payload["from_domain"] = from_domain
|
|
447
|
+
return await self.acall_tool("/v1/tools/email/send", payload)
|
|
448
|
+
|
|
449
|
+
def voice(self, objective: str, target_number: str, **kwargs: Any) -> Any:
|
|
450
|
+
"""Make an AI voice call. Blocking."""
|
|
451
|
+
return self.call_tool("/v1/tools/voice/call", {"objective": objective, "target_number": target_number, **kwargs})
|
|
452
|
+
|
|
453
|
+
async def avoice(self, objective: str, target_number: str, **kwargs: Any) -> Any:
|
|
454
|
+
"""Make an AI voice call. Async."""
|
|
455
|
+
return await self.acall_tool("/v1/tools/voice/call", {"objective": objective, "target_number": target_number, **kwargs})
|
|
456
|
+
|
|
457
|
+
def sms(self, message: str, to_number: str, **kwargs: Any) -> Any:
|
|
458
|
+
"""Send an SMS message. Blocking."""
|
|
459
|
+
return self.call_tool("/v1/tools/sms/send", {"message": message, "to_number": to_number, **kwargs})
|
|
460
|
+
|
|
461
|
+
async def asms(self, message: str, to_number: str, **kwargs: Any) -> Any:
|
|
462
|
+
"""Send an SMS message. Async."""
|
|
463
|
+
return await self.acall_tool("/v1/tools/sms/send", {"message": message, "to_number": to_number, **kwargs})
|
|
464
|
+
|
|
465
|
+
def people_search(self, **kwargs: Any) -> Any:
|
|
466
|
+
"""Search for people by job title, company, etc. Blocking."""
|
|
467
|
+
return self.call_tool("/v1/tools/research/people", kwargs)
|
|
468
|
+
|
|
469
|
+
async def apeople_search(self, **kwargs: Any) -> Any:
|
|
470
|
+
"""Search for people by job title, company, etc. Async."""
|
|
471
|
+
return await self.acall_tool("/v1/tools/research/people", kwargs)
|
|
472
|
+
|
|
473
|
+
def enrich_profile(self, **kwargs: Any) -> Any:
|
|
474
|
+
"""Enrich a person's profile from LinkedIn URL, email, or name. Blocking."""
|
|
475
|
+
return self.call_tool("/v1/tools/enrich/profile", kwargs)
|
|
476
|
+
|
|
477
|
+
async def aenrich_profile(self, **kwargs: Any) -> Any:
|
|
478
|
+
"""Enrich a person's profile from LinkedIn URL, email, or name. Async."""
|
|
479
|
+
return await self.acall_tool("/v1/tools/enrich/profile", kwargs)
|
|
480
|
+
|
|
481
|
+
def find_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
|
|
482
|
+
"""Find a person's email address. Blocking."""
|
|
483
|
+
payload: dict[str, Any] = {"company_domain": company_domain, **kwargs}
|
|
484
|
+
if full_name:
|
|
485
|
+
payload["full_name"] = full_name
|
|
486
|
+
if first_name:
|
|
487
|
+
payload["first_name"] = first_name
|
|
488
|
+
if last_name:
|
|
489
|
+
payload["last_name"] = last_name
|
|
490
|
+
return self.call_tool("/v1/tools/enrich/email", payload)
|
|
491
|
+
|
|
492
|
+
async def afind_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
|
|
493
|
+
"""Find a person's email address. Async."""
|
|
494
|
+
payload: dict[str, Any] = {"company_domain": company_domain, **kwargs}
|
|
495
|
+
if full_name:
|
|
496
|
+
payload["full_name"] = full_name
|
|
497
|
+
if first_name:
|
|
498
|
+
payload["first_name"] = first_name
|
|
499
|
+
if last_name:
|
|
500
|
+
payload["last_name"] = last_name
|
|
501
|
+
return await self.acall_tool("/v1/tools/enrich/email", payload)
|
|
502
|
+
|
|
503
|
+
def verify_email(self, email: str) -> Any:
|
|
504
|
+
"""Verify email deliverability. Blocking."""
|
|
505
|
+
return self.call_tool("/v1/tools/verify/email", {"email": email})
|
|
506
|
+
|
|
507
|
+
async def averify_email(self, email: str) -> Any:
|
|
508
|
+
"""Verify email deliverability. Async."""
|
|
509
|
+
return await self.acall_tool("/v1/tools/verify/email", {"email": email})
|
|
510
|
+
|
|
511
|
+
def deep_research_person(self, **kwargs: Any) -> Any:
|
|
512
|
+
"""Deep research on a person. Blocking."""
|
|
513
|
+
return self.call_tool("/v1/tools/research/person", kwargs)
|
|
514
|
+
|
|
515
|
+
async def adeep_research_person(self, **kwargs: Any) -> Any:
|
|
516
|
+
"""Deep research on a person. Async."""
|
|
517
|
+
return await self.acall_tool("/v1/tools/research/person", kwargs)
|
|
518
|
+
|
|
519
|
+
def social_profiles(self, **kwargs: Any) -> Any:
|
|
520
|
+
"""Discover social media profiles. Blocking."""
|
|
521
|
+
return self.call_tool("/v1/tools/research/social", kwargs)
|
|
522
|
+
|
|
523
|
+
async def asocial_profiles(self, **kwargs: Any) -> Any:
|
|
524
|
+
"""Discover social media profiles. Async."""
|
|
525
|
+
return await self.acall_tool("/v1/tools/research/social", kwargs)
|
|
526
|
+
|
|
527
|
+
def article_search(self, name: str, company: str, **kwargs: Any) -> Any:
|
|
528
|
+
"""Find articles by or about a person. Blocking."""
|
|
529
|
+
return self.call_tool("/v1/tools/research/articles", {"name": name, "company": company, **kwargs})
|
|
530
|
+
|
|
531
|
+
async def aarticle_search(self, name: str, company: str, **kwargs: Any) -> Any:
|
|
532
|
+
"""Find articles by or about a person. Async."""
|
|
533
|
+
return await self.acall_tool("/v1/tools/research/articles", {"name": name, "company": company, **kwargs})
|
|
534
|
+
|
|
535
|
+
def person_newsfeed(self, social_media_url: str, **kwargs: Any) -> Any:
|
|
536
|
+
"""Get recent social posts from a person. Blocking."""
|
|
537
|
+
return self.call_tool("/v1/tools/research/newsfeed", {"social_media_url": social_media_url, **kwargs})
|
|
538
|
+
|
|
539
|
+
async def aperson_newsfeed(self, social_media_url: str, **kwargs: Any) -> Any:
|
|
540
|
+
"""Get recent social posts from a person. Async."""
|
|
541
|
+
return await self.acall_tool("/v1/tools/research/newsfeed", {"social_media_url": social_media_url, **kwargs})
|
|
542
|
+
|
|
543
|
+
def person_interests(self, **kwargs: Any) -> Any:
|
|
544
|
+
"""Analyze a person's interests from their digital footprint. Blocking."""
|
|
545
|
+
return self.call_tool("/v1/tools/research/interests", kwargs)
|
|
546
|
+
|
|
547
|
+
async def aperson_interests(self, **kwargs: Any) -> Any:
|
|
548
|
+
"""Analyze a person's interests from their digital footprint. Async."""
|
|
549
|
+
return await self.acall_tool("/v1/tools/research/interests", kwargs)
|
|
550
|
+
|
|
551
|
+
def person_interactions(self, social_media_url: str, **kwargs: Any) -> Any:
|
|
552
|
+
"""Get a person's social interactions. Blocking."""
|
|
553
|
+
return self.call_tool("/v1/tools/research/interactions", {"social_media_url": social_media_url, **kwargs})
|
|
554
|
+
|
|
555
|
+
async def aperson_interactions(self, social_media_url: str, **kwargs: Any) -> Any:
|
|
556
|
+
"""Get a person's social interactions. Async."""
|
|
557
|
+
return await self.acall_tool("/v1/tools/research/interactions", {"social_media_url": social_media_url, **kwargs})
|
|
558
|
+
|
|
559
|
+
def commerce_search(self, query: str, *, limit: int = 10, **kwargs: Any) -> Any:
|
|
560
|
+
"""Search for products. Blocking."""
|
|
561
|
+
return self.call_tool("/v1/tools/commerce/search", {"query": query, "limit": limit, **kwargs})
|
|
562
|
+
|
|
563
|
+
async def acommerce_search(self, query: str, *, limit: int = 10, **kwargs: Any) -> Any:
|
|
564
|
+
"""Search for products. Async."""
|
|
565
|
+
return await self.acall_tool("/v1/tools/commerce/search", {"query": query, "limit": limit, **kwargs})
|
|
566
|
+
|
|
567
|
+
def commerce_buy(self, product_url: str, shipping_address: dict[str, Any], **kwargs: Any) -> Any:
|
|
568
|
+
"""Purchase a product. Blocking."""
|
|
569
|
+
return self.call_tool("/v1/tools/commerce/buy", {"product_url": product_url, "shipping_address": shipping_address, **kwargs})
|
|
570
|
+
|
|
571
|
+
async def acommerce_buy(self, product_url: str, shipping_address: dict[str, Any], **kwargs: Any) -> Any:
|
|
572
|
+
"""Purchase a product. Async."""
|
|
573
|
+
return await self.acall_tool("/v1/tools/commerce/buy", {"product_url": product_url, "shipping_address": shipping_address, **kwargs})
|
|
574
|
+
|
|
575
|
+
def build(self, product: dict[str, Any], **kwargs: Any) -> Any:
|
|
576
|
+
"""Build a website. Blocking."""
|
|
577
|
+
return self.call_tool("/v1/tools/build", {"product": product, **kwargs})
|
|
578
|
+
|
|
579
|
+
async def abuild(self, product: dict[str, Any], **kwargs: Any) -> Any:
|
|
580
|
+
"""Build a website. Async."""
|
|
581
|
+
return await self.acall_tool("/v1/tools/build", {"product": product, **kwargs})
|
|
582
|
+
|
|
392
583
|
# ------------------------------------------------------------------
|
|
393
584
|
# Balance
|
|
394
585
|
# ------------------------------------------------------------------
|
|
@@ -192,6 +192,42 @@ def sign_payment_authorization(
|
|
|
192
192
|
return result
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
def build_zero_cost_authorization(
|
|
196
|
+
*,
|
|
197
|
+
from_address: str,
|
|
198
|
+
to_address: str,
|
|
199
|
+
accepted: PaymentRequirements | dict[str, Any] | None = None,
|
|
200
|
+
resource: dict[str, Any] | None = None,
|
|
201
|
+
extensions: dict[str, Any] | None = None,
|
|
202
|
+
) -> dict[str, Any]:
|
|
203
|
+
"""Build a zero-cost x402 PaymentPayload for credit-covered requests.
|
|
204
|
+
|
|
205
|
+
Instead of skipping the payment header entirely (which causes the server's
|
|
206
|
+
x402 SDK to return a 402 error), we send a stub authorization with value=0
|
|
207
|
+
and signature=0x so the server can recognise the zero-cost intent.
|
|
208
|
+
"""
|
|
209
|
+
result: dict[str, Any] = {
|
|
210
|
+
"x402Version": 2,
|
|
211
|
+
"accepted": dict(accepted) if accepted else {},
|
|
212
|
+
"payload": {
|
|
213
|
+
"signature": "0x",
|
|
214
|
+
"authorization": {
|
|
215
|
+
"from": from_address,
|
|
216
|
+
"to": to_address,
|
|
217
|
+
"value": "0",
|
|
218
|
+
"validAfter": "0",
|
|
219
|
+
"validBefore": "0",
|
|
220
|
+
"nonce": "0x" + "00" * 32,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
if resource:
|
|
225
|
+
result["resource"] = resource
|
|
226
|
+
if extensions:
|
|
227
|
+
result["extensions"] = extensions
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
|
|
195
231
|
def encode_payment_header(auth: dict[str, Any]) -> str:
|
|
196
232
|
"""Base64-encode a PaymentPayload dict for the payment-signature header."""
|
|
197
233
|
return base64.b64encode(json.dumps(auth).encode()).decode()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oneshot-python"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.3"
|
|
4
4
|
description = "Core Python SDK for the OneShot API — HTTP client with x402 payment handling"
|
|
5
5
|
readme = {text = "Core Python SDK for the OneShot API", content-type = "text/plain"}
|
|
6
6
|
license = "MIT"
|
|
@@ -15,6 +15,7 @@ from eth_account import Account
|
|
|
15
15
|
|
|
16
16
|
from oneshot.x402 import (
|
|
17
17
|
_parse_usdc_amount,
|
|
18
|
+
build_zero_cost_authorization,
|
|
18
19
|
encode_payment_header,
|
|
19
20
|
parse_payment_required,
|
|
20
21
|
sign_payment_authorization,
|
|
@@ -158,6 +159,101 @@ class TestSignPaymentAuthorization:
|
|
|
158
159
|
assert auth1["payload"]["authorization"]["nonce"] != auth2["payload"]["authorization"]["nonce"]
|
|
159
160
|
|
|
160
161
|
|
|
162
|
+
class TestBuildZeroCostAuthorization:
|
|
163
|
+
def test_returns_valid_payload(self) -> None:
|
|
164
|
+
auth = build_zero_cost_authorization(
|
|
165
|
+
from_address=TEST_ADDRESS,
|
|
166
|
+
to_address=TO_ADDRESS,
|
|
167
|
+
accepted=MOCK_ACCEPTED,
|
|
168
|
+
)
|
|
169
|
+
assert auth["x402Version"] == 2
|
|
170
|
+
assert auth["payload"]["signature"] == "0x"
|
|
171
|
+
assert auth["payload"]["authorization"]["value"] == "0"
|
|
172
|
+
|
|
173
|
+
def test_authorization_addresses(self) -> None:
|
|
174
|
+
auth = build_zero_cost_authorization(
|
|
175
|
+
from_address=TEST_ADDRESS,
|
|
176
|
+
to_address=TO_ADDRESS,
|
|
177
|
+
accepted=MOCK_ACCEPTED,
|
|
178
|
+
)
|
|
179
|
+
assert auth["payload"]["authorization"]["from"] == TEST_ADDRESS
|
|
180
|
+
assert auth["payload"]["authorization"]["to"] == TO_ADDRESS
|
|
181
|
+
|
|
182
|
+
def test_nonce_is_32_zero_bytes(self) -> None:
|
|
183
|
+
auth = build_zero_cost_authorization(
|
|
184
|
+
from_address=TEST_ADDRESS,
|
|
185
|
+
to_address=TO_ADDRESS,
|
|
186
|
+
)
|
|
187
|
+
nonce = auth["payload"]["authorization"]["nonce"]
|
|
188
|
+
assert nonce == "0x" + "00" * 32
|
|
189
|
+
assert len(nonce) == 66
|
|
190
|
+
|
|
191
|
+
def test_includes_resource_when_provided(self) -> None:
|
|
192
|
+
resource = {"url": "https://api.example.com/v1/tools/research", "mimeType": "application/json"}
|
|
193
|
+
auth = build_zero_cost_authorization(
|
|
194
|
+
from_address=TEST_ADDRESS,
|
|
195
|
+
to_address=TO_ADDRESS,
|
|
196
|
+
resource=resource,
|
|
197
|
+
)
|
|
198
|
+
assert auth["resource"] == resource
|
|
199
|
+
|
|
200
|
+
def test_omits_resource_when_not_provided(self) -> None:
|
|
201
|
+
auth = build_zero_cost_authorization(
|
|
202
|
+
from_address=TEST_ADDRESS,
|
|
203
|
+
to_address=TO_ADDRESS,
|
|
204
|
+
)
|
|
205
|
+
assert "resource" not in auth
|
|
206
|
+
|
|
207
|
+
def test_includes_extensions_when_provided(self) -> None:
|
|
208
|
+
extensions = {"discovery": {"bodyType": "json", "input": {"topic": "AI"}}}
|
|
209
|
+
auth = build_zero_cost_authorization(
|
|
210
|
+
from_address=TEST_ADDRESS,
|
|
211
|
+
to_address=TO_ADDRESS,
|
|
212
|
+
extensions=extensions,
|
|
213
|
+
)
|
|
214
|
+
assert auth["extensions"] == extensions
|
|
215
|
+
|
|
216
|
+
def test_omits_extensions_when_not_provided(self) -> None:
|
|
217
|
+
auth = build_zero_cost_authorization(
|
|
218
|
+
from_address=TEST_ADDRESS,
|
|
219
|
+
to_address=TO_ADDRESS,
|
|
220
|
+
)
|
|
221
|
+
assert "extensions" not in auth
|
|
222
|
+
|
|
223
|
+
def test_echoes_accepted_requirements(self) -> None:
|
|
224
|
+
auth = build_zero_cost_authorization(
|
|
225
|
+
from_address=TEST_ADDRESS,
|
|
226
|
+
to_address=TO_ADDRESS,
|
|
227
|
+
accepted=MOCK_ACCEPTED,
|
|
228
|
+
)
|
|
229
|
+
assert auth["accepted"]["scheme"] == "exact"
|
|
230
|
+
assert auth["accepted"]["payTo"] == TO_ADDRESS
|
|
231
|
+
|
|
232
|
+
def test_base64_roundtrip(self) -> None:
|
|
233
|
+
"""Encoded zero-cost auth should be decodable by the server."""
|
|
234
|
+
auth = build_zero_cost_authorization(
|
|
235
|
+
from_address=TEST_ADDRESS,
|
|
236
|
+
to_address=TO_ADDRESS,
|
|
237
|
+
accepted=MOCK_ACCEPTED,
|
|
238
|
+
)
|
|
239
|
+
encoded = encode_payment_header(auth)
|
|
240
|
+
decoded = json.loads(base64.b64decode(encoded))
|
|
241
|
+
assert decoded["payload"]["authorization"]["value"] == "0"
|
|
242
|
+
assert decoded["payload"]["signature"] == "0x"
|
|
243
|
+
|
|
244
|
+
def test_server_detects_zero_value(self) -> None:
|
|
245
|
+
"""Server step 5c: checks decoded.payload.authorization.value === '0'."""
|
|
246
|
+
auth = build_zero_cost_authorization(
|
|
247
|
+
from_address=TEST_ADDRESS,
|
|
248
|
+
to_address=TO_ADDRESS,
|
|
249
|
+
accepted=MOCK_ACCEPTED,
|
|
250
|
+
)
|
|
251
|
+
encoded = encode_payment_header(auth)
|
|
252
|
+
decoded = json.loads(base64.b64decode(encoded))
|
|
253
|
+
# This is the exact check the server does in step 5c
|
|
254
|
+
assert decoded.get("payload", {}).get("authorization", {}).get("value") == "0"
|
|
255
|
+
|
|
256
|
+
|
|
161
257
|
class TestEncodePaymentHeader:
|
|
162
258
|
def test_base64_roundtrip(self) -> None:
|
|
163
259
|
auth = sign_payment_authorization(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|