@altaga/x402-sui 1.0.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/README.MD +439 -0
- package/dist/core/client.js +1 -0
- package/dist/core/facilitator.js +1 -0
- package/dist/core/http.js +1 -0
- package/dist/core/server-http.js +1 -0
- package/dist/core/server.js +1 -0
- package/dist/sui/exact/client.js +1 -0
- package/dist/sui/exact/facilitator.js +1 -0
- package/dist/sui/exact/server.js +1 -0
- package/package.json +45 -0
package/README.MD
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# @altaga/x402-sui
|
|
2
|
+
|
|
3
|
+
> Modular [x402](https://www.x402.org/) implementation for **Sui Sponsored Transactions** (gas-station style).
|
|
4
|
+
|
|
5
|
+
This package is a small, pluggable reference SDK for the x402 "Payment Required" HTTP
|
|
6
|
+
pattern, targeting the **Sui** blockchain. The design separates concerns into three
|
|
7
|
+
roles — **Client**, **Resource Server**, and **Facilitator** — and exposes an
|
|
8
|
+
HTTP middleware layer so any Express-based API can gate routes behind a 402
|
|
9
|
+
payment challenge with a few lines of glue.
|
|
10
|
+
|
|
11
|
+
The bundled `sui/exact` scheme uses Sui's *sponsored transaction* model: the
|
|
12
|
+
buyer signs the payment intent, the facilitator co-signs as the **gas owner**, and
|
|
13
|
+
the resource server never touches a private key.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Table of Contents
|
|
18
|
+
|
|
19
|
+
- [Architecture](#architecture)
|
|
20
|
+
- [Installation](#installation)
|
|
21
|
+
- [Project Layout](#project-layout)
|
|
22
|
+
- [Protocol Flow](#protocol-flow)
|
|
23
|
+
- [Quick Start](#quick-start)
|
|
24
|
+
- [1. Resource Server (the paid API)](#1-resource-server-the-paid-api)
|
|
25
|
+
- [2. Facilitator (the gas station)](#2-facilitator-the-gas-station)
|
|
26
|
+
- [3. Client (the buyer)](#3-client-the-buyer)
|
|
27
|
+
- [Module Reference](#module-reference)
|
|
28
|
+
- [`core/client`](#coreclient)
|
|
29
|
+
- [`core/http`](#corehttp)
|
|
30
|
+
- [`core/server`](#coreserver)
|
|
31
|
+
- [`core/server-http`](#coreserver-http)
|
|
32
|
+
- [`core/facilitator`](#corefacilitator)
|
|
33
|
+
- [`sui/exact/client`](#suiexactclient)
|
|
34
|
+
- [`sui/exact/server`](#suiexactserver)
|
|
35
|
+
- [`sui/exact/facilitator`](#suiexactfacilitator)
|
|
36
|
+
- [Building](#building)
|
|
37
|
+
- [Peer Dependencies](#peer-dependencies)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Architecture
|
|
43
|
+
|
|
44
|
+
x402 is a request/response convention layered on top of HTTP 402 *Payment Required*:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
┌─────────┐ ┌──────────────────┐ ┌──────────────┐
|
|
48
|
+
│ Client │ ──GET──▶│ Resource Server │ │ Facilitator │
|
|
49
|
+
│ │ │ (Express route) │ │ (gas owner) │
|
|
50
|
+
│ │◀─ 402 ──│ │ │ │
|
|
51
|
+
│ │ + x-accepts │ │ │
|
|
52
|
+
│ │ + x-facilitator-url │ │ │
|
|
53
|
+
│ │ │ │ │
|
|
54
|
+
│ │── POST /sponsor ────────────────────▶ │
|
|
55
|
+
│ │◀── sponsored tx bytes ───────────────── │
|
|
56
|
+
│ │── GET + X-PAYMENT ──▶│ │ │
|
|
57
|
+
│ │ │── /verify ──────▶│ │
|
|
58
|
+
│ │ │── /settle ─────▶│ │
|
|
59
|
+
│ │◀── 200 + x-payment-response ────────────│ │
|
|
60
|
+
└─────────┘ └──────────────────┘ └──────────────┘
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The SDK mirrors that split into three independent core modules, each of which
|
|
64
|
+
is scheme-agnostic. Scheme implementations (currently just `sui/exact`) are
|
|
65
|
+
plugged in with a simple `register(schemeName:network, impl)` call — wildcards
|
|
66
|
+
(`sui:*`, `*:*`) are supported.
|
|
67
|
+
|
|
68
|
+
| Role | Module | Responsibility |
|
|
69
|
+
|------|--------|----------------|
|
|
70
|
+
| Client | `core/client`, `core/http` | Parse a 402 challenge, pick a supported scheme, ask the facilitator to sponsor, sign the tx, replay the request with `X-PAYMENT`. |
|
|
71
|
+
| Resource Server | `core/server`, `core/server-http` | Issue 402 challenges for protected routes; verify and settle payments via the facilitator. |
|
|
72
|
+
| Facilitator | `core/facilitator` | Co-sign transactions as gas owner, dry-run to verify, and submit to the chain. |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install @altaga/x402-sui
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
You also need the peer dependencies in your project:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install @mysten/sui @mysten/bcs express
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> Pre-built bundles live in [`dist/`](./dist). The build step is only required
|
|
89
|
+
> if you're modifying the source under `core/` or `sui/`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Project Layout
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
sdk/
|
|
97
|
+
├── core/ # Scheme-agnostic framework
|
|
98
|
+
│ ├── client.mjs # x402Client — scheme registry & lookup
|
|
99
|
+
│ ├── http.mjs # x402HTTPClient — parse 402, encode X-PAYMENT
|
|
100
|
+
│ ├── server.mjs # x402ResourceServer — verify+settle via facilitator
|
|
101
|
+
│ ├── server-http.mjs # x402HTTPResourceServer — Express middleware
|
|
102
|
+
│ └── facilitator.mjs # x402Facilitator — sponsor / verify / settle
|
|
103
|
+
├── sui/
|
|
104
|
+
│ └── exact/ # The "exact" payment scheme on Sui
|
|
105
|
+
│ ├── client.mjs # Builds the payment PTB, signs, asks for sponsor
|
|
106
|
+
│ ├── server.mjs # Passthrough (work happens in facilitator)
|
|
107
|
+
│ └── facilitator.mjs # Co-signs as gas owner, dry-runs, submits
|
|
108
|
+
├── dist/ # esbuild output (consumed via package exports)
|
|
109
|
+
├── package.json
|
|
110
|
+
└── README.MD
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`package.json` exposes each piece as a subpath export so consumers can import
|
|
114
|
+
only what they need:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"@altaga/x402-sui/core/client": "…",
|
|
119
|
+
"@altaga/x402-sui/core/http": "…",
|
|
120
|
+
"@altaga/x402-sui/core/server": "…",
|
|
121
|
+
"@altaga/x402-sui/core/server-http": "…",
|
|
122
|
+
"@altaga/x402-sui/core/facilitator": "…",
|
|
123
|
+
"@altaga/x402-sui/sui/exact/client": "…",
|
|
124
|
+
"@altaga/x402-sui/sui/exact/server": "…",
|
|
125
|
+
"@altaga/x402-sui/sui/exact/facilitator":"…"
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Protocol Flow
|
|
132
|
+
|
|
133
|
+
1. **Challenge.** The client requests a protected route. The server replies
|
|
134
|
+
`402 Payment Required` with:
|
|
135
|
+
- `X-Accepts`: base64-encoded JSON array of accepted `{ scheme, network, asset, payTo, maxAmountRequired, ... }` payment requirements.
|
|
136
|
+
- `X-Facilitator-Url`: where the client should ask for gas sponsorship.
|
|
137
|
+
|
|
138
|
+
2. **Sponsor.** The client picks a supported scheme from `X-Accepts`, builds
|
|
139
|
+
the payment transaction (a `splitCoins` + `transferObjects` PTB on Sui),
|
|
140
|
+
and POSTs the raw tx bytes + sender address to `{facilitatorUrl}/sponsor`.
|
|
141
|
+
The facilitator returns `sponsoredTxBytes` (with itself as `gasOwner`) and
|
|
142
|
+
its `sponsorSignature`.
|
|
143
|
+
|
|
144
|
+
3. **Sign.** The client signs the sponsored bytes with its own keypair.
|
|
145
|
+
|
|
146
|
+
4. **Pay.** The client replays the original request with `X-PAYMENT:`
|
|
147
|
+
(base64 JSON of `{ scheme, network, transaction, signature, amount, payTo, asset }`).
|
|
148
|
+
|
|
149
|
+
5. **Verify & Settle.** The server forwards the payload to the facilitator:
|
|
150
|
+
- `/verify` performs a `dryRunTransactionBlock` to mathematically prove the
|
|
151
|
+
tx would succeed.
|
|
152
|
+
- `/settle` calls `executeTransactionBlock` with the combined
|
|
153
|
+
`[buyerSignature, sponsorSignature]` array and returns the digest.
|
|
154
|
+
|
|
155
|
+
6. **Respond.** On success, the server attaches `X-Payment-Response` (base64
|
|
156
|
+
JSON containing the `transactionDigest`) and hands off to the actual route
|
|
157
|
+
handler.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Quick Start
|
|
162
|
+
|
|
163
|
+
### 1. Resource Server (the paid API)
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
import express from "express";
|
|
167
|
+
import { SuiClient } from "@mysten/sui/client";
|
|
168
|
+
|
|
169
|
+
import { x402ResourceServer } from "@altaga/x402-sui/core/server";
|
|
170
|
+
import { HTTPFacilitatorClient } from "@altaga/x402-sui/core/http";
|
|
171
|
+
import { x402HTTPResourceServer } from "@altaga/x402-sui/core/server-http";
|
|
172
|
+
import { ExactSuiServerScheme } from "@altaga/x402-sui/sui/exact/server";
|
|
173
|
+
|
|
174
|
+
const app = express();
|
|
175
|
+
|
|
176
|
+
// 1. The server is configured with the URL of a trusted facilitator
|
|
177
|
+
const facilitator = new HTTPFacilitatorClient({ url: "https://facilitator.example.com" });
|
|
178
|
+
const resourceServer = new x402ResourceServer(facilitator);
|
|
179
|
+
|
|
180
|
+
// 2. Register scheme(s) you want to accept. Wildcards ("sui:*", "*:*") are OK.
|
|
181
|
+
resourceServer.register("exact:sui:mainnet", new ExactSuiServerScheme());
|
|
182
|
+
|
|
183
|
+
// 3. Declare which routes are paid, and what they cost
|
|
184
|
+
const routes = {
|
|
185
|
+
"GET /premium": {
|
|
186
|
+
accepts: {
|
|
187
|
+
scheme: "exact",
|
|
188
|
+
network: "sui:mainnet",
|
|
189
|
+
asset: "0x2::sui::SUI",
|
|
190
|
+
payTo: "0xYOUR_RECEIVING_ADDRESS",
|
|
191
|
+
maxAmountRequired: "1000000", // 0.001 SUI
|
|
192
|
+
description: "Premium data feed"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
app.use(new x402HTTPResourceServer(resourceServer, routes).middleware());
|
|
198
|
+
|
|
199
|
+
// 4. The actual handler — req.payment is populated by the middleware
|
|
200
|
+
app.get("/premium", (req, res) => {
|
|
201
|
+
res.json({ ok: true, paid: req.payment });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
app.listen(3000);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### 2. Facilitator (the gas station)
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
import express from "express";
|
|
211
|
+
import { SuiClient } from "@mysten/sui/client";
|
|
212
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
213
|
+
|
|
214
|
+
import { x402Facilitator } from "@altaga/x402-sui/core/facilitator";
|
|
215
|
+
import { ExactSuiFacilitatorScheme } from "@altaga/x402-sui/sui/exact/facilitator";
|
|
216
|
+
|
|
217
|
+
const client = new SuiClient({ url: "https://fullnode.mainnet.sui.io" });
|
|
218
|
+
const keypair = Ed25519Keypair.fromSecretKey(process.env.FACILITATOR_SECRET);
|
|
219
|
+
|
|
220
|
+
const facilitator = new x402Facilitator();
|
|
221
|
+
facilitator.register("exact:sui:mainnet", new ExactSuiFacilitatorScheme(client, keypair));
|
|
222
|
+
|
|
223
|
+
const app = express();
|
|
224
|
+
app.use(express.json());
|
|
225
|
+
|
|
226
|
+
// Sponsor: client posts { txBytes, sender, scheme, network }
|
|
227
|
+
app.post("/sponsor", async (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
const result = await facilitator.sponsor(req.body.scheme, req.body.network, {
|
|
230
|
+
txBytes: req.body.txBytes,
|
|
231
|
+
sender: req.body.sender,
|
|
232
|
+
});
|
|
233
|
+
res.json(result); // { sponsoredTxBytes, sponsorSignature }
|
|
234
|
+
} catch (e) {
|
|
235
|
+
res.status(400).json({ error: e.message });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Verify: dry-run the candidate transaction
|
|
240
|
+
app.post("/verify", async (req, res) => {
|
|
241
|
+
try {
|
|
242
|
+
const result = await facilitator.verify(req.body.paymentPayload, req.body.paymentRequirement);
|
|
243
|
+
res.json(result); // { isValid: true }
|
|
244
|
+
} catch (e) {
|
|
245
|
+
res.status(400).json({ isValid: false, error: e.message });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Settle: submit the transaction to the chain
|
|
250
|
+
app.post("/settle", async (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const result = await facilitator.settle(req.body.paymentPayload, req.body.paymentRequirement);
|
|
253
|
+
res.json(result); // { transactionDigest }
|
|
254
|
+
} catch (e) {
|
|
255
|
+
res.status(400).json({ error: e.message });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
app.listen: 4000;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
> The facilitator keypair must hold SUI for gas. If `client.getCoins` returns
|
|
263
|
+
> zero gas coins, sponsorship fails with `Facilitator out of gas!`.
|
|
264
|
+
|
|
265
|
+
### 3. Client (the buyer)
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
import { SuiClient } from "@mysten/sui/client";
|
|
269
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
270
|
+
|
|
271
|
+
import { x402Client } from "@altaga/x402-sui/core/client";
|
|
272
|
+
import { x402HTTPClient } from "@altaga/x402-sui/core/http";
|
|
273
|
+
import { ExactSuiClientScheme } from "@altaga/x402-sui/sui/exact/client";
|
|
274
|
+
|
|
275
|
+
const sui = new SuiClient({ url: "https://fullnode.mainnet.sui.io" });
|
|
276
|
+
const keypair = Ed25519Keypair.fromSecretKey(process.env.BUYER_SECRET);
|
|
277
|
+
|
|
278
|
+
const core = new x402Client();
|
|
279
|
+
core.register("exact:sui:mainnet", new ExactSuiClientScheme(sui, keypair));
|
|
280
|
+
|
|
281
|
+
const http = new x402HTTPClient(core);
|
|
282
|
+
|
|
283
|
+
// 1. Hit the protected endpoint
|
|
284
|
+
let res = await fetch("https://api.example.com/premium");
|
|
285
|
+
if (res.status === 402) {
|
|
286
|
+
// 2. Parse the challenge
|
|
287
|
+
const paymentRequired = http.getPaymentRequiredResponse(
|
|
288
|
+
(name) => res.headers.get(name),
|
|
289
|
+
null,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// 3. Build + sign + sponsor the payment
|
|
293
|
+
const payload = await http.createPaymentPayload(paymentRequired);
|
|
294
|
+
|
|
295
|
+
// 4. Replay the request with X-PAYMENT
|
|
296
|
+
res = await fetch("https://api.example.com/premium", {
|
|
297
|
+
headers: http.encodePaymentSignatureHeader(payload),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const settleInfo = http.getPaymentSettleResponse((n) => res.headers.get(n));
|
|
302
|
+
console.log("data:", await res.json(), "settle:", settleInfo);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Module Reference
|
|
308
|
+
|
|
309
|
+
### `core/client`
|
|
310
|
+
|
|
311
|
+
`x402Client` — a tiny scheme registry. `register(pattern, impl)` accepts
|
|
312
|
+
`scheme:network`, `scheme:*`, or `*:*`; `getScheme(scheme, network)` resolves
|
|
313
|
+
with the most specific match. Throws if nothing is registered.
|
|
314
|
+
|
|
315
|
+
### `core/http`
|
|
316
|
+
|
|
317
|
+
`x402HTTPClient(coreClient)` — HTTP-specific glue on top of a core client:
|
|
318
|
+
|
|
319
|
+
- `getPaymentRequiredResponse(getHeaderFn, bodyJson)` — decodes `X-Accepts`
|
|
320
|
+
(base64 JSON) and reads `X-Facilitator-Url` (with body fallback).
|
|
321
|
+
- `createPaymentPayload(paymentRequired)` — iterates `accepts`, hands the
|
|
322
|
+
first one a registered scheme can fulfil, and returns a normalized
|
|
323
|
+
`{ scheme, network, transaction, signature, amount, payTo, asset }`.
|
|
324
|
+
- `encodePaymentSignatureHeader(payload)` — base64-encodes the payload for
|
|
325
|
+
the `X-PAYMENT` request header.
|
|
326
|
+
- `getPaymentSettleResponse(getHeaderFn)` — decodes `X-Payment-Response` if
|
|
327
|
+
present.
|
|
328
|
+
|
|
329
|
+
### `core/server`
|
|
330
|
+
|
|
331
|
+
`x402ResourceServer(facilitatorClient)` — the server-side brain.
|
|
332
|
+
|
|
333
|
+
- `register(pattern, impl)` — same lookup rules as the client.
|
|
334
|
+
- `initialize()` — async hook for parity with richer implementations.
|
|
335
|
+
- `handlePayment(paymentPayload, routes)` — calls
|
|
336
|
+
`facilitatorClient.verify(...)` then `facilitatorClient.settle(...)` and
|
|
337
|
+
returns the settlement result.
|
|
338
|
+
|
|
339
|
+
`HTTPFacilitatorClient({ url })` — the default transport, POSTing JSON to
|
|
340
|
+
`{url}/verify` and `{url}/settle`. Roll your own if you need mTLS or auth
|
|
341
|
+
headers.
|
|
342
|
+
|
|
343
|
+
### `core/server-http`
|
|
344
|
+
|
|
345
|
+
`x402HTTPResourceServer(resourceServer, routesConfig).middleware()` returns
|
|
346
|
+
an Express middleware that:
|
|
347
|
+
|
|
348
|
+
1. Looks up the route via `${req.method} ${req.path}`.
|
|
349
|
+
2. If no `X-PAYMENT` header is present, responds `402` with
|
|
350
|
+
`X-Accepts` + `X-Facilitator-Url`.
|
|
351
|
+
3. If a header is present, base64-decodes it, calls
|
|
352
|
+
`resourceServer.handlePayment(...)`, attaches `X-Payment-Response` to the
|
|
353
|
+
outgoing response, populates `req.payment`, and calls `next()`.
|
|
354
|
+
4. On error, responds `400` with the error message.
|
|
355
|
+
|
|
356
|
+
### `core/facilitator`
|
|
357
|
+
|
|
358
|
+
`x402Facilitator` — scheme registry + dispatch for the three facilitator
|
|
359
|
+
operations:
|
|
360
|
+
|
|
361
|
+
- `sponsor(schemeName, network, payload)` — typically turns a raw client PTB
|
|
362
|
+
into a sponsored transaction with the facilitator as `gasOwner`.
|
|
363
|
+
- `verify(paymentPayload, paymentRequirements)` — must return
|
|
364
|
+
`{ isValid: true }` or throw.
|
|
365
|
+
- `settle(paymentPayload, paymentRequirements)` — must return
|
|
366
|
+
`{ transactionDigest }` or throw.
|
|
367
|
+
|
|
368
|
+
### `sui/exact/client`
|
|
369
|
+
|
|
370
|
+
`ExactSuiClientScheme(client, keypair)` — implements `createPaymentPayload`:
|
|
371
|
+
|
|
372
|
+
1. Fetchs the buyer's coins of `requirement.asset`; aborts if none exist.
|
|
373
|
+
2. Builds a PTB: `splitCoins(coin, [amount])` → `transferObjects([split], payTo)`.
|
|
374
|
+
3. If a `facilitatorUrl` was advertised, POSTs the raw bytes to
|
|
375
|
+
`{facilitatorUrl}/sponsor` and receives `sponsoredTxBytes` +
|
|
376
|
+
`sponsorSignature`.
|
|
377
|
+
4. Signs the (possibly sponsored) bytes with the buyer's keypair and returns
|
|
378
|
+
`{ transaction, signature: [buyer, sponsor?] }`.
|
|
379
|
+
|
|
380
|
+
### `sui/exact/server`
|
|
381
|
+
|
|
382
|
+
`ExactSuiServerScheme` — intentionally empty. On Sui, the heavy lifting
|
|
383
|
+
(verification, gas sponsorship, submission) is done by the facilitator, so
|
|
384
|
+
the server-side scheme is a passthrough. Register one for parity:
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
resourceServer.register("exact:sui:mainnet", new ExactSuiServerScheme());
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### `sui/exact/facilitator`
|
|
391
|
+
|
|
392
|
+
`ExactSuiFacilitatorScheme(client, keypair)` — the gas station.
|
|
393
|
+
|
|
394
|
+
- **`sponsor({ txBytes, sender })`** — deserializes the buyer's PTB, overrides
|
|
395
|
+
`setSender(sender)`, `setGasOwner(self)`, picks a gas coin, sets a
|
|
396
|
+
`setGasBudget(10_000_000)` and `setGasPrice(1000)`, calls `tx.build`, and
|
|
397
|
+
returns `{ sponsoredTxBytes, sponsorSignature }`.
|
|
398
|
+
- **`verify(payload, reqs)`** — `dryRunTransactionBlock` on the decoded
|
|
399
|
+
bytes; succeeds only if `effects.status.status === "success"`.
|
|
400
|
+
- **`settle(payload, reqs)`** — `executeTransactionBlock` with the combined
|
|
401
|
+
signature array; returns `{ transactionDigest }` on success.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Building
|
|
406
|
+
|
|
407
|
+
Source modules are written as `.mjs`. To rebuild the `dist/` bundles:
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
npm run build
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Which is shorthand for:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
esbuild core/*.mjs sui/exact/*.mjs \
|
|
417
|
+
--outdir=dist \
|
|
418
|
+
--minify \
|
|
419
|
+
--format=esm \
|
|
420
|
+
--target=es2022
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Peer Dependencies
|
|
426
|
+
|
|
427
|
+
| Package | Version | Why |
|
|
428
|
+
|---------|---------|-----|
|
|
429
|
+
| [`@mysten/sui`](https://www.npmjs.com/package/@mysten/sui) | `^1.14.0` | `Transaction`, `SuiClient`, keypairs |
|
|
430
|
+
| [`@mysten/bcs`](https://www.npmjs.com/package/@mysten/bcs) | `^1.2.0` | `toBase64` for serializing built PTBs |
|
|
431
|
+
| [`express`](https://www.npmjs.com/package/express) | `^4.21.2` | Used by `x402HTTPResourceServer.middleware()` |
|
|
432
|
+
|
|
433
|
+
`esbuild` is only needed if you re-run the build.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
[MIT](./LICENSE) — © Altaga
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class i{constructor(){this.schemes=new Map}register(e,s){return this.schemes.set(e,s),this}getScheme(e,s){const t=`${e}:${s}`;if(this.schemes.has(t))return this.schemes.get(t);const h=`${e}:*`;if(this.schemes.has(h))return this.schemes.get(h);if(this.schemes.has("*:*"))return this.schemes.get("*:*");throw new Error(`No payment scheme registered for ${t}`)}}export{i as x402Client};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class c{constructor(){this.schemes=new Map}register(e,s){return this.schemes.set(e,s),this}getScheme(e,s){const t=`${e}:${s}`;if(this.schemes.has(t))return this.schemes.get(t);if(this.schemes.has(`${e}:*`))return this.schemes.get(`${e}:*`);if(this.schemes.has("*:*"))return this.schemes.get("*:*");throw new Error(`No facilitator scheme registered for ${t}`)}async sponsor(e,s,t){return await this.getScheme(e,s).sponsor(t)}async verify(e,s){return await this.getScheme(e.scheme,e.network).verify(e,s)}async settle(e,s){return await this.getScheme(e.scheme,e.network).settle(e,s)}}export{c as x402Facilitator};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class s{constructor(t){this.coreClient=t}getPaymentRequiredResponse(t,e){const r=t("x-accepts"),n=t("x-facilitator-url");if(!r)throw new Error("Missing x-accepts header in 402 response");return{accepts:JSON.parse(Buffer.from(r,"base64").toString("utf-8")),facilitatorUrl:n||e?.facilitatorUrl}}async createPaymentPayload(t){for(const e of t.accepts)try{const n=await this.coreClient.getScheme(e.scheme,e.network).createPaymentPayload(e,t.facilitatorUrl);return{scheme:e.scheme,network:e.network,transaction:n.transaction,signature:n.signature,amount:e.maxAmountRequired||e.amount,payTo:e.payTo,asset:e.asset}}catch(r){console.warn(`Could not fulfill payment requirement for ${e.scheme}:${e.network}:`,r.message);continue}throw new Error("Could not fulfill any payment requirements provided by the server.")}encodePaymentSignatureHeader(t){const e=JSON.stringify(t);return{"X-PAYMENT":Buffer.from(e).toString("base64")}}getPaymentSettleResponse(t){const e=t("x-payment-response");return e?JSON.parse(Buffer.from(e,"base64").toString("utf-8")):null}}export{s as x402HTTPClient};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class u{constructor(t,e){this.resourceServer=t,this.routesConfig=e}middleware(){return async(t,e,a)=>{const i=`${t.method} ${t.path}`,r=this.routesConfig[i];if(!r)return a();const o=t.headers["x-payment"];if(!o)return e.status(402),e.setHeader("x-accepts",Buffer.from(JSON.stringify([r.accepts])).toString("base64")),this.resourceServer.facilitatorClient&&e.setHeader("x-facilitator-url",this.resourceServer.facilitatorClient.url),e.setHeader("Content-Type","application/json"),e.json({error:"Payment Required",accepts:[r.accepts]});try{const s=JSON.parse(Buffer.from(o,"base64").toString("utf-8")),n=await this.resourceServer.handlePayment(s,[r.accepts]);e.setHeader("x-payment-response",Buffer.from(JSON.stringify(n)).toString("base64")),t.payment=n,a()}catch(s){console.error("Payment settlement error:",s),e.status(400).json({error:"Payment Processing Failed",message:s.message})}}}}export{u as x402HTTPResourceServer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class a{constructor(t){this.url=t.url}async verify(t,e){const i=await fetch(`${this.url}/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({paymentPayload:t,paymentRequirement:e})});if(!i.ok)throw new Error(`Facilitator verify failed: ${await i.text()}`);return await i.json()}async settle(t,e){const i=await fetch(`${this.url}/settle`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({paymentPayload:t,paymentRequirement:e})});if(!i.ok)throw new Error(`Facilitator settle failed: ${await i.text()}`);return await i.json()}}class n{constructor(t){this.facilitatorClient=t,this.schemes=new Map}register(t,e){return this.schemes.set(t,e),this}async initialize(){console.log("x402ResourceServer initialized.")}async handlePayment(t,e){if(!(await this.facilitatorClient.verify(t,e)).isValid)throw new Error("Payment verification failed via Facilitator");return await this.facilitatorClient.settle(t,e)}}export{a as HTTPFacilitatorClient,n as x402ResourceServer};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Transaction as l}from"@mysten/sui/transactions";import"@mysten/bcs";class S{constructor(s,t){this.client=s,this.keypair=t,this.address=t.getPublicKey().toSuiAddress()}async createPaymentPayload(s,t){const o=new l,i=await this.client.getCoins({owner:this.address,coinType:s.asset});if(i.data.length===0)throw new Error(`Buyer has no ${s.asset}!`);const d=i.data[0],h=s.maxAmountRequired||s.amount||s.price,[y]=o.splitCoins(d.coinObjectId,[h]);o.transferObjects([y],s.payTo);const r=o.serialize();let n=r,e=null;if(t){const w=t.endsWith("/")?`${t}sponsor`:`${t}/sponsor`,a=await fetch(w,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({txBytes:r,sender:this.address,scheme:s.scheme,network:s.network})});if(!a.ok)throw new Error(`Sponsor failed: ${await a.text()}`);const p=await a.json();n=p.sponsoredTxBytes,e=p.sponsorSignature}const{signature:c}=await this.keypair.signTransaction(Buffer.from(n,"base64"));return{transaction:n,signature:e?[c,e]:[c]}}}export{S as ExactSuiClientScheme};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Transaction as o}from"@mysten/sui/transactions";import{toBase64 as c}from"@mysten/bcs";class l{constructor(s,e){this.client=s,this.keypair=e,this.address=e.getPublicKey().toSuiAddress()}async sponsor(s){const{txBytes:e,sender:r}=s,t=o.from(e);t.setSender(r),t.setGasOwner(this.address);const n=await this.client.getCoins({owner:this.address,coinType:"0x2::sui::SUI"});if(n.data.length===0)throw new Error("Facilitator out of gas!");t.setGasPayment([{objectId:n.data[0].coinObjectId,version:n.data[0].version,digest:n.data[0].digest}]),t.setGasBudget(1e7),t.setGasPrice(1e3);const a=await t.build({client:this.client}),{signature:i}=await this.keypair.signTransaction(a);return{sponsoredTxBytes:c(a),sponsorSignature:i}}async verify(s,e){const r=Buffer.from(s.transaction,"base64");try{const t=await this.client.dryRunTransactionBlock({transactionBlock:r});if(t.effects.status.status!=="success")throw new Error("Dry run failed: "+t.effects.status.error);return{isValid:!0}}catch(t){throw new Error(`Payment verification failed: ${t.message}`)}}async settle(s,e){const r=Buffer.from(s.transaction,"base64");try{const t=await this.client.executeTransactionBlock({transactionBlock:r,signature:s.signature,options:{showEffects:!0}});if(t.effects?.status.status!=="success")throw new Error(t.effects?.status.error||"Unknown error");return{transactionDigest:t.digest}}catch(t){throw new Error(`Payment settlement error: ${t.message}`)}}}export{l as ExactSuiFacilitatorScheme};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class c{}export{c as ExactSuiServerScheme};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@altaga/x402-sui",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Modular x402 implementation for Sui Sponsored Transactions",
|
|
5
|
+
"main": "dist/core/client.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "esbuild core/*.mjs sui/exact/*.mjs --outdir=dist --minify --format=esm --target=es2022",
|
|
15
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
"./core/client": "./dist/core/client.js",
|
|
19
|
+
"./core/http": "./dist/core/http.js",
|
|
20
|
+
"./core/server": "./dist/core/server.js",
|
|
21
|
+
"./core/server-http": "./dist/core/server-http.js",
|
|
22
|
+
"./core/facilitator": "./dist/core/facilitator.js",
|
|
23
|
+
"./sui/exact/client": "./dist/sui/exact/client.js",
|
|
24
|
+
"./sui/exact/server": "./dist/sui/exact/server.js",
|
|
25
|
+
"./sui/exact/facilitator": "./dist/sui/exact/facilitator.js"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"sui",
|
|
29
|
+
"x402",
|
|
30
|
+
"sponsored",
|
|
31
|
+
"transactions",
|
|
32
|
+
"crypto",
|
|
33
|
+
"payments"
|
|
34
|
+
],
|
|
35
|
+
"author": "Altaga",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@mysten/bcs": "^1.2.0",
|
|
39
|
+
"@mysten/sui": "^1.14.0",
|
|
40
|
+
"express": "^4.21.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"esbuild": "^0.28.0"
|
|
44
|
+
}
|
|
45
|
+
}
|