@nickthelegend69/fund402 0.1.0
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/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/adapters/express.d.ts +2 -0
- package/dist/adapters/express.js +32 -0
- package/dist/adapters/hono.d.ts +2 -0
- package/dist/adapters/hono.js +21 -0
- package/dist/adapters/next.d.ts +4 -0
- package/dist/adapters/next.js +37 -0
- package/dist/casper.d.ts +116 -0
- package/dist/casper.js +212 -0
- package/dist/client.d.ts +59 -0
- package/dist/client.js +179 -0
- package/dist/eip712.d.ts +19 -0
- package/dist/eip712.js +77 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +68 -0
- package/dist/server.d.ts +112 -0
- package/dist/server.js +251 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.js +4 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nickthelegend
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# @nickthelegend69/fund402
|
|
2
|
+
|
|
3
|
+
**Create x402-gated HTTP endpoints that are settled by a lending pool — on Casper.**
|
|
4
|
+
|
|
5
|
+
A normal [x402](https://x402.org) paywall makes the *caller* pay from their own
|
|
6
|
+
balance. Fund402 adds one twist that changes everything for AI agents:
|
|
7
|
+
|
|
8
|
+
> the caller borrows **just-in-time** from the Fund402 vault, the **lending pool
|
|
9
|
+
> fronts the CEP-18 payment to you (the merchant)**, and the agent repays later.
|
|
10
|
+
|
|
11
|
+
To you it looks like any x402 endpoint — except your callers can pay **even with an
|
|
12
|
+
empty wallet**, because the pool settles on their behalf. One SDK, both sides:
|
|
13
|
+
|
|
14
|
+
| | |
|
|
15
|
+
|---|---|
|
|
16
|
+
| 🟢 **Server (merchant)** | `paywall()` + drop-in middleware for **Express / Hono / Next.js** — issue the 402 challenge, verify the pool settled on-chain, serve the resource. |
|
|
17
|
+
| 🔵 **Client (agent)** | `fund402Fetch()` — a drop-in `fetch` that pays any Fund402 endpoint with JIT pool credit and replays the request. |
|
|
18
|
+
|
|
19
|
+
Casper-native: CEP-18 token + EIP-712 `exact` scheme over the `casper:*` network
|
|
20
|
+
family, verified against the live **CSPR.cloud x402 facilitator** and the deployed
|
|
21
|
+
**Fund402 vault** on testnet.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌────────────┐ 1. GET (no payment) ┌─────────────────────┐
|
|
25
|
+
│ │ ───────────────────────────▶ │ your API │
|
|
26
|
+
│ agent │ 2. 402 + x402 challenge │ paywall(...) │
|
|
27
|
+
│ fund402- │ ◀─────────────────────────── │ (Express/Hono/Next)│
|
|
28
|
+
│ Fetch │ └─────────┬───────────┘
|
|
29
|
+
│ │ 3. borrow_and_pay ┌─────────────────┐ │ 5. verify settlement
|
|
30
|
+
│ │ ──────────────────▶ │ Fund402 vault │ │ on-chain (CSPR.cloud)
|
|
31
|
+
│ │ (pool fronts $) │ (lending pool) │◀┘
|
|
32
|
+
│ │ 4. retry + payment-signature ▲
|
|
33
|
+
│ │ ───────────────────────────▶ pays merchant from pool
|
|
34
|
+
└────────────┘ 6. 200 + resource
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm i @nickthelegend69/fund402
|
|
41
|
+
# the agent/client side also needs a Casper key; axios is optional (fetch is built in)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Server — create an endpoint settled by the pool
|
|
47
|
+
|
|
48
|
+
### Express
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import express from "express";
|
|
52
|
+
import { expressPaywall } from "@nickthelegend69/fund402/express";
|
|
53
|
+
|
|
54
|
+
const app = express();
|
|
55
|
+
|
|
56
|
+
app.use("/v", expressPaywall({
|
|
57
|
+
payTo: "00" + MERCHANT_ACCOUNT_HASH, // who gets paid (tagged account hash)
|
|
58
|
+
asset: CEP18_PACKAGE_HASH, // the CEP-18 settlement token
|
|
59
|
+
price: "1000000", // base units per call (0.001 @ 9 decimals)
|
|
60
|
+
vaultContract: VAULT_PACKAGE_HASH, // the lending pool that settles
|
|
61
|
+
csprCloudApiKey: process.env.CSPR_CLOUD_API_KEY,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
app.get("/v/price/:pair", (req, res) => {
|
|
65
|
+
// only runs once the pool settled on-chain; proof is on req.fund402
|
|
66
|
+
res.json({ pair: req.params.pair, price: 64250.12, settledBy: req.fund402.deployHash });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
app.listen(3000);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Next.js (App Router)
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// app/api/v/[...path]/route.ts
|
|
76
|
+
import { withPaywall } from "@nickthelegend69/fund402/next";
|
|
77
|
+
|
|
78
|
+
export const GET = withPaywall(
|
|
79
|
+
{ payTo, asset, price: "1000000", vaultContract, csprCloudApiKey: process.env.CSPR_CLOUD_API_KEY },
|
|
80
|
+
async () => Response.json({ data: "the protected resource" })
|
|
81
|
+
);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Hono (Node, Bun, Workers, Deno)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { Hono } from "hono";
|
|
88
|
+
import { honoPaywall } from "@nickthelegend69/fund402/hono";
|
|
89
|
+
|
|
90
|
+
const app = new Hono();
|
|
91
|
+
app.use("/v/*", honoPaywall({ payTo, asset, price: "1000000", vaultContract, csprCloudApiKey }));
|
|
92
|
+
app.get("/v/price", (c) => c.json({ price: 64250.12 }));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Any framework
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { paywall } from "@nickthelegend69/fund402";
|
|
99
|
+
|
|
100
|
+
const pay = paywall({ payTo, asset, price: "1000000", vaultContract, csprCloudApiKey });
|
|
101
|
+
|
|
102
|
+
const g = await pay.guard({ method: req.method, url: fullUrl, headers: req.headers });
|
|
103
|
+
if (!g.paid) return send(g.response); // 402 challenge / 400 / error
|
|
104
|
+
res.setHeader("payment-response", g.paymentResponseHeader);
|
|
105
|
+
return serveTheResource(); // g.settlement has the on-chain proof
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Client — pay an endpoint with JIT pool credit
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { fund402Fetch } from "@nickthelegend69/fund402";
|
|
114
|
+
|
|
115
|
+
const f = fund402Fetch({
|
|
116
|
+
agentSecretKey, // PEM or hex
|
|
117
|
+
agentPublicKey, // 01.. / 02.. account-key hex
|
|
118
|
+
vaultContract: VAULT_PACKAGE_HASH,
|
|
119
|
+
network: "casper:casper-test",
|
|
120
|
+
onEvent: (e) => console.log(e.type, e.data), // intercepted_402 → borrowing → … → payment_confirmed
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// transparent: on 402, it borrows from the pool, settles on-chain, retries.
|
|
124
|
+
const res = await f("https://merchant.example/v/price/BTC-USD");
|
|
125
|
+
const data = await res.json(); // paid + served — the agent's own balance can be zero
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Prefer Axios? `withPaymentInterceptor(config)` returns an `AxiosInstance` with the
|
|
129
|
+
same behaviour (Axios is an optional peer dependency).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## How settlement works (and why you can trust it)
|
|
134
|
+
|
|
135
|
+
1. The agent calls the vault's `borrow_and_pay(merchant, amount, collateral, vault_id)`.
|
|
136
|
+
The **pool transfers `amount` CEP-18 to the merchant** and books a loan.
|
|
137
|
+
2. The agent attaches the resulting **deploy hash** to a signed x402 `exact` payload
|
|
138
|
+
and replays the request.
|
|
139
|
+
3. The server calls `verifyPoolSettlement()` — it reads the deploy from **CSPR.cloud**
|
|
140
|
+
and confirms it `processed`, paid the right **merchant + amount**, and targeted
|
|
141
|
+
the configured **vault package**. The gateway trusts the chain, not the caller.
|
|
142
|
+
4. Optionally also POSTs the signed authorization to an x402 **facilitator** `/verify`
|
|
143
|
+
(set `facilitatorUrl`) for defense-in-depth.
|
|
144
|
+
|
|
145
|
+
`verifyPoolSettlement` is resilient to indexer lag — it polls a bounded window while
|
|
146
|
+
the deploy is still propagating, and fails fast on an executed on-chain failure.
|
|
147
|
+
|
|
148
|
+
## Live deployment (casper-test)
|
|
149
|
+
|
|
150
|
+
| | |
|
|
151
|
+
|---|---|
|
|
152
|
+
| Vault (lending pool) package | `664d99de146b9b573161a387d89fefc649677351d8a6d2acbe22109bf88f6b12` |
|
|
153
|
+
| CEP-18 asset (Fund402 USDC / F402) | `389cedc529cc553e2639884c9dcc5e6dcbeb3920f7f5ca5a39bf7f7b866bccd0` |
|
|
154
|
+
| Network | `casper:casper-test` |
|
|
155
|
+
| Facilitator | `https://x402-facilitator.cspr.cloud` |
|
|
156
|
+
|
|
157
|
+
These are the defaults the SDK ships with — point `vaultContract` / `asset` at your
|
|
158
|
+
own deployment for production.
|
|
159
|
+
|
|
160
|
+
## Verified live ✅
|
|
161
|
+
|
|
162
|
+
`npm run test:e2e` stands up a real HTTP server with `paywall()`, points a
|
|
163
|
+
`fund402Fetch()` agent at it, and runs the **whole loop on casper-test**: the agent
|
|
164
|
+
hits the endpoint → gets a 402 → borrows from the pool (the vault fronts the F402 to
|
|
165
|
+
the merchant) → retries → the server verifies the settlement on-chain via CSPR.cloud
|
|
166
|
+
→ serves the resource.
|
|
167
|
+
|
|
168
|
+
Last live run — agent `01baa8d8…` (Tier-3, **zero collateral**) borrowed `1000000`
|
|
169
|
+
F402; the pool fronted it to the merchant and the server served the resource after
|
|
170
|
+
verifying it on-chain:
|
|
171
|
+
**[settlement deploy `96f30ddf…` ↗](https://testnet.cspr.live/deploy/96f30ddfac9b3b8bc04a9fe274b1c006aff398ac624e7360669a2c1f3dc28264)** (`status: processed`).
|
|
172
|
+
|
|
173
|
+
## API
|
|
174
|
+
|
|
175
|
+
**Server:** `paywall(cfg)` → `{ challenge, verify, guard }` · `buildPaymentRequirements`
|
|
176
|
+
· `challengeBody` · `decodePaymentSignature` · `verifyPoolSettlement` ·
|
|
177
|
+
`verifyWithFacilitator` · `explorerTx` · adapters `expressPaywall` / `honoPaywall` /
|
|
178
|
+
`withPaywall` (Next).
|
|
179
|
+
|
|
180
|
+
**Client:** `fund402Fetch(cfg)` · `withPaymentInterceptor(cfg)` (Axios) · `payViaPool`
|
|
181
|
+
· `decodeChallenge` · `selectCasperOption` · `testnetClient` / `mainnetClient`.
|
|
182
|
+
|
|
183
|
+
**On-chain primitives:** `borrowAndPayOnChain` · `repayLoanOnChain` ·
|
|
184
|
+
`ensureCollateralAllowance` · `buildExactPayload` · `waitForDeploy` ·
|
|
185
|
+
`agentTaggedAddress` · `transferAuthorizationDigest`.
|
|
186
|
+
|
|
187
|
+
## ⚠️ Security
|
|
188
|
+
|
|
189
|
+
The client side signs real deploys with a local key — **testnet only**, never reuse
|
|
190
|
+
keys on mainnet. Keep keys out of source control. MIT licensed.
|
|
191
|
+
|
|
192
|
+
Part of [Fund402](https://github.com/nickthelegend/fund402-casper) — JIT credit for AI agents on Casper.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Express adapter. `app.use("/v", expressPaywall({...}))` gates everything under
|
|
3
|
+
// it: unpaid requests get the 402 x402 challenge; settled ones fall through to
|
|
4
|
+
// your handler with `req.fund402` set to the on-chain settlement proof.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.expressPaywall = expressPaywall;
|
|
7
|
+
const server_1 = require("../server");
|
|
8
|
+
function fullUrl(req) {
|
|
9
|
+
const proto = req.headers["x-forwarded-proto"] || req.protocol || "http";
|
|
10
|
+
const host = req.headers["x-forwarded-host"] || req.headers["host"] || "localhost";
|
|
11
|
+
return `${proto}://${host}${req.originalUrl ?? req.url ?? ""}`;
|
|
12
|
+
}
|
|
13
|
+
function expressPaywall(config) {
|
|
14
|
+
const pay = "guard" in config ? config : (0, server_1.paywall)(config);
|
|
15
|
+
return async function fund402Middleware(req, res, next) {
|
|
16
|
+
try {
|
|
17
|
+
const g = await pay.guard({ method: req.method, url: fullUrl(req), headers: req.headers });
|
|
18
|
+
if (g.paid) {
|
|
19
|
+
res.setHeader("payment-response", g.paymentResponseHeader);
|
|
20
|
+
req.fund402 = g.settlement;
|
|
21
|
+
return next();
|
|
22
|
+
}
|
|
23
|
+
const r = g.response;
|
|
24
|
+
for (const [k, v] of Object.entries(r.headers))
|
|
25
|
+
res.setHeader(k, v);
|
|
26
|
+
return res.status(r.status).json(r.body);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return res.status(500).json({ error: `fund402 paywall error: ${e?.message ?? e}` });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Hono adapter. `app.use("/v/*", honoPaywall({...}))`. Works on any Hono runtime
|
|
3
|
+
// (Node, Bun, Workers, Deno). Settled requests expose the proof via `c.get("fund402")`.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.honoPaywall = honoPaywall;
|
|
6
|
+
const server_1 = require("../server");
|
|
7
|
+
function honoPaywall(config) {
|
|
8
|
+
const pay = "guard" in config ? config : (0, server_1.paywall)(config);
|
|
9
|
+
return async function fund402Middleware(c, next) {
|
|
10
|
+
const headers = {};
|
|
11
|
+
c.req.raw.headers.forEach((v, k) => (headers[k] = v));
|
|
12
|
+
const g = await pay.guard({ method: c.req.method, url: c.req.url, headers });
|
|
13
|
+
if (g.paid) {
|
|
14
|
+
c.header("payment-response", g.paymentResponseHeader);
|
|
15
|
+
c.set("fund402", g.settlement);
|
|
16
|
+
return next();
|
|
17
|
+
}
|
|
18
|
+
const r = g.response;
|
|
19
|
+
return c.json(r.body, r.status, r.headers);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Next.js App Router adapter. Wrap a route handler so it only runs once payment
|
|
3
|
+
// has settled on-chain; otherwise the wrapper returns the 402 x402 challenge.
|
|
4
|
+
//
|
|
5
|
+
// // app/api/v/[...path]/route.ts
|
|
6
|
+
// import { withPaywall } from "@nickthelegend69/fund402/next";
|
|
7
|
+
// export const GET = withPaywall(
|
|
8
|
+
// { payTo, asset, price: "1000000", vaultContract, csprCloudApiKey: process.env.CSPR_CLOUD_API_KEY },
|
|
9
|
+
// async (req) => Response.json({ data: "the protected resource" })
|
|
10
|
+
// );
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.withPaywall = withPaywall;
|
|
13
|
+
const server_1 = require("../server");
|
|
14
|
+
function withPaywall(config, handler) {
|
|
15
|
+
const pay = "guard" in config ? config : (0, server_1.paywall)(config);
|
|
16
|
+
return async function fund402Route(req, ctx) {
|
|
17
|
+
const url = req?.nextUrl?.href ?? req?.url ?? "";
|
|
18
|
+
const headers = {};
|
|
19
|
+
if (req?.headers?.forEach)
|
|
20
|
+
req.headers.forEach((v, k) => (headers[k] = v));
|
|
21
|
+
const g = await pay.guard({ method: req?.method, url, headers });
|
|
22
|
+
if (!g.paid) {
|
|
23
|
+
return new Response(JSON.stringify(g.response.body), {
|
|
24
|
+
status: g.response.status,
|
|
25
|
+
headers: g.response.headers,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const out = await handler(req, ctx);
|
|
29
|
+
try {
|
|
30
|
+
out.headers.set("payment-response", g.paymentResponseHeader);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* immutable response headers — best effort */
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/casper.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { RpcClient, PrivateKey, KeyAlgorithm } from "casper-js-sdk";
|
|
2
|
+
import type { PaymentRequirements } from "./types";
|
|
3
|
+
export interface CasperWiringConfig {
|
|
4
|
+
nodeUrl: string;
|
|
5
|
+
network: string;
|
|
6
|
+
chainName: string;
|
|
7
|
+
vaultContractHash: string;
|
|
8
|
+
agentSecretKey: string;
|
|
9
|
+
agentPublicKey: string;
|
|
10
|
+
keyAlgorithm?: KeyAlgorithm;
|
|
11
|
+
/** payment (gas) in motes for a borrow_and_pay call. Default 5 CSPR. */
|
|
12
|
+
borrowGasMotes?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function rpc(nodeUrl: string): RpcClient;
|
|
15
|
+
export declare function loadPrivateKey(secret: string, algo?: KeyAlgorithm): Promise<PrivateKey>;
|
|
16
|
+
/**
|
|
17
|
+
* Call the vault's `borrow_and_pay(merchant, amount, collateral, vault_id)`.
|
|
18
|
+
* The vault pulls the agent's CEP-18 collateral into escrow (transfer_from) and
|
|
19
|
+
* fronts the CEP-18 `amount` to the merchant from the liquidity pool. Returns the
|
|
20
|
+
* settlement deploy hash. (Tier-3 agents borrow with zero collateral; lower tiers
|
|
21
|
+
* must `approve` the vault for >= collateral first — see ensureCollateralAllowance.)
|
|
22
|
+
*/
|
|
23
|
+
export declare function borrowAndPayOnChain(cfg: CasperWiringConfig, args: {
|
|
24
|
+
merchant: string;
|
|
25
|
+
amount: bigint;
|
|
26
|
+
collateral: bigint;
|
|
27
|
+
vaultId: string;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
deployHash: string;
|
|
30
|
+
}>;
|
|
31
|
+
/** Repay a loan: vault pulls principal back from the agent (CEP-18 allowance required). */
|
|
32
|
+
export declare function repayLoanOnChain(cfg: CasperWiringConfig, loanId: number): Promise<{
|
|
33
|
+
deployHash: string;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Approve the vault to pull `amount` of the CEP-18 asset from the agent (needed
|
|
37
|
+
* before a collateralized borrow_and_pay can escrow, and before repay_loan can
|
|
38
|
+
* pull the principal). One-time per allowance top-up.
|
|
39
|
+
*/
|
|
40
|
+
export declare function ensureCollateralAllowance(cfg: CasperWiringConfig & {
|
|
41
|
+
assetPackageHash: string;
|
|
42
|
+
}, spender: {
|
|
43
|
+
vaultContractHash: string;
|
|
44
|
+
}, amount: bigint): Promise<{
|
|
45
|
+
deployHash: string;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Poll the node until a deploy executes; resolves true on success. Mirrors the
|
|
49
|
+
* pattern proven live in production: stringify the whole getDeploy response (the
|
|
50
|
+
* v5 SDK shape varies across Casper 1.x / 2.0 Condor), treat a non-null
|
|
51
|
+
* errorMessage / "Failure" tag as failure, and "cost"/"Success" as success.
|
|
52
|
+
*/
|
|
53
|
+
export declare function waitForDeploy(cfg: Pick<CasperWiringConfig, "nodeUrl">, deployHash: string, { tries, intervalMs }?: {
|
|
54
|
+
tries?: number;
|
|
55
|
+
intervalMs?: number;
|
|
56
|
+
}): Promise<boolean>;
|
|
57
|
+
/** Casper account-hash ("00" + 32-byte hash) for the agent's public key. */
|
|
58
|
+
export declare function agentTaggedAddress(agentPublicKey: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Build the real x402 v2 `exact` PaymentPayload for the casper:* family. The
|
|
61
|
+
* `authorization` + 65-byte EIP-712 `signature` verify against the CSPR.cloud
|
|
62
|
+
* facilitator's POST /verify. We attach the Fund402 `settlement.deployHash`
|
|
63
|
+
* extension — the on-chain vault borrow_and_pay deploy the gateway checks.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildExactPayload(cfg: CasperWiringConfig, req: Partial<PaymentRequirements> & {
|
|
66
|
+
payTo: string;
|
|
67
|
+
}, proof: {
|
|
68
|
+
deployHash: string;
|
|
69
|
+
}): Promise<{
|
|
70
|
+
x402Version: 2;
|
|
71
|
+
resource: {
|
|
72
|
+
url: string;
|
|
73
|
+
} | undefined;
|
|
74
|
+
accepted: {
|
|
75
|
+
scheme: "exact";
|
|
76
|
+
network: string;
|
|
77
|
+
asset: string;
|
|
78
|
+
payTo: string;
|
|
79
|
+
amount: string;
|
|
80
|
+
maxTimeoutSeconds: number;
|
|
81
|
+
extra: {
|
|
82
|
+
name: string;
|
|
83
|
+
version: string;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
scheme: "exact";
|
|
87
|
+
network: string;
|
|
88
|
+
payload: {
|
|
89
|
+
signature: string;
|
|
90
|
+
publicKey: string;
|
|
91
|
+
authorization: {
|
|
92
|
+
from: string;
|
|
93
|
+
to: string;
|
|
94
|
+
value: string;
|
|
95
|
+
validAfter: string;
|
|
96
|
+
validBefore: string;
|
|
97
|
+
nonce: string;
|
|
98
|
+
};
|
|
99
|
+
settlement: {
|
|
100
|
+
deployHash: string;
|
|
101
|
+
asset: string;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
paymentRequirements: {
|
|
105
|
+
scheme: "exact";
|
|
106
|
+
network: string;
|
|
107
|
+
asset: string;
|
|
108
|
+
payTo: string;
|
|
109
|
+
amount: string;
|
|
110
|
+
maxTimeoutSeconds: number;
|
|
111
|
+
extra: {
|
|
112
|
+
name: string;
|
|
113
|
+
version: string;
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
}>;
|
package/dist/casper.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Real Casper wiring (casper-js-sdk@5.x / Casper 2.0 Condor). Turns the abstract
|
|
3
|
+
// "borrow + settle" steps into actual on-chain calls against the Fund402 Vault,
|
|
4
|
+
// and builds the x402 `exact` PaymentPayload the facilitator's POST /verify checks.
|
|
5
|
+
//
|
|
6
|
+
// The payload is signed off the proven `eip712.ts` digest + `signAndAddAlgorithmBytes`
|
|
7
|
+
// (65-byte [algorithm|sig]) — byte-identical to the official @make-software/casper-x402
|
|
8
|
+
// client, but WITHOUT depending on its broken CJS build at runtime.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.rpc = rpc;
|
|
11
|
+
exports.loadPrivateKey = loadPrivateKey;
|
|
12
|
+
exports.borrowAndPayOnChain = borrowAndPayOnChain;
|
|
13
|
+
exports.repayLoanOnChain = repayLoanOnChain;
|
|
14
|
+
exports.ensureCollateralAllowance = ensureCollateralAllowance;
|
|
15
|
+
exports.waitForDeploy = waitForDeploy;
|
|
16
|
+
exports.agentTaggedAddress = agentTaggedAddress;
|
|
17
|
+
exports.buildExactPayload = buildExactPayload;
|
|
18
|
+
const casper_js_sdk_1 = require("casper-js-sdk");
|
|
19
|
+
const eip712_1 = require("./eip712");
|
|
20
|
+
function rpc(nodeUrl) {
|
|
21
|
+
return new casper_js_sdk_1.RpcClient(new casper_js_sdk_1.HttpHandler(nodeUrl));
|
|
22
|
+
}
|
|
23
|
+
async function loadPrivateKey(secret, algo = casper_js_sdk_1.KeyAlgorithm.ED25519) {
|
|
24
|
+
const trimmed = secret.trim();
|
|
25
|
+
if (trimmed.includes("BEGIN") && trimmed.includes("PRIVATE KEY")) {
|
|
26
|
+
return casper_js_sdk_1.PrivateKey.fromPem(trimmed, algo);
|
|
27
|
+
}
|
|
28
|
+
return casper_js_sdk_1.PrivateKey.fromHex(trimmed, algo);
|
|
29
|
+
}
|
|
30
|
+
const stripPkg = (s) => s.replace(/^(hash-|contract-package-|package-)/, "");
|
|
31
|
+
/**
|
|
32
|
+
* Build a CLKey arg from an account address. Accepts a raw 64-hex account hash,
|
|
33
|
+
* an "account-hash-…" formatted string, OR an x402 *tagged* address ("00" + 64-hex,
|
|
34
|
+
* the Account tag) as carried in a payment challenge's `payTo` — the 1-byte tag is
|
|
35
|
+
* stripped so the on-chain Key uses the bare 32-byte account hash.
|
|
36
|
+
*/
|
|
37
|
+
function addressKey(addr) {
|
|
38
|
+
let h = addr.replace(/^account-hash-/, "");
|
|
39
|
+
if (/^00[0-9a-fA-F]{64}$/.test(h))
|
|
40
|
+
h = h.slice(2); // drop the "00" account tag
|
|
41
|
+
return casper_js_sdk_1.CLValue.newCLKey(casper_js_sdk_1.Key.newKey(`account-hash-${h}`));
|
|
42
|
+
}
|
|
43
|
+
/** Build a CLKey arg for a contract/package ("hash-<package>"). */
|
|
44
|
+
function pkgKey(pkgHash) {
|
|
45
|
+
return casper_js_sdk_1.CLValue.newCLKey(casper_js_sdk_1.Key.newKey("hash-" + stripPkg(pkgHash)));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Session item calling a stored contract BY PACKAGE HASH (StoredVersionedContractByHash,
|
|
49
|
+
* latest version). This is the path proven live on testnet — the deployed Fund402
|
|
50
|
+
* vault + CEP-18 are addressed by their package hash (what cspr.live shows).
|
|
51
|
+
*/
|
|
52
|
+
function callPkg(pkgHash, entryPoint, args) {
|
|
53
|
+
const s = new casper_js_sdk_1.ExecutableDeployItem();
|
|
54
|
+
s.storedVersionedContractByHash = new casper_js_sdk_1.StoredVersionedContractByHash(casper_js_sdk_1.ContractHash.newContract(stripPkg(pkgHash)), entryPoint, args, undefined);
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Call the vault's `borrow_and_pay(merchant, amount, collateral, vault_id)`.
|
|
59
|
+
* The vault pulls the agent's CEP-18 collateral into escrow (transfer_from) and
|
|
60
|
+
* fronts the CEP-18 `amount` to the merchant from the liquidity pool. Returns the
|
|
61
|
+
* settlement deploy hash. (Tier-3 agents borrow with zero collateral; lower tiers
|
|
62
|
+
* must `approve` the vault for >= collateral first — see ensureCollateralAllowance.)
|
|
63
|
+
*/
|
|
64
|
+
async function borrowAndPayOnChain(cfg, args) {
|
|
65
|
+
const client = rpc(cfg.nodeUrl);
|
|
66
|
+
const algo = cfg.keyAlgorithm ?? casper_js_sdk_1.KeyAlgorithm.ED25519;
|
|
67
|
+
const signer = await loadPrivateKey(cfg.agentSecretKey, algo);
|
|
68
|
+
const sender = casper_js_sdk_1.PublicKey.fromHex(cfg.agentPublicKey);
|
|
69
|
+
const runtimeArgs = casper_js_sdk_1.Args.fromMap({
|
|
70
|
+
merchant: addressKey(args.merchant),
|
|
71
|
+
amount: casper_js_sdk_1.CLValue.newCLUInt256(args.amount.toString()),
|
|
72
|
+
collateral: casper_js_sdk_1.CLValue.newCLUInt256(args.collateral.toString()),
|
|
73
|
+
vault_id: casper_js_sdk_1.CLValue.newCLString(args.vaultId),
|
|
74
|
+
});
|
|
75
|
+
const header = casper_js_sdk_1.DeployHeader.default();
|
|
76
|
+
header.account = sender;
|
|
77
|
+
header.chainName = cfg.chainName;
|
|
78
|
+
header.ttl = new casper_js_sdk_1.Duration(casper_js_sdk_1.DEFAULT_DEPLOY_TTL);
|
|
79
|
+
const session = callPkg(cfg.vaultContractHash, "borrow_and_pay", runtimeArgs);
|
|
80
|
+
const payment = casper_js_sdk_1.ExecutableDeployItem.standardPayment(cfg.borrowGasMotes ?? "5000000000");
|
|
81
|
+
const deploy = casper_js_sdk_1.Deploy.makeDeploy(header, payment, session);
|
|
82
|
+
deploy.sign(signer);
|
|
83
|
+
const result = await client.putDeploy(deploy);
|
|
84
|
+
return { deployHash: result.deployHash.toHex() };
|
|
85
|
+
}
|
|
86
|
+
/** Repay a loan: vault pulls principal back from the agent (CEP-18 allowance required). */
|
|
87
|
+
async function repayLoanOnChain(cfg, loanId) {
|
|
88
|
+
const client = rpc(cfg.nodeUrl);
|
|
89
|
+
const algo = cfg.keyAlgorithm ?? casper_js_sdk_1.KeyAlgorithm.ED25519;
|
|
90
|
+
const signer = await loadPrivateKey(cfg.agentSecretKey, algo);
|
|
91
|
+
const sender = casper_js_sdk_1.PublicKey.fromHex(cfg.agentPublicKey);
|
|
92
|
+
const header = casper_js_sdk_1.DeployHeader.default();
|
|
93
|
+
header.account = sender;
|
|
94
|
+
header.chainName = cfg.chainName;
|
|
95
|
+
header.ttl = new casper_js_sdk_1.Duration(casper_js_sdk_1.DEFAULT_DEPLOY_TTL);
|
|
96
|
+
const session = callPkg(cfg.vaultContractHash, "repay_loan", casper_js_sdk_1.Args.fromMap({ loan_id: casper_js_sdk_1.CLValue.newCLUint64(BigInt(loanId)) }));
|
|
97
|
+
const payment = casper_js_sdk_1.ExecutableDeployItem.standardPayment("3000000000");
|
|
98
|
+
const deploy = casper_js_sdk_1.Deploy.makeDeploy(header, payment, session);
|
|
99
|
+
deploy.sign(signer);
|
|
100
|
+
const result = await client.putDeploy(deploy);
|
|
101
|
+
return { deployHash: result.deployHash.toHex() };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Approve the vault to pull `amount` of the CEP-18 asset from the agent (needed
|
|
105
|
+
* before a collateralized borrow_and_pay can escrow, and before repay_loan can
|
|
106
|
+
* pull the principal). One-time per allowance top-up.
|
|
107
|
+
*/
|
|
108
|
+
async function ensureCollateralAllowance(cfg, spender, amount) {
|
|
109
|
+
const client = rpc(cfg.nodeUrl);
|
|
110
|
+
const algo = cfg.keyAlgorithm ?? casper_js_sdk_1.KeyAlgorithm.ED25519;
|
|
111
|
+
const signer = await loadPrivateKey(cfg.agentSecretKey, algo);
|
|
112
|
+
const sender = casper_js_sdk_1.PublicKey.fromHex(cfg.agentPublicKey);
|
|
113
|
+
const header = casper_js_sdk_1.DeployHeader.default();
|
|
114
|
+
header.account = sender;
|
|
115
|
+
header.chainName = cfg.chainName;
|
|
116
|
+
header.ttl = new casper_js_sdk_1.Duration(casper_js_sdk_1.DEFAULT_DEPLOY_TTL);
|
|
117
|
+
const session = callPkg(cfg.assetPackageHash, "approve", casper_js_sdk_1.Args.fromMap({
|
|
118
|
+
spender: pkgKey(spender.vaultContractHash),
|
|
119
|
+
amount: casper_js_sdk_1.CLValue.newCLUInt256(amount.toString()),
|
|
120
|
+
}));
|
|
121
|
+
const payment = casper_js_sdk_1.ExecutableDeployItem.standardPayment("2000000000");
|
|
122
|
+
const deploy = casper_js_sdk_1.Deploy.makeDeploy(header, payment, session);
|
|
123
|
+
deploy.sign(signer);
|
|
124
|
+
const result = await client.putDeploy(deploy);
|
|
125
|
+
return { deployHash: result.deployHash.toHex() };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Poll the node until a deploy executes; resolves true on success. Mirrors the
|
|
129
|
+
* pattern proven live in production: stringify the whole getDeploy response (the
|
|
130
|
+
* v5 SDK shape varies across Casper 1.x / 2.0 Condor), treat a non-null
|
|
131
|
+
* errorMessage / "Failure" tag as failure, and "cost"/"Success" as success.
|
|
132
|
+
*/
|
|
133
|
+
async function waitForDeploy(cfg, deployHash, { tries = 60, intervalMs = 3000 } = {}) {
|
|
134
|
+
const client = rpc(cfg.nodeUrl);
|
|
135
|
+
for (let i = 0; i < tries; i++) {
|
|
136
|
+
try {
|
|
137
|
+
const res = await client.getDeploy(deployHash);
|
|
138
|
+
const txt = JSON.stringify(res?.executionResults ?? res?.executionInfo ?? res ?? {});
|
|
139
|
+
if (txt.includes('"Failure"') || /"error_?[Mm]essage":\s*"[^"]/.test(txt))
|
|
140
|
+
return false;
|
|
141
|
+
if (txt.includes('"Success"') || txt.includes('"cost"'))
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
/* not propagated yet */
|
|
146
|
+
}
|
|
147
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
/** Casper account-hash ("00" + 32-byte hash) for the agent's public key. */
|
|
152
|
+
function agentTaggedAddress(agentPublicKey) {
|
|
153
|
+
const pk = casper_js_sdk_1.PublicKey.fromHex(agentPublicKey);
|
|
154
|
+
const ah = pk.accountHash();
|
|
155
|
+
let hex = (ah?.toHex?.() ?? ah?.toString?.() ?? String(ah)).replace(/^account-hash-/, "");
|
|
156
|
+
hex = hex.replace(/^0x/, "");
|
|
157
|
+
return "00" + hex; // 00 = AccountHash tag
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Build the real x402 v2 `exact` PaymentPayload for the casper:* family. The
|
|
161
|
+
* `authorization` + 65-byte EIP-712 `signature` verify against the CSPR.cloud
|
|
162
|
+
* facilitator's POST /verify. We attach the Fund402 `settlement.deployHash`
|
|
163
|
+
* extension — the on-chain vault borrow_and_pay deploy the gateway checks.
|
|
164
|
+
*/
|
|
165
|
+
async function buildExactPayload(cfg, req, proof) {
|
|
166
|
+
const algo = cfg.keyAlgorithm ?? casper_js_sdk_1.KeyAlgorithm.ED25519;
|
|
167
|
+
const priv = await loadPrivateKey(cfg.agentSecretKey, algo);
|
|
168
|
+
const assetPkg = (req.asset ?? req.extra?.name ?? "").replace(/^0x/, "");
|
|
169
|
+
const amount = String(req.amount ?? "0");
|
|
170
|
+
const maxTimeoutSeconds = req.maxTimeoutSeconds ?? 300;
|
|
171
|
+
const name = req.extra?.name ?? "Cep18x402";
|
|
172
|
+
const version = req.extra?.version ?? "1";
|
|
173
|
+
const now = Math.floor(Date.now() / 1000);
|
|
174
|
+
const validAfter = now - 600;
|
|
175
|
+
const validBefore = now + maxTimeoutSeconds;
|
|
176
|
+
const nonce = (0, eip712_1.randomNonce)();
|
|
177
|
+
const from = "00" + priv.publicKey.accountHash().toHex(); // tagged account hash
|
|
178
|
+
const to = req.payTo;
|
|
179
|
+
const digest = (0, eip712_1.transferAuthorizationDigest)({ name, version, chainName: cfg.network, contractPackageHash: assetPkg }, { from, to, value: amount, validAfter: String(validAfter), validBefore: String(validBefore), nonce });
|
|
180
|
+
const signature = (0, eip712_1.bytesToHex)(priv.signAndAddAlgorithmBytes(digest));
|
|
181
|
+
const publicKey = priv.publicKey.toHex();
|
|
182
|
+
const requirements = {
|
|
183
|
+
scheme: "exact",
|
|
184
|
+
network: cfg.network,
|
|
185
|
+
asset: assetPkg,
|
|
186
|
+
payTo: to,
|
|
187
|
+
amount,
|
|
188
|
+
maxTimeoutSeconds,
|
|
189
|
+
extra: { name, version },
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
x402Version: 2,
|
|
193
|
+
resource: req.resource ? { url: req.resource } : undefined,
|
|
194
|
+
accepted: requirements,
|
|
195
|
+
scheme: "exact",
|
|
196
|
+
network: cfg.network,
|
|
197
|
+
payload: {
|
|
198
|
+
signature,
|
|
199
|
+
publicKey,
|
|
200
|
+
authorization: {
|
|
201
|
+
from,
|
|
202
|
+
to,
|
|
203
|
+
value: amount,
|
|
204
|
+
validAfter: String(validAfter),
|
|
205
|
+
validBefore: String(validBefore),
|
|
206
|
+
nonce,
|
|
207
|
+
},
|
|
208
|
+
settlement: { deployHash: proof.deployHash, asset: assetPkg },
|
|
209
|
+
},
|
|
210
|
+
paymentRequirements: requirements,
|
|
211
|
+
};
|
|
212
|
+
}
|