oneshot-python 0.8.1__tar.gz → 0.8.2__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.2}/PKG-INFO +1 -1
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/oneshot/client.py +20 -5
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/oneshot/x402.py +36 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/pyproject.toml +1 -1
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/tests/test_x402.py +96 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/.gitignore +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/README.md +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/oneshot/__init__.py +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/oneshot/_errors.py +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/tests/__init__.py +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/tests/test_balance.py +0 -0
- {oneshot_python-0.8.1 → oneshot_python-0.8.2}/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.2"
|
|
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)
|
|
@@ -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.2"
|
|
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
|