@cloudpayx/x402-pay 0.2.3 → 0.2.5
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/AGENTS.md +53 -0
- package/README.md +44 -14
- package/README.pre-pay-earn-release.bak +96 -0
- package/docs/API.md +76 -0
- package/docs/QUICKSTART.md +72 -0
- package/docs/agent-manifest.json +51 -0
- package/docs/agent-onboarding.md +236 -0
- package/examples/earn-http-server.mjs +60 -0
- package/examples/quickstart-validation.mjs +65 -0
- package/package.json +13 -3
- package/scripts/durable-purchase-lifecycle.mjs +34 -2
- package/scripts/persistent-spending-budget.mjs +101 -1
- package/scripts/public-node.mjs +2 -0
- package/scripts/secure-node-purchase.mjs +3 -1
- package/scripts/seller-earn.mjs +67 -0
- package/scripts/seller-http.mjs +287 -0
- package/scripts/seller-ledger-verifier.mjs +228 -0
- package/scripts/seller-node.mjs +180 -0
- package/scripts/seller-proof-guard.mjs +43 -0
- package/scripts/seller-transaction-guard.mjs +39 -0
- package/types/public-node.d.ts +20 -1
- package/README.before-0.2.3.bak +0 -94
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Client, decode } from "xrpl";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { MAINNET_LEDGER_URL } from "./trusted-xrpl-primitives.mjs";
|
|
4
|
+
import { getSellerTransactionHash } from "./seller-transaction-guard.mjs";
|
|
5
|
+
|
|
6
|
+
const HASH = /^[0-9A-F]{64}$/;
|
|
7
|
+
|
|
8
|
+
function fail(code) {
|
|
9
|
+
throw new Error(code);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function decimal(value) {
|
|
13
|
+
if (
|
|
14
|
+
typeof value !== "string" ||
|
|
15
|
+
!/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(value)
|
|
16
|
+
) {
|
|
17
|
+
fail("CPX_SELLER_LEDGER_AMOUNT_INVALID");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const [whole, fraction = ""] = value.split(".");
|
|
21
|
+
return whole + "." + fraction.replace(/0+$/, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks ledger evidence against the signed transaction and the
|
|
26
|
+
* exact x402 requirements. No facilitator claim is trusted here.
|
|
27
|
+
*/
|
|
28
|
+
export function verifySellerLedgerEvidence({
|
|
29
|
+
ledgerResult,
|
|
30
|
+
signedTxBlob,
|
|
31
|
+
requirements
|
|
32
|
+
}) {
|
|
33
|
+
const expectedHash = getSellerTransactionHash(signedTxBlob);
|
|
34
|
+
|
|
35
|
+
let signed;
|
|
36
|
+
try {
|
|
37
|
+
signed = decode(signedTxBlob);
|
|
38
|
+
} catch {
|
|
39
|
+
fail("CPX_SELLER_SIGNED_TRANSACTION_INVALID");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
!requirements ||
|
|
44
|
+
requirements.network !== "xrpl:0" ||
|
|
45
|
+
typeof requirements.payTo !== "string" ||
|
|
46
|
+
typeof requirements.asset !== "string" ||
|
|
47
|
+
typeof requirements.amount !== "string"
|
|
48
|
+
) {
|
|
49
|
+
fail("CPX_SELLER_LEDGER_REQUIREMENTS_INVALID");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Independently enforce the requested currency on the
|
|
53
|
+
// ACTUAL signed XRPL transaction, not just ledger metadata.
|
|
54
|
+
if (
|
|
55
|
+
signed.TransactionType !== "Payment" ||
|
|
56
|
+
signed.Destination !== requirements.payTo ||
|
|
57
|
+
signed.Paths !== undefined ||
|
|
58
|
+
signed.DeliverMin !== undefined ||
|
|
59
|
+
(typeof signed.Flags === "number" &&
|
|
60
|
+
(signed.Flags & 0x00020000) !== 0)
|
|
61
|
+
) {
|
|
62
|
+
fail("CPX_SELLER_SIGNED_PAYMENT_MISMATCH");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const signedAmount = signed.DeliverMax ?? signed.Amount;
|
|
66
|
+
|
|
67
|
+
if (requirements.asset === "XRP") {
|
|
68
|
+
if (
|
|
69
|
+
typeof signedAmount !== "string" ||
|
|
70
|
+
!/^[0-9]+$/.test(signedAmount) ||
|
|
71
|
+
!/^[0-9]+$/.test(requirements.amount) ||
|
|
72
|
+
BigInt(signedAmount) !== BigInt(requirements.amount) ||
|
|
73
|
+
signed.SendMax !== undefined
|
|
74
|
+
) {
|
|
75
|
+
fail("CPX_SELLER_SIGNED_ASSET_MISMATCH");
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
const expectedIssuer = requirements.extra?.issuer;
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
!signedAmount ||
|
|
82
|
+
typeof signedAmount !== "object" ||
|
|
83
|
+
signedAmount.currency !== requirements.asset ||
|
|
84
|
+
signedAmount.issuer !== expectedIssuer ||
|
|
85
|
+
decimal(signedAmount.value) !==
|
|
86
|
+
decimal(requirements.amount) ||
|
|
87
|
+
!signed.SendMax ||
|
|
88
|
+
typeof signed.SendMax !== "object" ||
|
|
89
|
+
signed.SendMax.currency !== requirements.asset ||
|
|
90
|
+
signed.SendMax.issuer !== expectedIssuer ||
|
|
91
|
+
decimal(signed.SendMax.value) !==
|
|
92
|
+
decimal(requirements.amount)
|
|
93
|
+
) {
|
|
94
|
+
fail("CPX_SELLER_SIGNED_ASSET_MISMATCH");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const invoiceId = requirements.extra?.invoiceId;
|
|
99
|
+
const destinationTag = requirements.extra?.destinationTag;
|
|
100
|
+
|
|
101
|
+
if (typeof invoiceId !== "string" ||
|
|
102
|
+
invoiceId.length < 1 || invoiceId.length > 256 ||
|
|
103
|
+
!Number.isSafeInteger(destinationTag) ||
|
|
104
|
+
destinationTag < 0 || destinationTag > 0xffffffff) {
|
|
105
|
+
fail("CPX_SELLER_LEDGER_INVOICE_OR_TAG_REQUIRED");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const expectedInvoice = createHash("sha256")
|
|
109
|
+
.update(invoiceId, "utf8")
|
|
110
|
+
.digest("hex")
|
|
111
|
+
.toUpperCase();
|
|
112
|
+
|
|
113
|
+
if (signed.InvoiceID?.toUpperCase() !== expectedInvoice ||
|
|
114
|
+
signed.DestinationTag !== destinationTag) {
|
|
115
|
+
fail("CPX_SELLER_SIGNED_INVOICE_OR_TAG_MISMATCH");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
requirements.extra?.sourceTag !== undefined &&
|
|
120
|
+
signed.SourceTag !== requirements.extra.sourceTag
|
|
121
|
+
) {
|
|
122
|
+
fail("CPX_SELLER_SIGNED_SOURCE_TAG_MISMATCH");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const tx = ledgerResult?.tx_json ?? ledgerResult?.tx;
|
|
126
|
+
const meta = ledgerResult?.meta ?? ledgerResult?.metaData;
|
|
127
|
+
|
|
128
|
+
const reportedHash = ledgerResult?.hash ?? tx?.hash;
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
ledgerResult?.validated !== true ||
|
|
132
|
+
typeof reportedHash !== "string" ||
|
|
133
|
+
!HASH.test(reportedHash.toUpperCase()) ||
|
|
134
|
+
reportedHash.toUpperCase() !== expectedHash ||
|
|
135
|
+
!Number.isSafeInteger(ledgerResult.ledger_index) ||
|
|
136
|
+
ledgerResult.ledger_index <= 0 ||
|
|
137
|
+
meta?.TransactionResult !== "tesSUCCESS" ||
|
|
138
|
+
tx?.TransactionType !== "Payment" ||
|
|
139
|
+
tx?.Destination !== requirements.payTo
|
|
140
|
+
) {
|
|
141
|
+
fail("CPX_SELLER_LEDGER_PAYMENT_NOT_CONFIRMED");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// A successful Payment result must contain a concrete delivered
|
|
145
|
+
// amount. Do not infer delivery solely from the transaction request.
|
|
146
|
+
if (
|
|
147
|
+
requirements.extra?.sourceTag !== undefined &&
|
|
148
|
+
tx.SourceTag !== requirements.extra.sourceTag
|
|
149
|
+
) {
|
|
150
|
+
fail("CPX_SELLER_LEDGER_SOURCE_TAG_MISMATCH");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const delivered = meta.delivered_amount;
|
|
154
|
+
|
|
155
|
+
if (requirements.asset === "XRP") {
|
|
156
|
+
if (
|
|
157
|
+
typeof delivered !== "string" ||
|
|
158
|
+
!/^[0-9]+$/.test(delivered) ||
|
|
159
|
+
!/^[0-9]+$/.test(requirements.amount) ||
|
|
160
|
+
BigInt(delivered) !== BigInt(requirements.amount)
|
|
161
|
+
) {
|
|
162
|
+
fail("CPX_SELLER_LEDGER_AMOUNT_MISMATCH");
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
if (
|
|
166
|
+
!delivered ||
|
|
167
|
+
typeof delivered !== "object" ||
|
|
168
|
+
delivered.currency !== requirements.asset ||
|
|
169
|
+
delivered.issuer !== requirements.extra?.issuer ||
|
|
170
|
+
decimal(delivered.value) !== decimal(requirements.amount)
|
|
171
|
+
) {
|
|
172
|
+
fail("CPX_SELLER_LEDGER_ASSET_MISMATCH");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (requirements.extra?.destinationTag !== undefined &&
|
|
177
|
+
tx.DestinationTag !== requirements.extra.destinationTag) {
|
|
178
|
+
fail("CPX_SELLER_LEDGER_DESTINATION_TAG_MISMATCH");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (tx.InvoiceID?.toUpperCase() !== expectedInvoice ||
|
|
182
|
+
tx.DestinationTag !== destinationTag ||
|
|
183
|
+
tx.Account !== signed.Account ||
|
|
184
|
+
tx.Fee !== signed.Fee ||
|
|
185
|
+
(tx.SourceTag ?? undefined) !==
|
|
186
|
+
(signed.SourceTag ?? undefined)) {
|
|
187
|
+
fail("CPX_SELLER_LEDGER_TRANSACTION_FIELDS_MISMATCH");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return Object.freeze({
|
|
191
|
+
validated: true,
|
|
192
|
+
transaction: expectedHash,
|
|
193
|
+
network: "xrpl:0",
|
|
194
|
+
ledgerIndex: ledgerResult.ledger_index
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Production verifier uses the fixed Ripple-operated XRPL mainnet
|
|
200
|
+
* endpoint. This is a single-provider trust dependency, not quorum.
|
|
201
|
+
* Never supplies signing keys or submits a transaction.
|
|
202
|
+
*/
|
|
203
|
+
export async function confirmSellerLedgerPayment({
|
|
204
|
+
signedTxBlob,
|
|
205
|
+
requirements
|
|
206
|
+
}) {
|
|
207
|
+
const hash = getSellerTransactionHash(signedTxBlob);
|
|
208
|
+
const client = new Client(MAINNET_LEDGER_URL);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await client.connect();
|
|
212
|
+
|
|
213
|
+
const response = await client.request({
|
|
214
|
+
command: "tx",
|
|
215
|
+
transaction: hash,
|
|
216
|
+
binary: false,
|
|
217
|
+
api_version: 2
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return verifySellerLedgerEvidence({
|
|
221
|
+
ledgerResult: response.result,
|
|
222
|
+
signedTxBlob,
|
|
223
|
+
requirements
|
|
224
|
+
});
|
|
225
|
+
} finally {
|
|
226
|
+
await client.disconnect();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
x402ResourceServer,
|
|
3
|
+
x402HTTPResourceServer,
|
|
4
|
+
HTTPFacilitatorClient
|
|
5
|
+
} from "@x402/core/server";
|
|
6
|
+
import { ExactXrplScheme } from "@x402/xrpl/exact/server";
|
|
7
|
+
import { isValidClassicAddress } from "xrpl";
|
|
8
|
+
|
|
9
|
+
const ASSETS = Object.freeze({
|
|
10
|
+
XRP: {
|
|
11
|
+
asset: "XRP"
|
|
12
|
+
},
|
|
13
|
+
RLUSD: {
|
|
14
|
+
asset: "524C555344000000000000000000000000000000",
|
|
15
|
+
issuer: "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De"
|
|
16
|
+
},
|
|
17
|
+
USDC: {
|
|
18
|
+
asset: "5553444300000000000000000000000000000000",
|
|
19
|
+
issuer: "rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE"
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function requirePrice(asset, amount) {
|
|
24
|
+
if (typeof amount !== "string") {
|
|
25
|
+
throw new TypeError("CPX_SELLER_AMOUNT_MUST_BE_STRING");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (asset === "XRP") {
|
|
29
|
+
if (!/^[1-9][0-9]*$/.test(amount)) {
|
|
30
|
+
throw new Error("CPX_SELLER_XRP_REQUIRES_POSITIVE_DROPS");
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
!/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(amount) ||
|
|
37
|
+
!/[1-9]/.test(amount)
|
|
38
|
+
) {
|
|
39
|
+
throw new Error("CPX_SELLER_INVALID_ISSUED_AMOUNT");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createCpXSellerConfig({
|
|
44
|
+
walletAddress,
|
|
45
|
+
asset,
|
|
46
|
+
amount,
|
|
47
|
+
method = "GET",
|
|
48
|
+
path = "/paid",
|
|
49
|
+
description = "Paid XRPL agent service",
|
|
50
|
+
invoiceId,
|
|
51
|
+
destinationTag
|
|
52
|
+
}) {
|
|
53
|
+
if (!isValidClassicAddress(walletAddress)) {
|
|
54
|
+
throw new Error("CPX_SELLER_INVALID_WALLET_ADDRESS");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!Object.hasOwn(ASSETS, asset)) {
|
|
58
|
+
throw new Error("CPX_SELLER_UNSUPPORTED_ASSET");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
requirePrice(asset, amount);
|
|
62
|
+
|
|
63
|
+
if (typeof invoiceId !== "string" ||
|
|
64
|
+
invoiceId.length < 1 || invoiceId.length > 256) {
|
|
65
|
+
throw new Error("CPX_SELLER_INVOICE_REQUIRED");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!Number.isSafeInteger(destinationTag) ||
|
|
69
|
+
destinationTag < 0 || destinationTag > 0xffffffff) {
|
|
70
|
+
throw new Error("CPX_SELLER_DESTINATION_TAG_REQUIRED");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!["GET", "POST"].includes(method)) {
|
|
74
|
+
throw new Error("CPX_SELLER_UNSUPPORTED_METHOD");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
typeof path !== "string" ||
|
|
79
|
+
!new RegExp("^/[a-zA-Z0-9/_-]+$").test(path) ||
|
|
80
|
+
path.includes("//") ||
|
|
81
|
+
path.includes("..")
|
|
82
|
+
) {
|
|
83
|
+
throw new Error("CPX_SELLER_INVALID_ROUTE");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const token = ASSETS[asset];
|
|
87
|
+
|
|
88
|
+
const paymentOption = {
|
|
89
|
+
scheme: "exact",
|
|
90
|
+
network: "xrpl:0",
|
|
91
|
+
maxTimeoutSeconds: 120,
|
|
92
|
+
payTo: walletAddress,
|
|
93
|
+
price: {
|
|
94
|
+
asset: token.asset,
|
|
95
|
+
amount,
|
|
96
|
+
...(token.issuer
|
|
97
|
+
? { extra: { issuer: token.issuer } }
|
|
98
|
+
: {})
|
|
99
|
+
},
|
|
100
|
+
extra: {
|
|
101
|
+
...(token.issuer ? { issuer: token.issuer } : {}),
|
|
102
|
+
invoiceId,
|
|
103
|
+
destinationTag,
|
|
104
|
+
...((asset === "RLUSD" || asset === "USDC") ? { areFeesSponsored: false } : {}),
|
|
105
|
+
...(asset === "XRP" || asset === "RLUSD" || asset === "USDC"
|
|
106
|
+
? { sourceTag: 804681468 }
|
|
107
|
+
: {})
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
[`${method} ${path}`]: {
|
|
113
|
+
accepts: [paymentOption],
|
|
114
|
+
description,
|
|
115
|
+
mimeType: "application/json"
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createCpXSellerServer({
|
|
121
|
+
facilitatorUrl,
|
|
122
|
+
routes
|
|
123
|
+
}) {
|
|
124
|
+
if (
|
|
125
|
+
typeof facilitatorUrl !== "string" ||
|
|
126
|
+
!facilitatorUrl.startsWith("https://")
|
|
127
|
+
) {
|
|
128
|
+
throw new Error("CPX_SELLER_HTTPS_FACILITATOR_REQUIRED");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
!routes ||
|
|
133
|
+
typeof routes !== "object" ||
|
|
134
|
+
Array.isArray(routes)
|
|
135
|
+
) {
|
|
136
|
+
throw new Error("CPX_SELLER_ROUTES_REQUIRED");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const facilitatorClient = new HTTPFacilitatorClient({
|
|
140
|
+
url: facilitatorUrl
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Observe official verification results without changing them.
|
|
144
|
+
const facilitator = new Proxy(facilitatorClient, {
|
|
145
|
+
get(target, property) {
|
|
146
|
+
if (property === "verify") {
|
|
147
|
+
return async (...args) => {
|
|
148
|
+
try {
|
|
149
|
+
const result = await target.verify(...args);
|
|
150
|
+
|
|
151
|
+
if (result?.isValid === false) {
|
|
152
|
+
console.error("[CPX_T54_VERIFY]", {
|
|
153
|
+
isValid: false,
|
|
154
|
+
invalidReason: result.invalidReason ?? null,
|
|
155
|
+
invalidMessage: result.invalidMessage ?? null
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error("[CPX_T54_VERIFY_EXCEPTION]", {
|
|
162
|
+
name: error?.name ?? "Error"
|
|
163
|
+
});
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const value = Reflect.get(target, property, target);
|
|
170
|
+
return typeof value === "function"
|
|
171
|
+
? value.bind(target)
|
|
172
|
+
: value;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const resourceServer = new x402ResourceServer(facilitator)
|
|
177
|
+
.register("xrpl:*", new ExactXrplScheme());
|
|
178
|
+
|
|
179
|
+
return new x402HTTPResourceServer(resourceServer, routes);
|
|
180
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Durable, atomic replay guard for exact payment-proof bytes.
|
|
7
|
+
*
|
|
8
|
+
* This is a supporting control, not a substitute for XRPL
|
|
9
|
+
* transaction-hash replay detection and validated-ledger checks.
|
|
10
|
+
*/
|
|
11
|
+
export function createSellerProofGuard({ journalDir }) {
|
|
12
|
+
if (typeof journalDir !== "string" || !journalDir.trim()) {
|
|
13
|
+
throw new Error("CPX_SELLER_JOURNAL_REQUIRED");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const directory = resolve(journalDir);
|
|
17
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
claim(proof) {
|
|
21
|
+
if (typeof proof !== "string" || !proof.trim()) {
|
|
22
|
+
throw new Error("CPX_SELLER_PROOF_REQUIRED");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const digest = createHash("sha256")
|
|
26
|
+
.update(proof, "utf8")
|
|
27
|
+
.digest("hex");
|
|
28
|
+
|
|
29
|
+
const file = join(directory, digest + ".claim");
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(file, "CLAIMED\n", {
|
|
33
|
+
flag: "wx",
|
|
34
|
+
mode: 0o600
|
|
35
|
+
});
|
|
36
|
+
return true;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error?.code === "EEXIST") return false;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { hashes } from "xrpl";
|
|
2
|
+
import { createSellerProofGuard } from "./seller-proof-guard.mjs";
|
|
3
|
+
|
|
4
|
+
const HEX = /^(?:[0-9a-fA-F]{2})+$/;
|
|
5
|
+
|
|
6
|
+
export function getSellerTransactionHash(signedTxBlob) {
|
|
7
|
+
if (
|
|
8
|
+
typeof signedTxBlob !== "string" ||
|
|
9
|
+
!HEX.test(signedTxBlob)
|
|
10
|
+
) {
|
|
11
|
+
throw new Error("CPX_SELLER_SIGNED_TRANSACTION_REQUIRED");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hash = hashes.hashSignedTx(signedTxBlob);
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
typeof hash !== "string" ||
|
|
18
|
+
!/^[0-9A-Fa-f]{64}$/.test(hash)
|
|
19
|
+
) {
|
|
20
|
+
throw new Error("CPX_SELLER_INVALID_TRANSACTION_HASH");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return hash.toUpperCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createSellerTransactionGuard({ journalDir }) {
|
|
27
|
+
const storage = createSellerProofGuard({ journalDir });
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
claimSignedTransaction(signedTxBlob) {
|
|
31
|
+
const hash = getSellerTransactionHash(signedTxBlob);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
hash,
|
|
35
|
+
claimed: storage.claim("xrpl:0:transaction:" + hash)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
package/types/public-node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** cpX x402 Pay
|
|
1
|
+
/** cpX x402 Pay supported Node.js API; no wallet keys are handled here. */
|
|
2
2
|
import type { ClientXrplSigner } from '@x402/xrpl';
|
|
3
3
|
import type { AgentPaymentConfig } from '../dist/src/policy.js';
|
|
4
4
|
import type { GuardOptions } from '../dist/src/one_shot_guard.js';
|
|
@@ -53,3 +53,22 @@ export declare function reconcileSecureNodePurchase(options: {journalDir: string
|
|
|
53
53
|
feeDrops: string;
|
|
54
54
|
invoiceID: string;
|
|
55
55
|
}>;
|
|
56
|
+
|
|
57
|
+
export declare function createCpXAgentEarn(options: {
|
|
58
|
+
walletAddress: string;
|
|
59
|
+
asset: "XRP" | "RLUSD" | "USDC";
|
|
60
|
+
amount: string;
|
|
61
|
+
invoiceId: string;
|
|
62
|
+
destinationTag: number;
|
|
63
|
+
facilitatorUrl: string;
|
|
64
|
+
journalDir: string;
|
|
65
|
+
method?: "GET" | "POST";
|
|
66
|
+
path?: string;
|
|
67
|
+
onPaid: (
|
|
68
|
+
request: Request,
|
|
69
|
+
settlement: unknown
|
|
70
|
+
) => Promise<Response> | Response;
|
|
71
|
+
}): {
|
|
72
|
+
initialize(): Promise<unknown>;
|
|
73
|
+
handle(request: Request): Promise<Response>;
|
|
74
|
+
};
|
package/README.before-0.2.3.bak
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# cpX x402 Pay | XRPL Agent Payment SDK
|
|
2
|
-
|
|
3
|
-
**Purpose:** Let an unaffiliated, self-funded agent participate in the XRPL x402 economy without a cloudpayX registration, dashboard, Supabase session, or required wallet brand. This is an XRPL-native SDK for autonomous agents purchasing x402-protected resources.
|
|
4
|
-
|
|
5
|
-
The SDK supports wallet-agnostic signing, three-asset payment policies, spending controls, guarded signing, and validated-ledger reconciliation. Agents must use compatible XRPL signers and maintain sufficient funds.
|
|
6
|
-
|
|
7
|
-
## Source
|
|
8
|
-
|
|
9
|
-
- `src/policy.ts` — strict, dependency-free XRPL x402 V2 policy checks.
|
|
10
|
-
- `src/assets.ts` — verified XRPL mainnet XRP / Ripple RLUSD / Circle USDC identity constants and three-asset policy builder.
|
|
11
|
-
- `src/index.ts` — adapter to the existing proven `@x402/core`, `@x402/fetch`, `@x402/xrpl` client.
|
|
12
|
-
- `examples/feral-agent.example.ts` — signer-injection example; no secrets or hardcoded wallet provider.
|
|
13
|
-
- `examples/simulation-only.mjs` — no-wallet, no-network, no-payment demonstration.
|
|
14
|
-
- `tests/policy.test.mjs` and `tests/three-assets.test.mjs` — locally executable approval/denial tests for all three assets.
|
|
15
|
-
|
|
16
|
-
## On your Mac (do not deploy yet)
|
|
17
|
-
|
|
18
|
-
**Keep this a separate test folder, do not copy over `src/cloudpayx_x402_pay.ts` or `src/cloudpayx_x402_pay_configured.ts`.**
|
|
19
|
-
|
|
20
|
-
1. Unzip, open Terminal in the unzipped directory.
|
|
21
|
-
2. Run `npm install` (the official `@x402/*` packages are required for the SDK adapter).
|
|
22
|
-
3. Run `npm run test:policy` and `npm run demo:dry-run`.
|
|
23
|
-
4. Run `npm run build` to check SDK integration types against the actual installed packages.
|
|
24
|
-
|
|
25
|
-
`npm run test:policy` needs only the TypeScript compiler, Node.js, and the dependency-free policy module. The build needs official x402 packages; network/package installation was unavailable during packaging.
|
|
26
|
-
|
|
27
|
-
## Three verified XRPL mainnet assets
|
|
28
|
-
|
|
29
|
-
The optional helper `createThreeAssetPolicy` pins the network to `xrpl:0` and enables ONLY these official asset identities:
|
|
30
|
-
|
|
31
|
-
| Asset | x402 `asset` | x402 `extra.issuer` | Max amount unit |
|
|
32
|
-
| --- | --- | --- | --- |
|
|
33
|
-
| XRP | `XRP` | None | Integer drops (1 XRP = 1,000,000 drops) |
|
|
34
|
-
| Ripple RLUSD | `524C555344000000000000000000000000000000` | `rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De` | Decimal RLUSD tokens |
|
|
35
|
-
| Circle USDC on XRPL | `5553444300000000000000000000000000000000` | `rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE` | Decimal USDC tokens |
|
|
36
|
-
|
|
37
|
-
Only these XRPL issuances are accepted by this helper, *not* similarly named bridged tokens, Base USDC, or arbitrary USD IOUs. Signing always uses a caller-supplied `ClientXrplSigner`. **There is no automatic token conversion, exchange quoting, currency netting, or account registration.**
|
|
38
|
-
|
|
39
|
-
## Sample: permissionless, self-funded agent using all three
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
import { createCpXX402AgentPay, createThreeAssetPolicy } from './src/index.js';
|
|
43
|
-
|
|
44
|
-
const { paidFetch } = createCpXX402AgentPay({
|
|
45
|
-
signer: MY_SECURE_XRPL_SIGNER, // compatible @x402/xrpl ClientXrplSigner
|
|
46
|
-
policy: createThreeAssetPolicy({
|
|
47
|
-
limits: {
|
|
48
|
-
xrpDrops: '50000', // 0.05 XRP
|
|
49
|
-
rlusd: '0.05', // 0.05 RLUSD
|
|
50
|
-
usdc: '0.05' // 0.05 Circle USDC (XRPL)
|
|
51
|
-
},
|
|
52
|
-
recipients: { mode: 'any' }, // opt-in open seller participation
|
|
53
|
-
approval: { mode: 'autonomous' } // explicit local operator/agent opt-in
|
|
54
|
-
})
|
|
55
|
-
});
|
|
56
|
-
const response = await paidFetch('https://a-trusted-x402-resource.example/report');
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
`MY_SECURE_XRPL_SIGNER` and seller URL are placeholders. Don't paste wallet secrets into this file. **An unknown agent can use the SDK with its own signer, but does not gain authority over anyone else's funds.**
|
|
60
|
-
|
|
61
|
-
`npm run demo:dry-run` simulates XRP, RLUSD, and USDC policy decisions without a wallet or network.
|
|
62
|
-
|
|
63
|
-
## Units are not interchangeable
|
|
64
|
-
|
|
65
|
-
| XRPL asset | x402 `amount` units | `maxAmount...` example |
|
|
66
|
-
| --- | --- | --- |
|
|
67
|
-
| Native XRP | Drops (1,000,000 drops = 1 XRP) | `{kind:'xrp',asset:'XRP',maxAmountDrops:'50000'}` = 0.05 XRP |
|
|
68
|
-
| RLUSD, USDC or other XRPL-issued token | Decimal issued-token value | `{kind:'issued', asset:'...', issuer:'...', maxAmount:'0.05'}` = 0.05 token |
|
|
69
|
-
|
|
70
|
-
This module rejects scientific notation and prices above its exact cap. Limits are independent; they are **not** an equivalent USD portfolio budget. **Network fees are extra; the per-payment cap bounds the x402 transfer amount, not the combined cost including XRPL fees, trustline reserves, or issuer transfer fees.** XRPL recipient validation here only checks basic address *shape*. The underlying SDK/signer/facilitator must validate destination checksum, invoice/transaction binding, signature authorization, settlement, and errors.
|
|
71
|
-
|
|
72
|
-
## Security boundaries
|
|
73
|
-
|
|
74
|
-
- No Xaman dependency and no human cloudpayX account required for self-funded agents.
|
|
75
|
-
- Strict XRPL mainnet `xrpl:0`, x402 V2, `exact` scheme, `areFeesSponsored=false`.
|
|
76
|
-
- Explicit assets and exact amount caps; opt-in seller-neutral recipient policy or allowlist.
|
|
77
|
-
- Explicit `autonomous` opt-in or application-provided authorization callback, with fail-closed exceptions.
|
|
78
|
-
- A self-funded agent retains *unrestricted authority over its own signer*. A local SDK is **not a trust boundary** against an agent holding the key: it can bypass these checks. Hardware signers, dedicated wallets, and controlled signer services are needed for stronger enforcement.
|
|
79
|
-
- **Security protections:** Spending budgets, duplicate-payment protection, resource-host validation, anti-SSRF controls, replay protection, fee budgeting, guarded signing, and validated-ledger reconciliation are covered by the SDK's security tests. These controls apply when payments use the guarded SDK interface.
|
|
80
|
-
- The live x402 facilitator/service should validate transaction signatures and bind invoice, amount, asset, destination and network, as described in the XRPL exact scheme spec.
|
|
81
|
-
|
|
82
|
-
## Operational conditions for USDC and RLUSD
|
|
83
|
-
|
|
84
|
-
The agent wallet must have the required XRPL token trustline/balance/authorization (where applicable) and enough XRP for transaction fees and reserves. A valid policy decision **does not** guarantee a successful token payment or a resource response. The resource server must advertise a compatible x402 `accepts` entry; the SDK does not auto-swap XRP into stablecoins.
|
|
85
|
-
|
|
86
|
-
XRPL-issued assets require sufficient balances, applicable trustlines, and XRP for fees and reserves. Payment success and resource delivery depend on the network and seller. An agent with unrestricted access to its own signing keys can bypass local SDK policies.
|
|
87
|
-
|
|
88
|
-
**Official issuers:** [Ripple RLUSD](https://docs.ripple.com/products/stablecoin/developer-resources/rlusd-on-the-xrpl) and [Circle XRPL USDC](https://www.circle.com/blog/now-available-usdc-on-the-xrpl).
|
|
89
|
-
|
|
90
|
-
## Integration guidance
|
|
91
|
-
|
|
92
|
-
Use a dedicated, limited-balance XRPL wallet when integrating a new signer or seller. Validate the complete payment and resource-delivery flow before granting unattended spending authority. For stronger enforcement, keep signing authority outside the agent's direct control.
|
|
93
|
-
|
|
94
|
-
Specifications: https://github.com/x402-foundation/x402/blob/main/specs/schemes/exact/scheme_exact_xrpl.md
|