oneshot-python 0.8.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oneshot-python
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Core Python SDK for the OneShot API — HTTP client with x402 payment handling
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -23,9 +23,14 @@ from oneshot._errors import (
23
23
  ToolError,
24
24
  ValidationError,
25
25
  )
26
- from oneshot.x402 import sign_payment_authorization, encode_payment_header, parse_payment_required
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.0"
33
+ SDK_VERSION = "0.8.2"
29
34
 
30
35
  # ---------------------------------------------------------------------------
31
36
  # Environment configuration
@@ -157,29 +162,47 @@ class OneShotClient:
157
162
 
158
163
  self._log(f"Payment required: {payment_request['amount']} USDC")
159
164
 
160
- # Step 3 — Sign x402 payment (v2 format with accepted requirements + Bazaar metadata)
161
- auth = sign_payment_authorization(
162
- private_key=self._private_key,
163
- from_address=self.address,
164
- to_address=payment_request["recipient"],
165
- amount=payment_request["amount"],
166
- token_address=payment_request["token_address"],
167
- chain_id=payment_request["chain_id"],
168
- network=f"eip155:{payment_request['chain_id']}",
169
- accepted=accepted,
170
- resource=parsed_req.get("resource"),
171
- extensions=parsed_req.get("extensions"),
172
- )
165
+ # Step 3 — Sign x402 payment (zero-cost auth if credits cover full cost)
166
+ amount = float(payment_request["amount"])
167
+ if amount == 0:
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
+ }
180
+ if quote_id:
181
+ headers["x-quote-id"] = quote_id
182
+ resp2 = await client.post(url, headers=headers, json=payload)
183
+ else:
184
+ auth = sign_payment_authorization(
185
+ private_key=self._private_key,
186
+ from_address=self.address,
187
+ to_address=payment_request["recipient"],
188
+ amount=payment_request["amount"],
189
+ token_address=payment_request["token_address"],
190
+ chain_id=payment_request["chain_id"],
191
+ network=f"eip155:{payment_request['chain_id']}",
192
+ accepted=accepted,
193
+ resource=parsed_req.get("resource"),
194
+ extensions=parsed_req.get("extensions"),
195
+ )
173
196
 
174
- # Step 4 — Re-POST with payment headers (x402 format)
175
- headers = {
176
- **self._headers(),
177
- "payment-signature": encode_payment_header(auth),
178
- }
179
- if quote_id:
180
- headers["x-quote-id"] = quote_id
197
+ # Step 4 — Re-POST with payment headers (x402 format)
198
+ headers = {
199
+ **self._headers(),
200
+ "payment-signature": encode_payment_header(auth),
201
+ }
202
+ if quote_id:
203
+ headers["x-quote-id"] = quote_id
181
204
 
182
- resp2 = await client.post(url, headers=headers, json=payload)
205
+ resp2 = await client.post(url, headers=headers, json=payload)
183
206
 
184
207
  if resp2.status_code not in (200, 201, 202):
185
208
  raise ToolError(
@@ -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.0"
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