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.
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/PKG-INFO +1 -1
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/README.md +1 -1
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/oneshot/client.py +5 -3
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/oneshot/x402.py +36 -34
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/pyproject.toml +1 -1
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/tests/test_x402.py +45 -16
- oneshot_python-0.5.0/uv.lock +1028 -0
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/.gitignore +0 -0
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/oneshot/__init__.py +0 -0
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/oneshot/_errors.py +0 -0
- {oneshot_python-0.4.0 → oneshot_python-0.5.0}/tests/__init__.py +0 -0
|
@@ -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 `
|
|
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
|
-
"
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
95
|
-
"validBefore":
|
|
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
|
-
"
|
|
108
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
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
|
|
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
|
-
#
|
|
64
|
-
assert auth["
|
|
65
|
-
assert auth["
|
|
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
|
-
#
|
|
75
|
-
|
|
76
|
-
assert
|
|
77
|
-
assert
|
|
78
|
-
|
|
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"
|