oneshot-python 0.4.0__tar.gz → 0.5.0__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.4.0
3
+ Version: 0.5.0
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
@@ -55,7 +55,7 @@ Paid endpoints use the [x402 protocol](https://x402.org):
55
55
  1. Client POSTs to a tool endpoint
56
56
  2. API returns `402 Payment Required` with a USDC quote
57
57
  3. Client signs a `TransferWithAuthorization` (EIP-3009) locally
58
- 4. Client re-POSTs with the signed payment in the `x-payment` header
58
+ 4. Client re-POSTs with the signed payment in the `payment-signature` header
59
59
  5. API processes the request and returns the result
60
60
 
61
61
  Your private key never leaves your machine. All signing happens locally via `eth-account`.
@@ -23,7 +23,7 @@ from oneshot._errors import (
23
23
  ToolError,
24
24
  ValidationError,
25
25
  )
26
- from oneshot.x402 import sign_payment_authorization
26
+ from oneshot.x402 import sign_payment_authorization, encode_payment_header
27
27
 
28
28
  SDK_VERSION = "0.3.0"
29
29
 
@@ -162,10 +162,12 @@ class OneShotClient:
162
162
  network=f"eip155:{payment_request['chain_id']}",
163
163
  )
164
164
 
165
- # Step 4 — Re-POST with payment headers
165
+ # Step 4 — Re-POST with payment headers (x402 format)
166
+ auth_json = json.dumps(auth)
166
167
  headers = {
167
168
  **self._headers(),
168
- "x-payment": json.dumps(auth),
169
+ "payment-signature": encode_payment_header(auth),
170
+ "x-payment": auth_json, # backwards compat for identity extraction
169
171
  }
170
172
  if quote_id:
171
173
  headers["x-quote-id"] = quote_id
@@ -1,12 +1,16 @@
1
1
  """EIP-712 payment signing for x402 protocol (USDC TransferWithAuthorization).
2
2
 
3
- Ports the signing logic from:
4
- - libs/agent-sdk/src/index.ts (L1511-1568)
5
- - apps/api-service/src/services/x402-facilitator.ts (L28-29) for domain names
3
+ Produces x402 PaymentPayload v1 format compatible with @x402/core SDK:
4
+ { x402Version: 1, scheme: "exact", network: "eip155:8453",
5
+ payload: { signature: "0x...", authorization: { from, to, value, ... } } }
6
+
7
+ The payload is base64-encoded and sent as the `payment-signature` header.
6
8
  """
7
9
 
8
10
  from __future__ import annotations
9
11
 
12
+ import base64
13
+ import json
10
14
  import os
11
15
  import time
12
16
  from typing import Any, TypedDict
@@ -15,24 +19,13 @@ from eth_account import Account
15
19
  from eth_account.messages import encode_typed_data
16
20
 
17
21
 
18
- class PaymentSignature(TypedDict):
19
- v: int
20
- r: str
21
- s: str
22
-
23
-
24
22
  class PaymentAuthorization(TypedDict):
25
- """Mirrors the TS SDK PaymentAuthorization interface."""
26
-
27
- from_address: str # 'from' is reserved in Python
28
- to: str
29
- value: str
30
- validAfter: int
31
- validBefore: int
32
- nonce: str
33
- signature: PaymentSignature
23
+ """x402 PaymentPayload v1 format."""
24
+
25
+ x402Version: int
26
+ scheme: str
34
27
  network: str
35
- token: str
28
+ payload: dict[str, Any]
36
29
 
37
30
 
38
31
  def _get_usdc_domain_name(chain_id: int) -> str:
@@ -62,11 +55,13 @@ def sign_payment_authorization(
62
55
  network: CAIP-2 network string (e.g. "eip155:8453").
63
56
 
64
57
  Returns:
65
- Payment authorization dict ready to be JSON-serialized as x-payment header.
58
+ x402 PaymentPayload v1 dict.
66
59
  """
67
60
  # Convert human-readable amount to USDC base units (6 decimals)
68
61
  value = _parse_usdc_amount(amount)
69
62
  now = int(time.time())
63
+ valid_after = now - 300
64
+ valid_before = now + 3600
70
65
  nonce = "0x" + os.urandom(32).hex()
71
66
 
72
67
  domain_data = {
@@ -91,8 +86,8 @@ def sign_payment_authorization(
91
86
  "from": from_address,
92
87
  "to": to_address,
93
88
  "value": value,
94
- "validAfter": now - 300,
95
- "validBefore": now + 3600,
89
+ "validAfter": valid_after,
90
+ "validBefore": valid_before,
96
91
  "nonce": bytes.fromhex(nonce[2:]),
97
92
  }
98
93
 
@@ -103,23 +98,30 @@ def sign_payment_authorization(
103
98
  )
104
99
  signed = Account.sign_message(signable, private_key=private_key)
105
100
 
101
+ # x402 PaymentPayload v1 format
106
102
  return {
107
- "from": from_address,
108
- "to": to_address,
109
- "value": str(value),
110
- "validAfter": now - 300,
111
- "validBefore": now + 3600,
112
- "nonce": nonce,
113
- "signature": {
114
- "v": signed.v,
115
- "r": hex(signed.r),
116
- "s": hex(signed.s),
117
- },
103
+ "x402Version": 1,
104
+ "scheme": "exact",
118
105
  "network": network,
119
- "token": token_address,
106
+ "payload": {
107
+ "signature": "0x" + (signed.signature.hex() if isinstance(signed.signature, bytes) else format(signed.signature, 'x')),
108
+ "authorization": {
109
+ "from": from_address,
110
+ "to": to_address,
111
+ "value": str(value),
112
+ "validAfter": str(valid_after),
113
+ "validBefore": str(valid_before),
114
+ "nonce": nonce,
115
+ },
116
+ },
120
117
  }
121
118
 
122
119
 
120
+ def encode_payment_header(auth: dict[str, Any]) -> str:
121
+ """Base64-encode a PaymentPayload dict for the payment-signature header."""
122
+ return base64.b64encode(json.dumps(auth).encode()).decode()
123
+
124
+
123
125
  def _parse_usdc_amount(amount: str) -> int:
124
126
  """Convert a human-readable USDC amount to base units (6 decimals).
125
127
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oneshot-python"
3
- version = "0.4.0"
3
+ version = "0.5.0"
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"
@@ -4,13 +4,19 @@ Verifies that:
4
4
  1. USDC amount parsing is correct (6 decimals)
5
5
  2. EIP-712 signatures are valid and recoverable
6
6
  3. Domain name is "USD Coin" for Base Mainnet
7
+ 4. Output matches x402 PaymentPayload v1 format
8
+ 5. Base64 encoding for payment-signature header works
7
9
  """
8
10
 
11
+ import base64
12
+ import json
13
+
9
14
  from eth_account import Account
10
15
 
11
16
  from oneshot.x402 import (
12
17
  _get_usdc_domain_name,
13
18
  _parse_usdc_amount,
19
+ encode_payment_header,
14
20
  sign_payment_authorization,
15
21
  )
16
22
 
@@ -49,7 +55,7 @@ class TestDomainName:
49
55
 
50
56
 
51
57
  class TestSignPaymentAuthorization:
52
- def test_returns_valid_structure(self) -> None:
58
+ def test_returns_x402_payload_v1_format(self) -> None:
53
59
  auth = sign_payment_authorization(
54
60
  private_key=TEST_PRIVATE_KEY,
55
61
  from_address=TEST_ADDRESS,
@@ -60,22 +66,25 @@ class TestSignPaymentAuthorization:
60
66
  network="eip155:8453",
61
67
  )
62
68
 
63
- # Check all required fields
64
- assert auth["from"] == TEST_ADDRESS
65
- assert auth["to"] == "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
66
- assert auth["value"] == "1000000"
67
- assert isinstance(auth["validAfter"], int)
68
- assert isinstance(auth["validBefore"], int)
69
- assert auth["validBefore"] > auth["validAfter"]
70
- assert auth["nonce"].startswith("0x") and len(auth["nonce"]) == 66
69
+ # x402 PaymentPayload v1 envelope
70
+ assert auth["x402Version"] == 1
71
+ assert auth["scheme"] == "exact"
71
72
  assert auth["network"] == "eip155:8453"
72
- assert auth["token"] == USDC_ADDRESS
73
73
 
74
- # Check signature
75
- sig = auth["signature"]
76
- assert sig["v"] in (27, 28)
77
- assert sig["r"].startswith("0x")
78
- assert sig["s"].startswith("0x")
74
+ # Payload contains signature and authorization
75
+ payload = auth["payload"]
76
+ assert isinstance(payload["signature"], str)
77
+ assert payload["signature"].startswith("0x")
78
+
79
+ # Authorization fields
80
+ authorization = payload["authorization"]
81
+ assert authorization["from"] == TEST_ADDRESS
82
+ assert authorization["to"] == "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
83
+ assert authorization["value"] == "1000000"
84
+ assert isinstance(authorization["validAfter"], str)
85
+ assert isinstance(authorization["validBefore"], str)
86
+ assert int(authorization["validBefore"]) > int(authorization["validAfter"])
87
+ assert authorization["nonce"].startswith("0x") and len(authorization["nonce"]) == 66
79
88
 
80
89
  def test_different_calls_produce_different_nonces(self) -> None:
81
90
  auth1 = sign_payment_authorization(
@@ -97,4 +106,24 @@ class TestSignPaymentAuthorization:
97
106
  network="eip155:8453",
98
107
  )
99
108
 
100
- assert auth1["nonce"] != auth2["nonce"]
109
+ assert auth1["payload"]["authorization"]["nonce"] != auth2["payload"]["authorization"]["nonce"]
110
+
111
+
112
+ class TestEncodePaymentHeader:
113
+ def test_base64_roundtrip(self) -> None:
114
+ auth = sign_payment_authorization(
115
+ private_key=TEST_PRIVATE_KEY,
116
+ from_address=TEST_ADDRESS,
117
+ to_address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
118
+ amount="0.50",
119
+ token_address=USDC_ADDRESS,
120
+ chain_id=CHAIN_ID,
121
+ network="eip155:8453",
122
+ )
123
+
124
+ encoded = encode_payment_header(auth)
125
+ decoded = json.loads(base64.b64decode(encoded))
126
+
127
+ assert decoded["x402Version"] == 1
128
+ assert decoded["scheme"] == "exact"
129
+ assert decoded["payload"]["authorization"]["value"] == "500000"