@catena/sdk 0.0.0-alpha-20260724191736
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 +191 -0
- package/README.md +147 -0
- package/dist/client-DZHdFs_y.d.mts +566 -0
- package/dist/client-G-8NfBx_.mjs +597 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.mjs +2 -0
- package/dist/keypair.d.mts +29 -0
- package/dist/keypair.mjs +43 -0
- package/dist/x402.d.mts +175 -0
- package/dist/x402.mjs +225 -0
- package/package.json +46 -0
package/dist/x402.d.mts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { T as X402Resource, i as FetchLike, n as CatenaClient, w as X402PaymentRequirements } from "./client-DZHdFs_y.mjs";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
|
|
4
|
+
//#region src/x402.d.ts
|
|
5
|
+
declare const paymentRequiredSchema: v.LooseObjectSchema<{
|
|
6
|
+
/**
|
|
7
|
+
* Pinned to the wire version this loop speaks. Paying a v1 or future
|
|
8
|
+
* incompatible challenge would settle money for a retry header the server
|
|
9
|
+
* cannot consume.
|
|
10
|
+
*/
|
|
11
|
+
readonly x402Version: v.LiteralSchema<2, undefined>;
|
|
12
|
+
readonly accepts: v.ArraySchema<v.LooseObjectSchema<{
|
|
13
|
+
readonly scheme: v.StringSchema<undefined>;
|
|
14
|
+
readonly network: v.StringSchema<undefined>;
|
|
15
|
+
readonly asset: v.StringSchema<undefined>;
|
|
16
|
+
readonly amount: v.StringSchema<undefined>;
|
|
17
|
+
readonly payTo: v.StringSchema<undefined>;
|
|
18
|
+
readonly maxTimeoutSeconds: v.NumberSchema<undefined>;
|
|
19
|
+
}, undefined>, undefined>;
|
|
20
|
+
readonly resource: v.OptionalSchema<v.LooseObjectSchema<{
|
|
21
|
+
readonly url: v.StringSchema<undefined>;
|
|
22
|
+
readonly serviceName: v.SchemaWithFallback<v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MaxLengthAction<string, 255, undefined>]>, undefined>, undefined>;
|
|
23
|
+
}, undefined>, undefined>;
|
|
24
|
+
}, undefined>;
|
|
25
|
+
type X402PaymentRequired = v.InferOutput<typeof paymentRequiredSchema>;
|
|
26
|
+
/**
|
|
27
|
+
* Decode a 402 response's `PAYMENT-REQUIRED` challenge. Returns undefined for
|
|
28
|
+
* a 402 that carries no decodable v2 challenge (not an x402 endpoint).
|
|
29
|
+
*/
|
|
30
|
+
declare function decodePaymentRequired(response: Response): X402PaymentRequired | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Payment failure raised by this module; `reasons` carries the policy or
|
|
33
|
+
* decline detail when the server provided any, and subclasses mark the cases
|
|
34
|
+
* with a specific recovery path. Not every loop failure is one of these:
|
|
35
|
+
* a create-phase `ApiError` the loop does not absorb as a candidate skip
|
|
36
|
+
* and a "not-submitted" `IntentSubmitError` propagate unchanged.
|
|
37
|
+
*/
|
|
38
|
+
declare class X402PaymentError extends Error {
|
|
39
|
+
readonly reasons: readonly string[];
|
|
40
|
+
constructor(message: string, reasons?: readonly string[]);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Every payable candidate in the challenge targets a network the source
|
|
44
|
+
* account is not on, so nothing could be funded. Distinct from a generic
|
|
45
|
+
* non-payable requirement: the fix is to fund the payment from a wallet
|
|
46
|
+
* account on one of `requiredNetworks`, not to change policy. Consumers can
|
|
47
|
+
* name those networks (and, for a CLI, tell the user how to switch accounts).
|
|
48
|
+
*/
|
|
49
|
+
declare class X402NetworkMismatchError extends X402PaymentError {
|
|
50
|
+
readonly accountId: string;
|
|
51
|
+
readonly requiredNetworks: readonly string[];
|
|
52
|
+
constructor(accountId: string, requiredNetworks: readonly string[]);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The challenge's `payTo` is not a saved counterparty wallet rail, so the
|
|
56
|
+
* allowlist has nothing to authorize and the payment fails closed. The 402's
|
|
57
|
+
* `payTo` is attacker-controlled, so Catena never auto-adds it: the fix is a
|
|
58
|
+
* deliberate `create_counterparty` (itself policy-gated) for `payTo` on
|
|
59
|
+
* `network`, then a re-run. Consumers can name the recipient (and, for a CLI,
|
|
60
|
+
* print the exact counterparty-create command).
|
|
61
|
+
*/
|
|
62
|
+
declare class X402CounterpartyNotFoundError extends X402PaymentError {
|
|
63
|
+
readonly payTo: string;
|
|
64
|
+
readonly network: string;
|
|
65
|
+
/**
|
|
66
|
+
* The challenge's advertised service name, when present — a suggested
|
|
67
|
+
* counterparty name.
|
|
68
|
+
*/
|
|
69
|
+
readonly serviceName: string | undefined;
|
|
70
|
+
constructor(payTo: string, network: string, serviceName?: string);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The payment exceeded a policy limit and is parked for a human approval.
|
|
74
|
+
* Not a decline: once an operator approves it in the console, re-running the
|
|
75
|
+
* same request (same endpoint, same price) consumes the approval and pays.
|
|
76
|
+
* `expiresAt` is when the pending approval self-expires.
|
|
77
|
+
*/
|
|
78
|
+
declare class X402ApprovalPendingError extends X402PaymentError {
|
|
79
|
+
readonly intentId: string;
|
|
80
|
+
readonly expiresAt: string | null;
|
|
81
|
+
constructor(intentId: string, reasons: readonly string[], expiresAt: string | null);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The paid retry failed AFTER the payment settled. The receipt is the
|
|
85
|
+
* caller's recovery handle: retry the request manually with its
|
|
86
|
+
* `paymentSignature` as the `PAYMENT-SIGNATURE` header (also re-readable via
|
|
87
|
+
* `getIntent(intentId)` at `data.x402.paymentSignature` while the
|
|
88
|
+
* authorization is valid) — re-running the whole loop would mint a second
|
|
89
|
+
* payment.
|
|
90
|
+
*/
|
|
91
|
+
declare class X402RetryFailedError extends X402PaymentError {
|
|
92
|
+
readonly receipt: X402PaymentReceipt;
|
|
93
|
+
constructor(receipt: X402PaymentReceipt, cause: unknown);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The submission failed after the intent was created with its outcome
|
|
97
|
+
* unknown — the server may have already claimed and settled (a response lost
|
|
98
|
+
* in transit is the canonical case). Re-running the loop would mint a second
|
|
99
|
+
* intent and risk a double-pay, so the caller must reconcile via
|
|
100
|
+
* `getIntent(intentId)`: if it completed, reuse
|
|
101
|
+
* `data.x402.paymentSignature` as the `PAYMENT-SIGNATURE` header; if
|
|
102
|
+
* processing, it is mid-settlement — keep polling. Pay again only once it is
|
|
103
|
+
* terminally blocked or failed.
|
|
104
|
+
*/
|
|
105
|
+
declare class X402SubmitInterruptedError extends X402PaymentError {
|
|
106
|
+
readonly intentId: string;
|
|
107
|
+
readonly requirements: X402PaymentRequirements;
|
|
108
|
+
constructor(intentId: string, requirements: X402PaymentRequirements, cause: unknown);
|
|
109
|
+
}
|
|
110
|
+
interface X402PaymentReceipt {
|
|
111
|
+
/**
|
|
112
|
+
* The Catena intent that settled the payment; pass to `getIntent`.
|
|
113
|
+
*/
|
|
114
|
+
intentId: string;
|
|
115
|
+
/**
|
|
116
|
+
* Encoded value for the retry's `PAYMENT-SIGNATURE` header. Re-readable
|
|
117
|
+
* via `getIntent(intentId)` at `data.x402.paymentSignature` while the
|
|
118
|
+
* authorization is valid.
|
|
119
|
+
*/
|
|
120
|
+
paymentSignature: string;
|
|
121
|
+
/**
|
|
122
|
+
* The challenge candidate that was paid.
|
|
123
|
+
*/
|
|
124
|
+
requirements: X402PaymentRequirements;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Pay one decoded 402 challenge from `accountId` and return the
|
|
128
|
+
* `PAYMENT-SIGNATURE` header value. Tries the challenge's exact-scheme
|
|
129
|
+
* candidates in order, skipping ones Catena reports as unpayable (wrong
|
|
130
|
+
* network/asset); everything else — policy blocks, counterparty rejections,
|
|
131
|
+
* signing failures — surfaces as `X402PaymentError` (or the underlying
|
|
132
|
+
* `ApiError` / `IntentSubmitError`).
|
|
133
|
+
*/
|
|
134
|
+
declare function payX402Challenge(client: CatenaClient, args: {
|
|
135
|
+
accountId: string;
|
|
136
|
+
paymentRequired: X402PaymentRequired;
|
|
137
|
+
resource?: X402Resource;
|
|
138
|
+
/**
|
|
139
|
+
* Per-call ceiling in atomic USDC units (6 decimals; "1000000" = $1).
|
|
140
|
+
* The org policy's spend limits still govern server-side; this lets the
|
|
141
|
+
* caller refuse a price it never meant to accept, before any intent.
|
|
142
|
+
*/
|
|
143
|
+
maxAtomicAmount?: bigint;
|
|
144
|
+
}): Promise<X402PaymentReceipt>;
|
|
145
|
+
interface X402FetchOptions {
|
|
146
|
+
/**
|
|
147
|
+
* Source wallet account the payments draw from.
|
|
148
|
+
*/
|
|
149
|
+
accountId: string;
|
|
150
|
+
baseFetch?: FetchLike;
|
|
151
|
+
/**
|
|
152
|
+
* Per-call price ceiling in atomic USDC units (6 decimals). Challenges
|
|
153
|
+
* asking for more are refused before any intent is created.
|
|
154
|
+
*/
|
|
155
|
+
maxAtomicAmount?: bigint;
|
|
156
|
+
/**
|
|
157
|
+
* Called after each successful payment, before the retry — for receipts,
|
|
158
|
+
* logging, or spend telemetry.
|
|
159
|
+
*/
|
|
160
|
+
onPayment?: (receipt: X402PaymentReceipt) => void;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Wrap `fetch` so a v2 x402 402 is paid via Catena and retried once with the
|
|
164
|
+
* `PAYMENT-SIGNATURE` header. Non-402 responses (and 402s without a decodable
|
|
165
|
+
* challenge) pass through untouched. Throws `X402PaymentError` before paying
|
|
166
|
+
* when the request body cannot be replayed — pass bodies via the `init`
|
|
167
|
+
* argument as a string, `URLSearchParams`, Blob, ArrayBuffer, or typed
|
|
168
|
+
* array/DataView; a `Request` input must be bodiless. Payment failures surface
|
|
169
|
+
* as `X402PaymentError` (or a subclass documenting its recovery path); an
|
|
170
|
+
* `ApiError` the loop does not absorb as a candidate skip and a "not-submitted"
|
|
171
|
+
* `IntentSubmitError` propagate unchanged.
|
|
172
|
+
*/
|
|
173
|
+
declare function wrapFetchWithX402Payment(client: CatenaClient, options: X402FetchOptions): FetchLike;
|
|
174
|
+
//#endregion
|
|
175
|
+
export { X402ApprovalPendingError, X402CounterpartyNotFoundError, X402FetchOptions, X402NetworkMismatchError, X402PaymentError, X402PaymentReceipt, X402PaymentRequired, type X402PaymentRequirements, type X402Resource, X402RetryFailedError, X402SubmitInterruptedError, decodePaymentRequired, payX402Challenge, wrapFetchWithX402Payment };
|
package/dist/x402.mjs
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { a as x402PaymentRequirementsSchema, n as IntentSubmitError, o as x402ResourceSchema, t as ApiError } from "./client-G-8NfBx_.mjs";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
//#region src/lib/assert-never.ts
|
|
4
|
+
function assertNever(value) {
|
|
5
|
+
throw new Error(`Unhandled case: ${String(value)}`);
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/x402.ts
|
|
9
|
+
const PAYMENT_REQUIRED_HEADERS = ["PAYMENT-REQUIRED", "X-Payment-Required"];
|
|
10
|
+
const PAYMENT_SIGNATURE_HEADER = "PAYMENT-SIGNATURE";
|
|
11
|
+
const PAYMENT_RESPONSE_HEADER = "PAYMENT-RESPONSE";
|
|
12
|
+
const EVM_TX_HASH = /^0x[0-9a-fA-F]{64}$/;
|
|
13
|
+
const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
14
|
+
const paymentResponseSchema = v.looseObject({
|
|
15
|
+
success: v.boolean(),
|
|
16
|
+
transaction: v.string()
|
|
17
|
+
});
|
|
18
|
+
const paymentRequiredSchema = v.looseObject({
|
|
19
|
+
x402Version: v.literal(2),
|
|
20
|
+
accepts: v.array(x402PaymentRequirementsSchema),
|
|
21
|
+
resource: v.optional(x402ResourceSchema)
|
|
22
|
+
});
|
|
23
|
+
const MAX_CANDIDATE_REQUIREMENTS = 5;
|
|
24
|
+
const X402_NETWORK_MISMATCH_CODE = "x402_network_mismatch";
|
|
25
|
+
const X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE = "x402_counterparty_rail_not_found";
|
|
26
|
+
const TRY_NEXT_REQUIREMENT_CODES = new Set([
|
|
27
|
+
"x402_requirement_not_payable",
|
|
28
|
+
X402_NETWORK_MISMATCH_CODE,
|
|
29
|
+
X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE
|
|
30
|
+
]);
|
|
31
|
+
const NETWORK_LABELS = {
|
|
32
|
+
"eip155:8453": "Base",
|
|
33
|
+
"eip155:84532": "Base Sepolia"
|
|
34
|
+
};
|
|
35
|
+
function networkLabel(network) {
|
|
36
|
+
const label = Object.hasOwn(NETWORK_LABELS, network) ? NETWORK_LABELS[network] : void 0;
|
|
37
|
+
return label ? `${label} (${network})` : network;
|
|
38
|
+
}
|
|
39
|
+
function decodeBase64Json(value) {
|
|
40
|
+
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
41
|
+
const bytes = Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0));
|
|
42
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
43
|
+
}
|
|
44
|
+
function decodePaymentRequired(response) {
|
|
45
|
+
const header = PAYMENT_REQUIRED_HEADERS.map((name) => response.headers.get(name)).find((value) => value !== null);
|
|
46
|
+
if (!header) return;
|
|
47
|
+
try {
|
|
48
|
+
const parsed = v.safeParse(paymentRequiredSchema, decodeBase64Json(header));
|
|
49
|
+
return parsed.success ? parsed.output : void 0;
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function reportSettlementHint(client, intentId, retried) {
|
|
55
|
+
try {
|
|
56
|
+
const header = retried.headers.get(PAYMENT_RESPONSE_HEADER);
|
|
57
|
+
if (!header) return;
|
|
58
|
+
const parsed = v.safeParse(paymentResponseSchema, decodeBase64Json(header));
|
|
59
|
+
if (!parsed.success || !parsed.output.success || !EVM_TX_HASH.test(parsed.output.transaction)) return;
|
|
60
|
+
await client.reportSettlement({
|
|
61
|
+
intentId,
|
|
62
|
+
txHash: parsed.output.transaction
|
|
63
|
+
});
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
var X402PaymentError = class extends Error {
|
|
67
|
+
reasons;
|
|
68
|
+
constructor(message, reasons = []) {
|
|
69
|
+
super(reasons.length > 0 ? `${message}: ${reasons.join("; ")}` : message);
|
|
70
|
+
this.name = "X402PaymentError";
|
|
71
|
+
this.reasons = reasons;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var X402NetworkMismatchError = class extends X402PaymentError {
|
|
75
|
+
accountId;
|
|
76
|
+
requiredNetworks;
|
|
77
|
+
constructor(accountId, requiredNetworks) {
|
|
78
|
+
super(`Account ${accountId} cannot fund this 402 challenge: it must be paid from a wallet account on ${requiredNetworks.map(networkLabel).join(" or ")}`);
|
|
79
|
+
this.name = "X402NetworkMismatchError";
|
|
80
|
+
this.accountId = accountId;
|
|
81
|
+
this.requiredNetworks = requiredNetworks;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var X402CounterpartyNotFoundError = class extends X402PaymentError {
|
|
85
|
+
payTo;
|
|
86
|
+
network;
|
|
87
|
+
serviceName;
|
|
88
|
+
constructor(payTo, network, serviceName) {
|
|
89
|
+
super(`This 402 pays ${payTo} on ${networkLabel(network)}, which is not a saved counterparty wallet rail; add it as a counterparty before paying`);
|
|
90
|
+
this.name = "X402CounterpartyNotFoundError";
|
|
91
|
+
this.payTo = payTo;
|
|
92
|
+
this.network = network;
|
|
93
|
+
this.serviceName = serviceName;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var X402ApprovalPendingError = class extends X402PaymentError {
|
|
97
|
+
intentId;
|
|
98
|
+
expiresAt;
|
|
99
|
+
constructor(intentId, reasons, expiresAt) {
|
|
100
|
+
super(`The payment is awaiting a human approval (intent ${intentId}); once an operator approves it, retry the request to complete the payment`, reasons);
|
|
101
|
+
this.name = "X402ApprovalPendingError";
|
|
102
|
+
this.intentId = intentId;
|
|
103
|
+
this.expiresAt = expiresAt;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var X402RetryFailedError = class extends X402PaymentError {
|
|
107
|
+
receipt;
|
|
108
|
+
constructor(receipt, cause) {
|
|
109
|
+
super(`The payment settled (intent ${receipt.intentId}) but the paid retry failed; retry the request with the PAYMENT-SIGNATURE header from the receipt (or getIntent("${receipt.intentId}") data.x402.paymentSignature) instead of paying again`);
|
|
110
|
+
this.name = "X402RetryFailedError";
|
|
111
|
+
this.receipt = receipt;
|
|
112
|
+
this.cause = cause;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var X402SubmitInterruptedError = class extends X402PaymentError {
|
|
116
|
+
intentId;
|
|
117
|
+
requirements;
|
|
118
|
+
constructor(intentId, requirements, cause) {
|
|
119
|
+
super(`The payment could not be confirmed (intent ${intentId}); check getIntent("${intentId}") — completed: reuse its data.x402.paymentSignature as the PAYMENT-SIGNATURE header; processing: poll until terminal; pay again only after blocked or failed`);
|
|
120
|
+
this.name = "X402SubmitInterruptedError";
|
|
121
|
+
this.intentId = intentId;
|
|
122
|
+
this.requirements = requirements;
|
|
123
|
+
this.cause = cause;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
async function payX402Challenge(client, args) {
|
|
127
|
+
const exactCandidates = args.paymentRequired.accepts.filter((requirement) => requirement.scheme === "exact");
|
|
128
|
+
const candidates = exactCandidates.filter((requirement) => args.maxAtomicAmount === void 0 || /^\d+$/.test(requirement.amount) && BigInt(requirement.amount) <= args.maxAtomicAmount).slice(0, MAX_CANDIDATE_REQUIREMENTS);
|
|
129
|
+
if (candidates.length === 0) throw new X402PaymentError(exactCandidates.length > 0 && args.maxAtomicAmount !== void 0 ? "Every requirement in the 402 challenge costs more than the caller's maximum amount" : "The 402 challenge offers no exact-scheme requirement Catena can pay");
|
|
130
|
+
const resource = args.resource ?? args.paymentRequired.resource;
|
|
131
|
+
const skips = [];
|
|
132
|
+
for (const requirements of candidates) {
|
|
133
|
+
const receipt = await payRequirement(client, {
|
|
134
|
+
accountId: args.accountId,
|
|
135
|
+
requirements,
|
|
136
|
+
resource
|
|
137
|
+
}).catch((err) => {
|
|
138
|
+
if (err instanceof ApiError && err.code !== void 0 && TRY_NEXT_REQUIREMENT_CODES.has(err.code)) {
|
|
139
|
+
skips.push({
|
|
140
|
+
error: err,
|
|
141
|
+
requirement: requirements
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
throw err;
|
|
146
|
+
});
|
|
147
|
+
if (receipt) return receipt;
|
|
148
|
+
}
|
|
149
|
+
if (skips.length > 0 && skips.every((skip) => skip.error.code === X402_NETWORK_MISMATCH_CODE)) throw new X402NetworkMismatchError(args.accountId, [...new Set(skips.map((skip) => skip.requirement.network))]);
|
|
150
|
+
if (skips.length > 0 && skips.every((skip) => skip.error.code === X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE && skip.error.status === 404) && EVM_ADDRESS.test(skips[0].requirement.payTo)) throw new X402CounterpartyNotFoundError(skips[0].requirement.payTo, skips[0].requirement.network, resource?.serviceName);
|
|
151
|
+
const lastSkip = skips.at(-1);
|
|
152
|
+
throw new X402PaymentError("No requirement in the 402 challenge is payable from this account", lastSkip ? [lastSkip.error.message] : []);
|
|
153
|
+
}
|
|
154
|
+
const x402IntentDataSchema = v.object({ x402: v.object({ paymentSignature: v.string() }) });
|
|
155
|
+
async function payRequirement(client, args) {
|
|
156
|
+
const intent = await client.submitIntent({
|
|
157
|
+
action: {
|
|
158
|
+
type: "x402",
|
|
159
|
+
accountId: args.accountId,
|
|
160
|
+
paymentRequirements: args.requirements,
|
|
161
|
+
...args.resource !== void 0 && { resource: args.resource }
|
|
162
|
+
},
|
|
163
|
+
idempotencyKey: crypto.randomUUID()
|
|
164
|
+
}).catch((err) => {
|
|
165
|
+
if (err instanceof IntentSubmitError && err.outcome === "unknown") throw new X402SubmitInterruptedError(err.intentId, args.requirements, err);
|
|
166
|
+
throw err;
|
|
167
|
+
});
|
|
168
|
+
if (intent.status === "completed") {
|
|
169
|
+
const data = v.safeParse(x402IntentDataSchema, intent.data);
|
|
170
|
+
if (data.success) return {
|
|
171
|
+
intentId: intent.id,
|
|
172
|
+
paymentSignature: data.output.x402.paymentSignature,
|
|
173
|
+
requirements: args.requirements
|
|
174
|
+
};
|
|
175
|
+
throw new X402PaymentError("The completed payment did not carry an x402 payment signature");
|
|
176
|
+
}
|
|
177
|
+
switch (intent.status) {
|
|
178
|
+
case "pending": throw new X402ApprovalPendingError(intent.id, intent.reasons, intent.expiresAt);
|
|
179
|
+
case "processing": throw new X402PaymentError(`The payment is still processing (intent ${intent.id}); this client cannot resume an in-flight x402 settlement`);
|
|
180
|
+
case "blocked":
|
|
181
|
+
case "failed": throw new X402PaymentError("Catena declined the payment", intent.reasons);
|
|
182
|
+
}
|
|
183
|
+
return assertNever(intent.status);
|
|
184
|
+
}
|
|
185
|
+
function isReplayable(input, init) {
|
|
186
|
+
if (init?.body !== void 0 && init.body !== null) {
|
|
187
|
+
if (typeof init.body !== "string" && !(init.body instanceof URLSearchParams) && !(init.body instanceof Blob) && !(init.body instanceof ArrayBuffer) && !ArrayBuffer.isView(init.body)) return false;
|
|
188
|
+
}
|
|
189
|
+
if (typeof Request !== "undefined" && input instanceof Request) return input.body === null && !input.bodyUsed;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
function wrapFetchWithX402Payment(client, options) {
|
|
193
|
+
const baseFetch = (options.baseFetch ?? globalThis.fetch).bind(globalThis);
|
|
194
|
+
return async (input, init) => {
|
|
195
|
+
const response = await baseFetch(input, init);
|
|
196
|
+
if (response.status !== 402) return response;
|
|
197
|
+
const paymentRequired = decodePaymentRequired(response);
|
|
198
|
+
if (!paymentRequired) return response;
|
|
199
|
+
if (!isReplayable(input, init)) throw new X402PaymentError("The request body cannot be replayed after payment; buffer the body (string, Blob, or ArrayBuffer) before using the x402 fetch wrapper");
|
|
200
|
+
const receipt = await payX402Challenge(client, {
|
|
201
|
+
accountId: options.accountId,
|
|
202
|
+
paymentRequired,
|
|
203
|
+
...options.maxAtomicAmount !== void 0 && { maxAtomicAmount: options.maxAtomicAmount }
|
|
204
|
+
});
|
|
205
|
+
options.onPayment?.(receipt);
|
|
206
|
+
const headers = new Headers(typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
|
|
207
|
+
new Headers(init?.headers).forEach((value, key) => {
|
|
208
|
+
headers.set(key, value);
|
|
209
|
+
});
|
|
210
|
+
headers.set(PAYMENT_SIGNATURE_HEADER, receipt.paymentSignature);
|
|
211
|
+
let retried;
|
|
212
|
+
try {
|
|
213
|
+
retried = await baseFetch(input, {
|
|
214
|
+
...init,
|
|
215
|
+
headers
|
|
216
|
+
});
|
|
217
|
+
} catch (err) {
|
|
218
|
+
throw new X402RetryFailedError(receipt, err);
|
|
219
|
+
}
|
|
220
|
+
reportSettlementHint(client, receipt.intentId, retried);
|
|
221
|
+
return retried;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
export { X402ApprovalPendingError, X402CounterpartyNotFoundError, X402NetworkMismatchError, X402PaymentError, X402RetryFailedError, X402SubmitInterruptedError, decodePaymentRequired, payX402Challenge, wrapFetchWithX402Payment };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@catena/sdk",
|
|
3
|
+
"version": "0.0.0-alpha-20260724191736",
|
|
4
|
+
"description": "Typed client SDK for the Catena agent API",
|
|
5
|
+
"homepage": "https://catena.com/",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/client.d.mts",
|
|
14
|
+
"default": "./dist/client.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./keypair": {
|
|
17
|
+
"types": "./dist/keypair.d.mts",
|
|
18
|
+
"default": "./dist/keypair.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./x402": {
|
|
21
|
+
"types": "./dist/x402.d.mts",
|
|
22
|
+
"default": "./dist/x402.mjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"tag": "alpha"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@turnkey/api-key-stamper": "0.6.7",
|
|
31
|
+
"valibot": "1.4.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"publint": "0.3.21",
|
|
35
|
+
"tsdown": "0.22.2"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"gitHead": "1e4e40d0767efc5d927803fec08eaea24035017d",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown",
|
|
43
|
+
"clean": "git clean -xdf dist node_modules/.cache",
|
|
44
|
+
"publint": "pnpm run build && publint --pack pnpm --strict"
|
|
45
|
+
}
|
|
46
|
+
}
|