s402 0.2.0 → 0.2.1
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.
- package/CHANGELOG.md +15 -1
- package/README.md +14 -0
- package/package.json +13 -11
- package/test/conformance/README.md +172 -0
- package/test/conformance/vectors/body-transport.json +142 -0
- package/test/conformance/vectors/compat-normalize.json +259 -0
- package/test/conformance/vectors/payload-decode.json +110 -0
- package/test/conformance/vectors/payload-encode.json +127 -0
- package/test/conformance/vectors/receipt-format.json +668 -0
- package/test/conformance/vectors/receipt-parse.json +450 -0
- package/test/conformance/vectors/requirements-decode.json +377 -0
- package/test/conformance/vectors/requirements-encode.json +338 -0
- package/test/conformance/vectors/roundtrip.json +156 -0
- package/test/conformance/vectors/settle-decode.json +76 -0
- package/test/conformance/vectors/settle-encode.json +65 -0
- package/test/conformance/vectors/validation-reject.json +317 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.1] - 2026-03-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Conformance test vectors ship in npm package** — 133 machine-readable JSON test vectors across 12 files now included via `test/conformance/vectors`. Cross-language implementors (Go, Python, Rust) can `npm pack s402` to get the vectors without cloning the repo.
|
|
13
|
+
- **API stability declaration** — `API-STABILITY.md` classifies all 83 exports as stable, experimental, or internal.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Barrel export JSDoc updated to chain-agnostic wording (was "Sui-native").
|
|
18
|
+
|
|
8
19
|
## [0.2.0] - 2026-03-01
|
|
9
20
|
|
|
10
21
|
### Added
|
|
@@ -27,7 +38,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
27
38
|
|
|
28
39
|
- **BREAKING**: `s402RouteConfig.asset` is now required (was optional).
|
|
29
40
|
- JSDoc on `s402PaymentRequirements` updated to chain-agnostic wording (network, asset, amount fields).
|
|
30
|
-
-
|
|
41
|
+
- **Conformance test suite** — 133 machine-readable JSON test vectors across 12 files for cross-language implementation verification. Covers encode/decode, body transport, compat normalization, receipt format/parse, validation rejection, key-stripping, and roundtrip identity. Vectors ship in the npm package.
|
|
42
|
+
- **API stability declaration** — `API-STABILITY.md` classifies all 83 exports as stable/experimental/internal.
|
|
43
|
+
- 405 tests across 12 suites (was 207 at v0.1.0).
|
|
31
44
|
|
|
32
45
|
## [0.1.8] - 2026-02-27
|
|
33
46
|
|
|
@@ -116,6 +129,7 @@ _Version bump for npm publish after license change._
|
|
|
116
129
|
- Property-based fuzz testing via fast-check
|
|
117
130
|
- 207 tests, zero runtime dependencies
|
|
118
131
|
|
|
132
|
+
[0.2.1]: https://github.com/s402-protocol/core/compare/v0.2.0...v0.2.1
|
|
119
133
|
[0.2.0]: https://github.com/s402-protocol/core/compare/v0.1.8...v0.2.0
|
|
120
134
|
[0.1.8]: https://github.com/s402-protocol/core/compare/v0.1.7...v0.1.8
|
|
121
135
|
[0.1.7]: https://github.com/s402-protocol/core/compare/v0.1.6...v0.1.7
|
package/README.md
CHANGED
|
@@ -325,6 +325,20 @@ const requirements: s402PaymentRequirements = {
|
|
|
325
325
|
|
|
326
326
|
5. **Errors tell you what to do.** Every error code includes `retryable` (can the client try again?) and `suggestedAction` (what should it do?). Agents can self-recover.
|
|
327
327
|
|
|
328
|
+
## Conformance Testing
|
|
329
|
+
|
|
330
|
+
s402 ships 133 machine-readable JSON test vectors for cross-language conformance. If you're implementing s402 in Go, Python, Rust, or any other language, use these vectors to verify your implementation matches the spec.
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
# Vectors are in the npm package
|
|
334
|
+
ls node_modules/s402/test/conformance/vectors/
|
|
335
|
+
|
|
336
|
+
# Or clone the repo
|
|
337
|
+
ls test/conformance/vectors/
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
See [`test/conformance/README.md`](./test/conformance/README.md) for the vector format, encoding scheme, and implementation guide.
|
|
341
|
+
|
|
328
342
|
## Related
|
|
329
343
|
|
|
330
344
|
- **[SweeFi](https://github.com/sweeinc/sweefi)** — Open-source payment SDK built on s402. 10 packages including PTB builders, MCP tools, CLI, and UI components.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s402",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "s402 — Chain-agnostic HTTP 402 wire format. Types, HTTP encoding, and scheme registry for five payment schemes. Wire-compatible with x402. Zero runtime dependencies.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"README.md",
|
|
37
37
|
"LICENSE",
|
|
38
38
|
"CHANGELOG.md",
|
|
39
|
-
"SECURITY.md"
|
|
39
|
+
"SECURITY.md",
|
|
40
|
+
"test/conformance/vectors"
|
|
40
41
|
],
|
|
41
42
|
"main": "./dist/index.mjs",
|
|
42
43
|
"module": "./dist/index.mjs",
|
|
@@ -85,21 +86,22 @@
|
|
|
85
86
|
"default": "./dist/receipts.mjs"
|
|
86
87
|
}
|
|
87
88
|
},
|
|
88
|
-
"devDependencies": {
|
|
89
|
-
"@vitest/coverage-v8": "^3.2.4",
|
|
90
|
-
"fast-check": "^4.5.3",
|
|
91
|
-
"tsdown": "^0.20.3",
|
|
92
|
-
"typescript": "^5.7.0",
|
|
93
|
-
"vitepress": "^1.6.4",
|
|
94
|
-
"vitest": "^3.0.5"
|
|
95
|
-
},
|
|
96
89
|
"scripts": {
|
|
97
90
|
"build": "tsdown",
|
|
98
91
|
"typecheck": "tsc --noEmit",
|
|
99
92
|
"test": "vitest run",
|
|
100
93
|
"test:watch": "vitest",
|
|
94
|
+
"prepublishOnly": "npm run build && npm run typecheck && npm run test",
|
|
101
95
|
"docs:dev": "vitepress dev docs",
|
|
102
96
|
"docs:build": "vitepress build docs",
|
|
103
97
|
"docs:preview": "vitepress preview docs"
|
|
98
|
+
},
|
|
99
|
+
"devDependencies": {
|
|
100
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
101
|
+
"fast-check": "^4.5.3",
|
|
102
|
+
"tsdown": "^0.20.3",
|
|
103
|
+
"typescript": "^5.7.0",
|
|
104
|
+
"vitepress": "^1.6.4",
|
|
105
|
+
"vitest": "^3.0.5"
|
|
104
106
|
}
|
|
105
|
-
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# s402 Conformance Test Vectors
|
|
2
|
+
|
|
3
|
+
Machine-readable test vectors that define s402-compliant behavior. Use these to verify any s402 implementation — TypeScript, Go, Python, Rust, etc.
|
|
4
|
+
|
|
5
|
+
## Getting the Vectors
|
|
6
|
+
|
|
7
|
+
The vectors ship with the `s402` npm package:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm pack s402
|
|
11
|
+
tar xzf s402-*.tgz
|
|
12
|
+
ls package/test/conformance/vectors/
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or clone the repo directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/s402-protocol/core.git
|
|
19
|
+
ls core/test/conformance/vectors/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Vector Format
|
|
23
|
+
|
|
24
|
+
Each JSON file contains an array of test cases:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
[
|
|
28
|
+
{
|
|
29
|
+
"description": "Human-readable test name",
|
|
30
|
+
"input": { ... },
|
|
31
|
+
"expected": { ... },
|
|
32
|
+
"shouldReject": false
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Accept vectors (`shouldReject: false`)
|
|
38
|
+
|
|
39
|
+
The implementation MUST produce `expected` when given `input`.
|
|
40
|
+
|
|
41
|
+
### Reject vectors (`shouldReject: true`)
|
|
42
|
+
|
|
43
|
+
The implementation MUST reject the input with an error. The `expectedErrorCode` field indicates which error:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"description": "Rejects negative amount",
|
|
48
|
+
"input": { "header": "base64-encoded-malformed-json" },
|
|
49
|
+
"shouldReject": true,
|
|
50
|
+
"expectedErrorCode": "INVALID_PAYLOAD"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Vector Files
|
|
55
|
+
|
|
56
|
+
| File | Tests | Description |
|
|
57
|
+
|------|-------|-------------|
|
|
58
|
+
| `requirements-encode.json` | Encode | `s402PaymentRequirements` object → base64 header string |
|
|
59
|
+
| `requirements-decode.json` | Decode | base64 header string → `s402PaymentRequirements` object |
|
|
60
|
+
| `payload-encode.json` | Encode | `s402PaymentPayload` object → base64 header string |
|
|
61
|
+
| `payload-decode.json` | Decode | base64 header string → `s402PaymentPayload` object |
|
|
62
|
+
| `settle-encode.json` | Encode | `s402SettleResponse` object → base64 header string |
|
|
63
|
+
| `settle-decode.json` | Decode | base64 header string → `s402SettleResponse` object |
|
|
64
|
+
| `body-transport.json` | Both | Requirements/payload/settle via JSON body (no base64) |
|
|
65
|
+
| `compat-normalize.json` | Normalize | x402 V1/V2 input → normalized s402 output |
|
|
66
|
+
| `receipt-format.json` | Format | Receipt fields → `X-S402-Receipt` header string |
|
|
67
|
+
| `receipt-parse.json` | Parse | `X-S402-Receipt` header string → parsed receipt fields |
|
|
68
|
+
| `validation-reject.json` | Reject | Malformed inputs that MUST be rejected |
|
|
69
|
+
| `roundtrip.json` | Roundtrip | encode → decode → re-encode = identical output |
|
|
70
|
+
|
|
71
|
+
## Encoding Scheme
|
|
72
|
+
|
|
73
|
+
s402 uses **Unicode-safe base64** for HTTP header transport:
|
|
74
|
+
|
|
75
|
+
1. JSON-serialize the object: `JSON.stringify(obj)`
|
|
76
|
+
2. UTF-8 encode the string: `TextEncoder.encode(json)`
|
|
77
|
+
3. Base64 encode the bytes: `btoa(bytes.map(b => String.fromCharCode(b)).join(''))`
|
|
78
|
+
|
|
79
|
+
For ASCII-only content (the common case), this produces the same output as plain `btoa(json)`.
|
|
80
|
+
|
|
81
|
+
Body transport uses raw JSON (no base64).
|
|
82
|
+
|
|
83
|
+
## Receipt Header Format
|
|
84
|
+
|
|
85
|
+
The `X-S402-Receipt` header uses colon-separated fields:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
v2:base64(signature):callNumber:timestampMs:base64(responseHash)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `v2` — version prefix (always "v2" for signed receipts)
|
|
92
|
+
- `signature` — 64-byte Ed25519 signature, base64-encoded
|
|
93
|
+
- `callNumber` — sequential call number (1-indexed, bigint string)
|
|
94
|
+
- `timestampMs` — Unix timestamp in milliseconds (bigint string)
|
|
95
|
+
- `responseHash` — response body hash (typically SHA-256, 32 bytes), base64-encoded
|
|
96
|
+
|
|
97
|
+
In the vector JSON files, `signature` and `responseHash` are represented as arrays of byte values (0-255) since JSON has no binary type.
|
|
98
|
+
|
|
99
|
+
## Implementing in Another Language
|
|
100
|
+
|
|
101
|
+
1. Load the JSON vector files
|
|
102
|
+
2. For each vector where `shouldReject` is `false`:
|
|
103
|
+
- Feed `input` to your encode/decode function
|
|
104
|
+
- Compare the result to `expected`
|
|
105
|
+
3. For each vector where `shouldReject` is `true`:
|
|
106
|
+
- Feed `input` to your decode function
|
|
107
|
+
- Verify it throws/returns an error with the matching error code
|
|
108
|
+
4. For roundtrip vectors:
|
|
109
|
+
- Verify `expected.identical` is `true`
|
|
110
|
+
- Verify `expected.firstEncode === expected.reEncode`
|
|
111
|
+
|
|
112
|
+
### Key stripping on decode
|
|
113
|
+
|
|
114
|
+
All three decode functions (`decodePaymentRequired`, `decodePaymentPayload`, `decodeSettleResponse`) MUST strip unknown top-level keys. Only known fields should survive the decode. This is a defense-in-depth measure at the HTTP trust boundary.
|
|
115
|
+
|
|
116
|
+
**Requirements known top-level keys:** `s402Version`, `accepts`, `network`, `asset`, `amount`, `payTo`, `facilitatorUrl`, `mandate`, `protocolFeeBps`, `protocolFeeAddress`, `receiptRequired`, `settlementMode`, `expiresAt`, `stream`, `escrow`, `unlock`, `prepaid`, `extensions`.
|
|
117
|
+
|
|
118
|
+
**Requirements sub-object known keys** (also stripped of unknowns):
|
|
119
|
+
- `mandate`: `required`, `minPerTx`, `coinType`
|
|
120
|
+
- `stream`: `ratePerSecond`, `budgetCap`, `minDeposit`, `streamSetupUrl`
|
|
121
|
+
- `escrow`: `seller`, `arbiter`, `deadlineMs`
|
|
122
|
+
- `unlock`: `encryptionId`, `walrusBlobId`, `encryptionPackageId`
|
|
123
|
+
- `prepaid`: `ratePerCall`, `maxCalls`, `minDeposit`, `withdrawalDelayMs`, `providerPubkey`, `disputeWindowMs`
|
|
124
|
+
|
|
125
|
+
**Payload known top-level keys:** `s402Version`, `scheme`, `payload`.
|
|
126
|
+
|
|
127
|
+
**Payload inner keys** (per scheme):
|
|
128
|
+
- `exact`, `stream`, `escrow`: `transaction`, `signature`
|
|
129
|
+
- `unlock`: `transaction`, `signature`, `encryptionId`
|
|
130
|
+
- `prepaid`: `transaction`, `signature`, `ratePerCall`, `maxCalls`
|
|
131
|
+
|
|
132
|
+
**Settle response known keys:** `success`, `txDigest`, `receiptId`, `finalityMs`, `streamId`, `escrowId`, `balanceId`, `error`, `errorCode`.
|
|
133
|
+
|
|
134
|
+
### JSON key ordering
|
|
135
|
+
|
|
136
|
+
s402 vectors use JavaScript's `JSON.stringify()` key ordering (insertion order). Cross-language implementations MUST serialize keys in the same order as the vector files to produce identical base64 output. If your language's JSON serializer uses alphabetical ordering by default, you'll need ordered serialization (e.g., Go's `json.Marshal` on a struct with ordered fields, Python's `json.dumps` with `sort_keys=False`).
|
|
137
|
+
|
|
138
|
+
Note: **decode also reorders keys**. The decode functions iterate their allowlist in the order listed above, not the input order. If the input JSON has keys in a different order, the decoded output follows the allowlist order. This affects roundtrip tests — the re-encoded output uses the allowlist order, which may differ from the original input order.
|
|
139
|
+
|
|
140
|
+
### Header size limits
|
|
141
|
+
|
|
142
|
+
Implementations SHOULD enforce a maximum header size of **64 KiB** (`MAX_HEADER_BYTES = 65536`). Headers exceeding this limit indicate abuse or misconfiguration and SHOULD be rejected before base64 decoding.
|
|
143
|
+
|
|
144
|
+
### Rejection vector dispatch
|
|
145
|
+
|
|
146
|
+
Rejection vectors are dispatched in this precedence order:
|
|
147
|
+
|
|
148
|
+
1. If `expectedErrorCode` is `"RECEIPT_PARSE_ERROR"` → test `parseReceiptHeader`. Note: receipts throw a plain `Error`, not an `s402Error`, since receipts are a separate subsystem.
|
|
149
|
+
2. If `input.decodeAs` is `"payload"` → test `decodePaymentPayload`
|
|
150
|
+
3. If `input.decodeAs` is `"compat"` → test `normalizeRequirements` with the raw JSON (not base64)
|
|
151
|
+
4. Otherwise → test `decodePaymentRequired` (the default)
|
|
152
|
+
|
|
153
|
+
### Error codes
|
|
154
|
+
|
|
155
|
+
Rejection vectors use `expectedErrorCode` values from the s402 error code enum:
|
|
156
|
+
|
|
157
|
+
- `INVALID_PAYLOAD` — malformed or invalid input
|
|
158
|
+
- `RECEIPT_PARSE_ERROR` — malformed receipt header (vector convention for receipt-specific parsing errors)
|
|
159
|
+
|
|
160
|
+
## Regenerating Vectors
|
|
161
|
+
|
|
162
|
+
If you modify the s402 encoding logic, regenerate vectors:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npx tsx test/conformance/generate-vectors.ts
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Then run the conformance tests to verify:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pnpm run test
|
|
172
|
+
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"description": "Requirements body encode/decode",
|
|
4
|
+
"input": {
|
|
5
|
+
"type": "requirements",
|
|
6
|
+
"value": {
|
|
7
|
+
"s402Version": "1",
|
|
8
|
+
"accepts": [
|
|
9
|
+
"exact"
|
|
10
|
+
],
|
|
11
|
+
"network": "sui:mainnet",
|
|
12
|
+
"asset": "0x2::sui::SUI",
|
|
13
|
+
"amount": "1000000",
|
|
14
|
+
"payTo": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"expected": {
|
|
18
|
+
"body": "{\"s402Version\":\"1\",\"accepts\":[\"exact\"],\"network\":\"sui:mainnet\",\"asset\":\"0x2::sui::SUI\",\"amount\":\"1000000\",\"payTo\":\"0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890\"}",
|
|
19
|
+
"decoded": {
|
|
20
|
+
"s402Version": "1",
|
|
21
|
+
"accepts": [
|
|
22
|
+
"exact"
|
|
23
|
+
],
|
|
24
|
+
"network": "sui:mainnet",
|
|
25
|
+
"asset": "0x2::sui::SUI",
|
|
26
|
+
"amount": "1000000",
|
|
27
|
+
"payTo": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"shouldReject": false
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"description": "Requirements body with extensions",
|
|
34
|
+
"input": {
|
|
35
|
+
"type": "requirements",
|
|
36
|
+
"value": {
|
|
37
|
+
"s402Version": "1",
|
|
38
|
+
"accepts": [
|
|
39
|
+
"exact"
|
|
40
|
+
],
|
|
41
|
+
"network": "sui:mainnet",
|
|
42
|
+
"asset": "0x2::sui::SUI",
|
|
43
|
+
"amount": "1000000",
|
|
44
|
+
"payTo": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
|
45
|
+
"extensions": {
|
|
46
|
+
"auction": {
|
|
47
|
+
"type": "sealed-bid",
|
|
48
|
+
"deadline": 1708000000000
|
|
49
|
+
},
|
|
50
|
+
"customField": "hello world"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"expected": {
|
|
55
|
+
"body": "{\"s402Version\":\"1\",\"accepts\":[\"exact\"],\"network\":\"sui:mainnet\",\"asset\":\"0x2::sui::SUI\",\"amount\":\"1000000\",\"payTo\":\"0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890\",\"extensions\":{\"auction\":{\"type\":\"sealed-bid\",\"deadline\":1708000000000},\"customField\":\"hello world\"}}",
|
|
56
|
+
"decoded": {
|
|
57
|
+
"s402Version": "1",
|
|
58
|
+
"accepts": [
|
|
59
|
+
"exact"
|
|
60
|
+
],
|
|
61
|
+
"network": "sui:mainnet",
|
|
62
|
+
"asset": "0x2::sui::SUI",
|
|
63
|
+
"amount": "1000000",
|
|
64
|
+
"payTo": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
|
|
65
|
+
"extensions": {
|
|
66
|
+
"auction": {
|
|
67
|
+
"type": "sealed-bid",
|
|
68
|
+
"deadline": 1708000000000
|
|
69
|
+
},
|
|
70
|
+
"customField": "hello world"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"shouldReject": false
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"description": "Payload body encode/decode",
|
|
78
|
+
"input": {
|
|
79
|
+
"type": "payload",
|
|
80
|
+
"value": {
|
|
81
|
+
"s402Version": "1",
|
|
82
|
+
"scheme": "exact",
|
|
83
|
+
"payload": {
|
|
84
|
+
"transaction": "dHhfYnl0ZXM=",
|
|
85
|
+
"signature": "c2lnX2J5dGVz"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"expected": {
|
|
90
|
+
"body": "{\"s402Version\":\"1\",\"scheme\":\"exact\",\"payload\":{\"transaction\":\"dHhfYnl0ZXM=\",\"signature\":\"c2lnX2J5dGVz\"}}",
|
|
91
|
+
"decoded": {
|
|
92
|
+
"s402Version": "1",
|
|
93
|
+
"scheme": "exact",
|
|
94
|
+
"payload": {
|
|
95
|
+
"transaction": "dHhfYnl0ZXM=",
|
|
96
|
+
"signature": "c2lnX2J5dGVz"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"shouldReject": false
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"description": "Settle body encode/decode",
|
|
104
|
+
"input": {
|
|
105
|
+
"type": "settle",
|
|
106
|
+
"value": {
|
|
107
|
+
"success": true,
|
|
108
|
+
"txDigest": "ABC123",
|
|
109
|
+
"finalityMs": 300
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"expected": {
|
|
113
|
+
"body": "{\"success\":true,\"txDigest\":\"ABC123\",\"finalityMs\":300}",
|
|
114
|
+
"decoded": {
|
|
115
|
+
"success": true,
|
|
116
|
+
"txDigest": "ABC123",
|
|
117
|
+
"finalityMs": 300
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"shouldReject": false
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"description": "Settle failure body encode/decode",
|
|
124
|
+
"input": {
|
|
125
|
+
"type": "settle",
|
|
126
|
+
"value": {
|
|
127
|
+
"success": false,
|
|
128
|
+
"error": "Gas object not found",
|
|
129
|
+
"errorCode": "SETTLEMENT_FAILED"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"expected": {
|
|
133
|
+
"body": "{\"success\":false,\"error\":\"Gas object not found\",\"errorCode\":\"SETTLEMENT_FAILED\"}",
|
|
134
|
+
"decoded": {
|
|
135
|
+
"success": false,
|
|
136
|
+
"error": "Gas object not found",
|
|
137
|
+
"errorCode": "SETTLEMENT_FAILED"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"shouldReject": false
|
|
141
|
+
}
|
|
142
|
+
]
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"description": "x402 V1 flat format → s402",
|
|
4
|
+
"input": {
|
|
5
|
+
"x402Version": 1,
|
|
6
|
+
"scheme": "exact",
|
|
7
|
+
"network": "sui:mainnet",
|
|
8
|
+
"asset": "0x2::sui::SUI",
|
|
9
|
+
"amount": "1000000",
|
|
10
|
+
"payTo": "0xrecipient123",
|
|
11
|
+
"maxTimeoutSeconds": 60
|
|
12
|
+
},
|
|
13
|
+
"expected": {
|
|
14
|
+
"s402Version": "1",
|
|
15
|
+
"accepts": [
|
|
16
|
+
"exact"
|
|
17
|
+
],
|
|
18
|
+
"network": "sui:mainnet",
|
|
19
|
+
"asset": "0x2::sui::SUI",
|
|
20
|
+
"amount": "1000000",
|
|
21
|
+
"payTo": "0xrecipient123"
|
|
22
|
+
},
|
|
23
|
+
"shouldReject": false
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"description": "x402 V2 envelope (single offer) → s402",
|
|
27
|
+
"input": {
|
|
28
|
+
"x402Version": 2,
|
|
29
|
+
"accepts": [
|
|
30
|
+
{
|
|
31
|
+
"scheme": "exact",
|
|
32
|
+
"network": "sui:mainnet",
|
|
33
|
+
"asset": "0x2::sui::SUI",
|
|
34
|
+
"amount": "2000000",
|
|
35
|
+
"payTo": "0xrecipient456"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"expected": {
|
|
40
|
+
"s402Version": "1",
|
|
41
|
+
"accepts": [
|
|
42
|
+
"exact"
|
|
43
|
+
],
|
|
44
|
+
"network": "sui:mainnet",
|
|
45
|
+
"asset": "0x2::sui::SUI",
|
|
46
|
+
"amount": "2000000",
|
|
47
|
+
"payTo": "0xrecipient456"
|
|
48
|
+
},
|
|
49
|
+
"shouldReject": false
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"description": "x402 V2 envelope (multiple offers) → s402 (first offer taken)",
|
|
53
|
+
"input": {
|
|
54
|
+
"x402Version": 2,
|
|
55
|
+
"accepts": [
|
|
56
|
+
{
|
|
57
|
+
"scheme": "exact",
|
|
58
|
+
"network": "sui:mainnet",
|
|
59
|
+
"asset": "0x2::sui::SUI",
|
|
60
|
+
"amount": "1000000",
|
|
61
|
+
"payTo": "0xrecipientFirst"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"scheme": "exact",
|
|
65
|
+
"network": "eip155:8453",
|
|
66
|
+
"asset": "USDC",
|
|
67
|
+
"amount": "500000",
|
|
68
|
+
"payTo": "0xrecipientSecond"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"expected": {
|
|
73
|
+
"s402Version": "1",
|
|
74
|
+
"accepts": [
|
|
75
|
+
"exact"
|
|
76
|
+
],
|
|
77
|
+
"network": "sui:mainnet",
|
|
78
|
+
"asset": "0x2::sui::SUI",
|
|
79
|
+
"amount": "1000000",
|
|
80
|
+
"payTo": "0xrecipientFirst"
|
|
81
|
+
},
|
|
82
|
+
"shouldReject": false
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"description": "x402 V1 with maxAmountRequired → amount",
|
|
86
|
+
"input": {
|
|
87
|
+
"x402Version": 1,
|
|
88
|
+
"scheme": "exact",
|
|
89
|
+
"network": "eip155:8453",
|
|
90
|
+
"asset": "USDC",
|
|
91
|
+
"maxAmountRequired": "7500000",
|
|
92
|
+
"payTo": "0xethRecipient"
|
|
93
|
+
},
|
|
94
|
+
"expected": {
|
|
95
|
+
"s402Version": "1",
|
|
96
|
+
"accepts": [
|
|
97
|
+
"exact"
|
|
98
|
+
],
|
|
99
|
+
"network": "eip155:8453",
|
|
100
|
+
"asset": "USDC",
|
|
101
|
+
"amount": "7500000",
|
|
102
|
+
"payTo": "0xethRecipient"
|
|
103
|
+
},
|
|
104
|
+
"shouldReject": false
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"description": "Native s402 passed through unchanged",
|
|
108
|
+
"input": {
|
|
109
|
+
"s402Version": "1",
|
|
110
|
+
"accepts": [
|
|
111
|
+
"exact"
|
|
112
|
+
],
|
|
113
|
+
"network": "sui:mainnet",
|
|
114
|
+
"asset": "0x2::sui::SUI",
|
|
115
|
+
"amount": "1000000",
|
|
116
|
+
"payTo": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
|
117
|
+
},
|
|
118
|
+
"expected": {
|
|
119
|
+
"s402Version": "1",
|
|
120
|
+
"accepts": [
|
|
121
|
+
"exact"
|
|
122
|
+
],
|
|
123
|
+
"network": "sui:mainnet",
|
|
124
|
+
"asset": "0x2::sui::SUI",
|
|
125
|
+
"amount": "1000000",
|
|
126
|
+
"payTo": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
|
127
|
+
},
|
|
128
|
+
"shouldReject": false
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"description": "x402 V1 with extra unknown fields (stripped)",
|
|
132
|
+
"input": {
|
|
133
|
+
"x402Version": 1,
|
|
134
|
+
"scheme": "exact",
|
|
135
|
+
"network": "sui:mainnet",
|
|
136
|
+
"asset": "0x2::sui::SUI",
|
|
137
|
+
"amount": "1000000",
|
|
138
|
+
"payTo": "0xrecipient789",
|
|
139
|
+
"unknownField": "should be stripped",
|
|
140
|
+
"anotherRandom": 42
|
|
141
|
+
},
|
|
142
|
+
"expected": {
|
|
143
|
+
"s402Version": "1",
|
|
144
|
+
"accepts": [
|
|
145
|
+
"exact"
|
|
146
|
+
],
|
|
147
|
+
"network": "sui:mainnet",
|
|
148
|
+
"asset": "0x2::sui::SUI",
|
|
149
|
+
"amount": "1000000",
|
|
150
|
+
"payTo": "0xrecipient789"
|
|
151
|
+
},
|
|
152
|
+
"shouldReject": false
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"description": "x402 with facilitatorUrl (SSRF-safe HTTPS)",
|
|
156
|
+
"input": {
|
|
157
|
+
"x402Version": 1,
|
|
158
|
+
"scheme": "exact",
|
|
159
|
+
"network": "sui:mainnet",
|
|
160
|
+
"asset": "0x2::sui::SUI",
|
|
161
|
+
"amount": "1000000",
|
|
162
|
+
"payTo": "0xrecipientUrl",
|
|
163
|
+
"facilitatorUrl": "https://safe-facilitator.example.com/settle"
|
|
164
|
+
},
|
|
165
|
+
"expected": {
|
|
166
|
+
"s402Version": "1",
|
|
167
|
+
"accepts": [
|
|
168
|
+
"exact"
|
|
169
|
+
],
|
|
170
|
+
"network": "sui:mainnet",
|
|
171
|
+
"asset": "0x2::sui::SUI",
|
|
172
|
+
"amount": "1000000",
|
|
173
|
+
"payTo": "0xrecipientUrl",
|
|
174
|
+
"facilitatorUrl": "https://safe-facilitator.example.com/settle"
|
|
175
|
+
},
|
|
176
|
+
"shouldReject": false
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"description": "x402 V1 with both amount and maxAmountRequired (amount wins)",
|
|
180
|
+
"input": {
|
|
181
|
+
"x402Version": 1,
|
|
182
|
+
"scheme": "exact",
|
|
183
|
+
"network": "sui:mainnet",
|
|
184
|
+
"asset": "0x2::sui::SUI",
|
|
185
|
+
"amount": "2000000",
|
|
186
|
+
"maxAmountRequired": "1000000",
|
|
187
|
+
"payTo": "0xrecipientBoth"
|
|
188
|
+
},
|
|
189
|
+
"expected": {
|
|
190
|
+
"s402Version": "1",
|
|
191
|
+
"accepts": [
|
|
192
|
+
"exact"
|
|
193
|
+
],
|
|
194
|
+
"network": "sui:mainnet",
|
|
195
|
+
"asset": "0x2::sui::SUI",
|
|
196
|
+
"amount": "2000000",
|
|
197
|
+
"payTo": "0xrecipientBoth"
|
|
198
|
+
},
|
|
199
|
+
"shouldReject": false
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"description": "x402 V2 envelope with resource metadata",
|
|
203
|
+
"input": {
|
|
204
|
+
"x402Version": 2,
|
|
205
|
+
"accepts": [
|
|
206
|
+
{
|
|
207
|
+
"scheme": "exact",
|
|
208
|
+
"network": "sui:mainnet",
|
|
209
|
+
"asset": "0x2::sui::SUI",
|
|
210
|
+
"amount": "3000000",
|
|
211
|
+
"payTo": "0xrecipientRes"
|
|
212
|
+
}
|
|
213
|
+
],
|
|
214
|
+
"resource": {
|
|
215
|
+
"url": "https://api.example.com/data",
|
|
216
|
+
"mimeType": "application/json"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"expected": {
|
|
220
|
+
"s402Version": "1",
|
|
221
|
+
"accepts": [
|
|
222
|
+
"exact"
|
|
223
|
+
],
|
|
224
|
+
"network": "sui:mainnet",
|
|
225
|
+
"asset": "0x2::sui::SUI",
|
|
226
|
+
"amount": "3000000",
|
|
227
|
+
"payTo": "0xrecipientRes"
|
|
228
|
+
},
|
|
229
|
+
"shouldReject": false
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"description": "x402 V1 with extensions preserved",
|
|
233
|
+
"input": {
|
|
234
|
+
"x402Version": 1,
|
|
235
|
+
"scheme": "exact",
|
|
236
|
+
"network": "sui:mainnet",
|
|
237
|
+
"asset": "0x2::sui::SUI",
|
|
238
|
+
"amount": "1000000",
|
|
239
|
+
"payTo": "0xrecipientExt",
|
|
240
|
+
"extensions": {
|
|
241
|
+
"custom": "data"
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
"expected": {
|
|
245
|
+
"s402Version": "1",
|
|
246
|
+
"accepts": [
|
|
247
|
+
"exact"
|
|
248
|
+
],
|
|
249
|
+
"network": "sui:mainnet",
|
|
250
|
+
"asset": "0x2::sui::SUI",
|
|
251
|
+
"amount": "1000000",
|
|
252
|
+
"payTo": "0xrecipientExt",
|
|
253
|
+
"extensions": {
|
|
254
|
+
"custom": "data"
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
"shouldReject": false
|
|
258
|
+
}
|
|
259
|
+
]
|