@catena/sdk 0.0.0-bootstrap.0 → 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 +191 -0
- package/README.md +479 -2
- package/dist/client-CHZMO00P.mjs +857 -0
- package/dist/client-Cm2wNUuB.d.mts +1096 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.mjs +3 -0
- package/dist/keypair-BjMJtI3-.d.mts +35 -0
- package/dist/keypair.d.mts +2 -0
- package/dist/keypair.mjs +50 -0
- package/dist/mpp.d.mts +315 -0
- package/dist/mpp.mjs +564 -0
- package/dist/settlement-report-CT3EbtLL.mjs +36 -0
- package/dist/viem.d.mts +135 -0
- package/dist/viem.mjs +301 -0
- package/dist/x402.d.mts +206 -0
- package/dist/x402.mjs +221 -0
- package/package.json +64 -5
package/dist/viem.d.mts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { F as OnchainNetworkInput, U as X402Authorization, W as X402PaymentRequirements, a as IntentSubmitError, n as CatenaClient, t as ApiError } from "./client-Cm2wNUuB.mjs";
|
|
2
|
+
import { X402ApprovalPendingError, X402PaymentError, X402SubmitInterruptedError } from "./x402.mjs";
|
|
3
|
+
import { LocalAccount } from "viem";
|
|
4
|
+
//#region src/viem.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The account was asked for a capability a custodial Catena wallet does not
|
|
7
|
+
* have. The wallet's signing policy authorizes only EIP-3009
|
|
8
|
+
* `TransferWithAuthorization` typed data; `signMessage`, `signTransaction`,
|
|
9
|
+
* and raw-hash signing are structurally unavailable.
|
|
10
|
+
*/
|
|
11
|
+
declare class UnsupportedAccountCapabilityError extends Error {
|
|
12
|
+
readonly capability: "signMessage" | "signTransaction";
|
|
13
|
+
constructor(capability: "signMessage" | "signTransaction");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Machine-readable reason a typed-data payload was refused before any
|
|
17
|
+
* request was made. Branch on `reason`, not message prose.
|
|
18
|
+
*/
|
|
19
|
+
type UnsupportedTypedDataReason = "primary_type" | "types" | "network" | "asset" | "domain" | "message" | "nonce" | "from_mismatch" | "recipient" | "expired" | "window";
|
|
20
|
+
/**
|
|
21
|
+
* The typed data is not something a Catena wallet can sign. Thrown
|
|
22
|
+
* client-side, before any intent exists; nothing was submitted or paid.
|
|
23
|
+
*/
|
|
24
|
+
declare class UnsupportedTypedDataError extends Error {
|
|
25
|
+
readonly reason: UnsupportedTypedDataReason;
|
|
26
|
+
constructor(reason: UnsupportedTypedDataReason, message: string);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The account id resolves to a liquidation deposit address rather than the
|
|
30
|
+
* wallet's own address. x402 authorizations can only be signed for a wallet
|
|
31
|
+
* account; pick an account whose deposit address source is `wallet`.
|
|
32
|
+
*/
|
|
33
|
+
declare class NotAWalletAccountError extends Error {
|
|
34
|
+
readonly accountId: string;
|
|
35
|
+
readonly source: string;
|
|
36
|
+
constructor(accountId: string, source: string);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Catena declined the payment (policy block or terminal failure). `reasons`
|
|
40
|
+
* carries the server's detail; the same struct will not succeed on retry.
|
|
41
|
+
*/
|
|
42
|
+
declare class X402PaymentDeclinedError extends X402PaymentError {
|
|
43
|
+
readonly intentId: string;
|
|
44
|
+
constructor(intentId: string, reasons: readonly string[]);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The payment was accepted and is still in flight, which a synchronous
|
|
48
|
+
* `signTypedData` cannot resume. Do not pay again on the strength of this:
|
|
49
|
+
* reconcile the intent instead. Note that polling does not advance an
|
|
50
|
+
* approved x402 payment resting in this state — it waits for the paid
|
|
51
|
+
* request to be re-run.
|
|
52
|
+
*/
|
|
53
|
+
declare class X402PaymentInFlightError extends X402PaymentError {
|
|
54
|
+
readonly intentId: string;
|
|
55
|
+
constructor(intentId: string, status: string);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The payment completed but the signature it carried cannot be used — absent,
|
|
59
|
+
* or an envelope this SDK could not decode. The money has moved, so this is
|
|
60
|
+
* not a retry: reconcile the intent before paying again. Re-reading it will
|
|
61
|
+
* return the same unusable value, so recovery here means reconciling the
|
|
62
|
+
* payment with the operator, not fetching the signature again.
|
|
63
|
+
*/
|
|
64
|
+
declare class X402PaymentSignatureUnusableError extends X402PaymentError {
|
|
65
|
+
readonly intentId: string;
|
|
66
|
+
constructor(intentId: string, message: string);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The completed payment's signature did not recover to the wallet address
|
|
70
|
+
* for the typed data that was submitted. The signature must not be used;
|
|
71
|
+
* this indicates server-side drift and is worth reporting.
|
|
72
|
+
*
|
|
73
|
+
* An `X402PaymentError` because it is thrown after the payment completed:
|
|
74
|
+
* money has moved, so it belongs to the family a caller branches on before
|
|
75
|
+
* deciding whether to pay again. Reconcile with `getIntent(intentId)`.
|
|
76
|
+
*/
|
|
77
|
+
declare class SignatureVerificationFailedError extends X402PaymentError {
|
|
78
|
+
readonly intentId: string;
|
|
79
|
+
readonly expectedAddress: string;
|
|
80
|
+
readonly recoveredAddress: string | undefined;
|
|
81
|
+
constructor(intentId: string, expectedAddress: string, recoveredAddress: string | undefined);
|
|
82
|
+
}
|
|
83
|
+
interface CatenaAccountOptions {
|
|
84
|
+
/**
|
|
85
|
+
* The wallet account payments draw from. Its deposit address must resolve
|
|
86
|
+
* with source `wallet`.
|
|
87
|
+
*/
|
|
88
|
+
accountId: string;
|
|
89
|
+
/**
|
|
90
|
+
* Network used to resolve the wallet address once at creation, defaulting
|
|
91
|
+
* to `base`. It must be the network the wallet account is configured for:
|
|
92
|
+
* a wallet accepts deposits on exactly one network, and asking for another
|
|
93
|
+
* fails the call rather than falling back. Pass `base-sepolia` explicitly
|
|
94
|
+
* for a testnet wallet. The same network then bounds what the account can
|
|
95
|
+
* pay: typed data naming the other supported chain is refused server-side,
|
|
96
|
+
* because the account cannot fund a payment off its own network.
|
|
97
|
+
*/
|
|
98
|
+
network?: OnchainNetworkInput;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The account shape `createCatenaAccount` resolves to: a viem local account
|
|
102
|
+
* whose only working capability is `signTypedData` for x402 EIP-3009
|
|
103
|
+
* payment authorizations.
|
|
104
|
+
*/
|
|
105
|
+
type CatenaAccount = LocalAccount;
|
|
106
|
+
/**
|
|
107
|
+
* Present a Catena wallet as a viem `Account` for x402 client tooling.
|
|
108
|
+
*
|
|
109
|
+
* The returned account's `signTypedData` accepts exactly one shape — EIP-3009
|
|
110
|
+
* `TransferWithAuthorization` for USDC on Base or Base Sepolia, the payload
|
|
111
|
+
* x402 exact-scheme clients construct — and routes it through a Catena x402
|
|
112
|
+
* intent: policy evaluates the payment, the custodial key signs the caller's
|
|
113
|
+
* exact struct, and the signature is verified to recover to the wallet
|
|
114
|
+
* address before it is returned. `signMessage` and `signTransaction` throw
|
|
115
|
+
* `UnsupportedAccountCapabilityError`; there is no raw-hash `sign`.
|
|
116
|
+
*
|
|
117
|
+
* Failure modes: `UnsupportedTypedDataError` (refused client-side, nothing
|
|
118
|
+
* submitted), `X402ApprovalPendingError` (parked for a human approval —
|
|
119
|
+
* approve in the console, then have the tooling retry; a retry with a fresh
|
|
120
|
+
* nonce still consumes the approval), `X402PaymentDeclinedError` (policy
|
|
121
|
+
* block or terminal failure), and — all of these after the payment may have
|
|
122
|
+
* moved, so reconcile with `getIntent(intentId)` rather than paying again —
|
|
123
|
+
* `SignatureVerificationFailedError` (the returned signature did not verify
|
|
124
|
+
* and was withheld), `X402SubmitInterruptedError`,
|
|
125
|
+
* `X402PaymentSignatureUnusableError`, and `X402PaymentInFlightError`. Then
|
|
126
|
+
* `IntentSubmitError` with `outcome: "not-submitted"` (the
|
|
127
|
+
* intent exists but nothing was submitted and no money moved, so a retry is
|
|
128
|
+
* safe), and `ApiError` for request-level failures such as a reused nonce.
|
|
129
|
+
*
|
|
130
|
+
* Requires the optional `viem` peer dependency; importing this module
|
|
131
|
+
* without viem installed fails with a module-not-found error naming it.
|
|
132
|
+
*/
|
|
133
|
+
declare function createCatenaAccount(client: CatenaClient, options: CatenaAccountOptions): Promise<CatenaAccount>;
|
|
134
|
+
//#endregion
|
|
135
|
+
export { ApiError, CatenaAccount, CatenaAccountOptions, IntentSubmitError, NotAWalletAccountError, type OnchainNetworkInput, SignatureVerificationFailedError, UnsupportedAccountCapabilityError, UnsupportedTypedDataError, UnsupportedTypedDataReason, X402ApprovalPendingError, type X402Authorization, X402PaymentDeclinedError, X402PaymentError, X402PaymentInFlightError, type X402PaymentRequirements, X402PaymentSignatureUnusableError, X402SubmitInterruptedError, createCatenaAccount };
|
package/dist/viem.mjs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { b as x402CredentialFromIntentData, n as IntentSubmitError, t as ApiError } from "./client-CHZMO00P.mjs";
|
|
2
|
+
import { X402ApprovalPendingError, X402PaymentError, X402SubmitInterruptedError } from "./x402.mjs";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
import { getAddress, isAddress, isHex, recoverTypedDataAddress } from "viem";
|
|
5
|
+
import { toAccount } from "viem/accounts";
|
|
6
|
+
//#region src/viem.ts
|
|
7
|
+
const NETWORKS = {
|
|
8
|
+
"8453": {
|
|
9
|
+
network: "eip155:8453",
|
|
10
|
+
usdc: getAddress("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
|
|
11
|
+
},
|
|
12
|
+
"84532": {
|
|
13
|
+
network: "eip155:84532",
|
|
14
|
+
usdc: getAddress("0x036CbD53842c5426634e7929541eC2318f3dCF7e")
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const EIP712_DOMAIN_TYPE = [
|
|
18
|
+
{
|
|
19
|
+
name: "name",
|
|
20
|
+
type: "string"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "version",
|
|
24
|
+
type: "string"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "chainId",
|
|
28
|
+
type: "uint256"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "verifyingContract",
|
|
32
|
+
type: "address"
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
const CANONICAL_DOMAIN_FIELDS = new Set([
|
|
36
|
+
"name",
|
|
37
|
+
"version",
|
|
38
|
+
"chainId",
|
|
39
|
+
"verifyingContract"
|
|
40
|
+
]);
|
|
41
|
+
const TRANSFER_WITH_AUTHORIZATION_TYPES = { TransferWithAuthorization: [
|
|
42
|
+
{
|
|
43
|
+
name: "from",
|
|
44
|
+
type: "address"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "to",
|
|
48
|
+
type: "address"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "value",
|
|
52
|
+
type: "uint256"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "validAfter",
|
|
56
|
+
type: "uint256"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "validBefore",
|
|
60
|
+
type: "uint256"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "nonce",
|
|
64
|
+
type: "bytes32"
|
|
65
|
+
}
|
|
66
|
+
] };
|
|
67
|
+
const MIN_REMAINING_VALIDITY_SECONDS = 5;
|
|
68
|
+
const MAX_TIMEOUT_SECONDS = 3600;
|
|
69
|
+
const VALIDBEFORE_SKEW_SECONDS = 30;
|
|
70
|
+
var UnsupportedAccountCapabilityError = class extends Error {
|
|
71
|
+
capability;
|
|
72
|
+
constructor(capability) {
|
|
73
|
+
super(`A Catena wallet is custodial and its signing policy authorizes only EIP-3009 TransferWithAuthorization typed data; ${capability} is not available. Use signTypedData with an x402 payment authorization.`);
|
|
74
|
+
this.name = "UnsupportedAccountCapabilityError";
|
|
75
|
+
this.capability = capability;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var UnsupportedTypedDataError = class extends Error {
|
|
79
|
+
reason;
|
|
80
|
+
constructor(reason, message) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.name = "UnsupportedTypedDataError";
|
|
83
|
+
this.reason = reason;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var NotAWalletAccountError = class extends Error {
|
|
87
|
+
accountId;
|
|
88
|
+
source;
|
|
89
|
+
constructor(accountId, source) {
|
|
90
|
+
super(`Account ${accountId} resolves to a ${source} deposit address, not the wallet's own address; x402 authorizations can only be signed for a wallet account`);
|
|
91
|
+
this.name = "NotAWalletAccountError";
|
|
92
|
+
this.accountId = accountId;
|
|
93
|
+
this.source = source;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var X402PaymentDeclinedError = class extends X402PaymentError {
|
|
97
|
+
intentId;
|
|
98
|
+
constructor(intentId, reasons) {
|
|
99
|
+
super(`Catena declined the payment (intent ${intentId})`, reasons);
|
|
100
|
+
this.name = "X402PaymentDeclinedError";
|
|
101
|
+
this.intentId = intentId;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var X402PaymentInFlightError = class extends X402PaymentError {
|
|
105
|
+
intentId;
|
|
106
|
+
constructor(intentId, status) {
|
|
107
|
+
super(`The payment is still ${status} (intent ${intentId}); this client cannot resume an in-flight x402 settlement. Reconcile with getIntent("${intentId}") rather than paying again`);
|
|
108
|
+
this.name = "X402PaymentInFlightError";
|
|
109
|
+
this.intentId = intentId;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var X402PaymentSignatureUnusableError = class extends X402PaymentError {
|
|
113
|
+
intentId;
|
|
114
|
+
constructor(intentId, message) {
|
|
115
|
+
super(`${message} (intent ${intentId}); the payment may have settled — check getIntent("${intentId}") before paying again`);
|
|
116
|
+
this.name = "X402PaymentSignatureUnusableError";
|
|
117
|
+
this.intentId = intentId;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var SignatureVerificationFailedError = class extends X402PaymentError {
|
|
121
|
+
intentId;
|
|
122
|
+
expectedAddress;
|
|
123
|
+
recoveredAddress;
|
|
124
|
+
constructor(intentId, expectedAddress, recoveredAddress) {
|
|
125
|
+
super(`The signature for intent ${intentId} did not recover to the wallet address; do not use it. The payment may have settled — check getIntent("${intentId}") before paying again`);
|
|
126
|
+
this.name = "SignatureVerificationFailedError";
|
|
127
|
+
this.intentId = intentId;
|
|
128
|
+
this.expectedAddress = expectedAddress;
|
|
129
|
+
this.recoveredAddress = recoveredAddress;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const MAX_UINT256 = 2n ** 256n - 1n;
|
|
133
|
+
const uint256Schema = v.pipe(v.union([
|
|
134
|
+
v.pipe(v.string(), v.regex(/^(0|[1-9]\d*)$/), v.maxLength(78)),
|
|
135
|
+
v.pipe(v.number(), v.check((value) => Number.isSafeInteger(value) && value >= 0)),
|
|
136
|
+
v.pipe(v.bigint(), v.check((value) => value >= 0n))
|
|
137
|
+
]), v.check((value) => {
|
|
138
|
+
const decimal = toDecimalString(value);
|
|
139
|
+
return /^\d+$/.test(decimal) && BigInt(decimal) <= MAX_UINT256;
|
|
140
|
+
}));
|
|
141
|
+
const typedDataInputSchema = v.looseObject({
|
|
142
|
+
primaryType: v.string(),
|
|
143
|
+
domain: v.looseObject({
|
|
144
|
+
name: v.optional(v.string()),
|
|
145
|
+
version: v.optional(v.string()),
|
|
146
|
+
chainId: v.optional(v.union([v.number(), v.bigint()])),
|
|
147
|
+
verifyingContract: v.optional(v.string())
|
|
148
|
+
}),
|
|
149
|
+
types: v.record(v.string(), v.unknown()),
|
|
150
|
+
message: v.looseObject({
|
|
151
|
+
from: v.string(),
|
|
152
|
+
to: v.string(),
|
|
153
|
+
value: uint256Schema,
|
|
154
|
+
validAfter: uint256Schema,
|
|
155
|
+
validBefore: uint256Schema,
|
|
156
|
+
nonce: v.string()
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
const paymentSignatureEnvelopeSchema = v.looseObject({ payload: v.looseObject({ signature: v.string() }) });
|
|
160
|
+
function decodeBase64Json(value) {
|
|
161
|
+
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
162
|
+
try {
|
|
163
|
+
const bytes = Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0));
|
|
164
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function toDecimalString(value) {
|
|
170
|
+
return typeof value === "string" ? value : value.toString();
|
|
171
|
+
}
|
|
172
|
+
const typeTableSchema = v.array(v.looseObject({
|
|
173
|
+
name: v.string(),
|
|
174
|
+
type: v.string()
|
|
175
|
+
}));
|
|
176
|
+
function encodeTypeTable(value) {
|
|
177
|
+
const parsed = v.safeParse(typeTableSchema, value);
|
|
178
|
+
return parsed.success ? JSON.stringify(parsed.output.map((entry) => [entry.name, entry.type])) : void 0;
|
|
179
|
+
}
|
|
180
|
+
function encodeCanonicalTable(table) {
|
|
181
|
+
return JSON.stringify(table.map((entry) => [entry.name, entry.type]));
|
|
182
|
+
}
|
|
183
|
+
const CANONICAL_TRANSFER_TYPE = encodeCanonicalTable(TRANSFER_WITH_AUTHORIZATION_TYPES.TransferWithAuthorization);
|
|
184
|
+
const CANONICAL_DOMAIN_TYPE = encodeCanonicalTable(EIP712_DOMAIN_TYPE);
|
|
185
|
+
function isEvmAddress(value) {
|
|
186
|
+
return isAddress(value);
|
|
187
|
+
}
|
|
188
|
+
async function createCatenaAccount(client, options) {
|
|
189
|
+
const deposit = await client.getAccountDepositAddress(options.accountId, {
|
|
190
|
+
network: options.network ?? "base",
|
|
191
|
+
asset: "usdc"
|
|
192
|
+
});
|
|
193
|
+
if (deposit.source !== "wallet") throw new NotAWalletAccountError(options.accountId, deposit.source);
|
|
194
|
+
const address = getAddress(deposit.address);
|
|
195
|
+
return toAccount({
|
|
196
|
+
address,
|
|
197
|
+
signMessage: () => Promise.reject(new UnsupportedAccountCapabilityError("signMessage")),
|
|
198
|
+
signTransaction: () => Promise.reject(new UnsupportedAccountCapabilityError("signTransaction")),
|
|
199
|
+
signTypedData: (parameters) => signX402TypedData(client, {
|
|
200
|
+
accountId: options.accountId,
|
|
201
|
+
address,
|
|
202
|
+
parameters
|
|
203
|
+
})
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async function signX402TypedData(client, args) {
|
|
207
|
+
const parsed = v.safeParse(typedDataInputSchema, args.parameters);
|
|
208
|
+
if (!parsed.success) throw new UnsupportedTypedDataError("message", "The typed data is not a TransferWithAuthorization payload this account can sign");
|
|
209
|
+
const { primaryType, domain, types, message } = parsed.output;
|
|
210
|
+
if (primaryType !== "TransferWithAuthorization") throw new UnsupportedTypedDataError("primary_type", `This account signs only TransferWithAuthorization typed data; got ${primaryType}`);
|
|
211
|
+
const transferType = types["TransferWithAuthorization"];
|
|
212
|
+
if (encodeTypeTable(transferType) !== CANONICAL_TRANSFER_TYPE) throw new UnsupportedTypedDataError("types", "The TransferWithAuthorization type definition does not match EIP-3009");
|
|
213
|
+
if (domain.chainId === void 0) throw new UnsupportedTypedDataError("network", "The typed data names no chainId");
|
|
214
|
+
const chain = Object.hasOwn(NETWORKS, String(domain.chainId)) ? NETWORKS[String(domain.chainId)] : void 0;
|
|
215
|
+
if (!chain) throw new UnsupportedTypedDataError("network", `Chain ${String(domain.chainId)} is not supported; use Base (8453) or Base Sepolia (84532)`);
|
|
216
|
+
if (domain.verifyingContract === void 0 || !isEvmAddress(domain.verifyingContract) || getAddress(domain.verifyingContract) !== chain.usdc) throw new UnsupportedTypedDataError("asset", `The verifying contract must be USDC ${chain.usdc} on chain ${String(domain.chainId)}`);
|
|
217
|
+
if (!domain.name || !domain.version) throw new UnsupportedTypedDataError("domain", "The typed-data domain must carry the token's name and version");
|
|
218
|
+
const unsupportedDomainFields = Object.entries(domain).filter(([key, value]) => !CANONICAL_DOMAIN_FIELDS.has(key) && value !== void 0).map(([key]) => key);
|
|
219
|
+
if (unsupportedDomainFields.length > 0) throw new UnsupportedTypedDataError("domain", `The typed-data domain carries fields this account cannot sign over: ${unsupportedDomainFields.join(", ")}`);
|
|
220
|
+
const domainType = types["EIP712Domain"];
|
|
221
|
+
if (domainType !== void 0 && encodeTypeTable(domainType) !== CANONICAL_DOMAIN_TYPE) throw new UnsupportedTypedDataError("domain", "The EIP712Domain type definition must be the canonical name, version, chainId, verifyingContract");
|
|
222
|
+
const nonce = message.nonce.toLowerCase();
|
|
223
|
+
if (!/^0x[0-9a-f]{64}$/.test(nonce) || !isHex(nonce)) throw new UnsupportedTypedDataError("nonce", "The nonce must be 32 bytes of hex");
|
|
224
|
+
if (!isEvmAddress(message.from) || getAddress(message.from) !== args.address) throw new UnsupportedTypedDataError("from_mismatch", `The authorization must draw from this account's wallet address ${args.address}`);
|
|
225
|
+
if (!isEvmAddress(message.to)) throw new UnsupportedTypedDataError("recipient", "The authorization's recipient must be a 20-byte hex address, either all-lowercase or with a valid EIP-55 checksum");
|
|
226
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
227
|
+
const validAfter = BigInt(toDecimalString(message.validAfter));
|
|
228
|
+
const validBefore = BigInt(toDecimalString(message.validBefore));
|
|
229
|
+
if (validBefore <= BigInt(nowSeconds + MIN_REMAINING_VALIDITY_SECONDS)) throw new UnsupportedTypedDataError("expired", "The authorization expires too soon to settle; build one with more validity");
|
|
230
|
+
if (validAfter >= validBefore) throw new UnsupportedTypedDataError("window", "The authorization expires before it becomes valid");
|
|
231
|
+
if (validAfter > BigInt(nowSeconds)) throw new UnsupportedTypedDataError("window", "The authorization must already be valid; it starts in the future");
|
|
232
|
+
if (validBefore > BigInt(nowSeconds + MAX_TIMEOUT_SECONDS + VALIDBEFORE_SKEW_SECONDS)) throw new UnsupportedTypedDataError("window", `The authorization is valid for longer than the ${MAX_TIMEOUT_SECONDS}s a payment may be authorized for`);
|
|
233
|
+
const authorization = {
|
|
234
|
+
from: message.from,
|
|
235
|
+
to: message.to,
|
|
236
|
+
value: toDecimalString(message.value),
|
|
237
|
+
validAfter: toDecimalString(message.validAfter),
|
|
238
|
+
validBefore: toDecimalString(message.validBefore),
|
|
239
|
+
nonce
|
|
240
|
+
};
|
|
241
|
+
const paymentRequirements = {
|
|
242
|
+
scheme: "exact",
|
|
243
|
+
network: chain.network,
|
|
244
|
+
asset: getAddress(domain.verifyingContract),
|
|
245
|
+
amount: authorization.value,
|
|
246
|
+
payTo: message.to,
|
|
247
|
+
maxTimeoutSeconds: Math.min(MAX_TIMEOUT_SECONDS, Math.max(1, Number(validBefore - validAfter))),
|
|
248
|
+
extra: {
|
|
249
|
+
name: domain.name,
|
|
250
|
+
version: domain.version
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const intent = await client.submitIntent({
|
|
254
|
+
action: {
|
|
255
|
+
type: "x402",
|
|
256
|
+
accountId: args.accountId,
|
|
257
|
+
paymentRequirements,
|
|
258
|
+
authorization
|
|
259
|
+
},
|
|
260
|
+
idempotencyKey: crypto.randomUUID()
|
|
261
|
+
}).catch((err) => {
|
|
262
|
+
if (err instanceof IntentSubmitError && err.outcome === "unknown") throw new X402SubmitInterruptedError(err.intentId, paymentRequirements, err);
|
|
263
|
+
throw err;
|
|
264
|
+
});
|
|
265
|
+
if (intent.status === "pending") throw new X402ApprovalPendingError(intent.id, intent.reasons, intent.expiresAt);
|
|
266
|
+
if (intent.status === "blocked" || intent.status === "failed") throw new X402PaymentDeclinedError(intent.id, intent.reasons);
|
|
267
|
+
if (intent.status !== "completed") throw new X402PaymentInFlightError(intent.id, intent.status);
|
|
268
|
+
const paymentSignature = x402CredentialFromIntentData(intent.data);
|
|
269
|
+
if (paymentSignature === void 0) throw new X402PaymentSignatureUnusableError(intent.id, "The completed payment did not carry an x402 payment signature");
|
|
270
|
+
const envelope = v.safeParse(paymentSignatureEnvelopeSchema, decodeBase64Json(paymentSignature));
|
|
271
|
+
if (!envelope.success || !isHex(envelope.output.payload.signature)) throw new X402PaymentSignatureUnusableError(intent.id, "The payment signature payload could not be decoded");
|
|
272
|
+
const signature = envelope.output.payload.signature;
|
|
273
|
+
let recovered;
|
|
274
|
+
try {
|
|
275
|
+
recovered = await recoverTypedDataAddress({
|
|
276
|
+
domain: {
|
|
277
|
+
name: domain.name,
|
|
278
|
+
version: domain.version,
|
|
279
|
+
chainId: Number(domain.chainId),
|
|
280
|
+
verifyingContract: getAddress(domain.verifyingContract)
|
|
281
|
+
},
|
|
282
|
+
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
|
|
283
|
+
primaryType: "TransferWithAuthorization",
|
|
284
|
+
message: {
|
|
285
|
+
from: getAddress(message.from),
|
|
286
|
+
to: getAddress(message.to),
|
|
287
|
+
value: BigInt(authorization.value),
|
|
288
|
+
validAfter: BigInt(authorization.validAfter),
|
|
289
|
+
validBefore: BigInt(authorization.validBefore),
|
|
290
|
+
nonce
|
|
291
|
+
},
|
|
292
|
+
signature
|
|
293
|
+
});
|
|
294
|
+
} catch {
|
|
295
|
+
recovered = void 0;
|
|
296
|
+
}
|
|
297
|
+
if (recovered !== args.address) throw new SignatureVerificationFailedError(intent.id, args.address, recovered);
|
|
298
|
+
return signature;
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
export { ApiError, IntentSubmitError, NotAWalletAccountError, SignatureVerificationFailedError, UnsupportedAccountCapabilityError, UnsupportedTypedDataError, X402ApprovalPendingError, X402PaymentDeclinedError, X402PaymentError, X402PaymentInFlightError, X402PaymentSignatureUnusableError, X402SubmitInterruptedError, createCatenaAccount };
|
package/dist/x402.d.mts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { G as X402Resource, W as X402PaymentRequirements, i as FetchLike, n as CatenaClient } from "./client-Cm2wNUuB.mjs";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
//#region src/x402.d.ts
|
|
4
|
+
declare const paymentRequiredSchema: v.LooseObjectSchema<{
|
|
5
|
+
/**
|
|
6
|
+
* Pinned to the wire version this loop speaks. Paying a v1 or future
|
|
7
|
+
* incompatible challenge would settle money for a retry header the server
|
|
8
|
+
* cannot consume.
|
|
9
|
+
*/
|
|
10
|
+
readonly x402Version: v.LiteralSchema<2, undefined>;
|
|
11
|
+
readonly accepts: v.ArraySchema<v.LooseObjectSchema<{
|
|
12
|
+
readonly scheme: v.StringSchema<undefined>;
|
|
13
|
+
readonly network: v.StringSchema<undefined>;
|
|
14
|
+
readonly asset: v.StringSchema<undefined>;
|
|
15
|
+
readonly amount: v.StringSchema<undefined>;
|
|
16
|
+
readonly payTo: v.StringSchema<undefined>;
|
|
17
|
+
readonly maxTimeoutSeconds: v.NumberSchema<undefined>;
|
|
18
|
+
}, undefined>, undefined>;
|
|
19
|
+
readonly resource: v.OptionalSchema<v.LooseObjectSchema<{
|
|
20
|
+
readonly url: v.StringSchema<undefined>;
|
|
21
|
+
readonly serviceName: v.SchemaWithFallback<v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MaxLengthAction<string, 255, undefined>]>, undefined>, undefined>;
|
|
22
|
+
}, undefined>, undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* x402 v2 extension envelope; may carry the seller's signed offers
|
|
25
|
+
* (offer-receipt extension). Untyped here — extraction is tolerant.
|
|
26
|
+
*/
|
|
27
|
+
readonly extensions: v.OptionalSchema<v.UnknownSchema, undefined>;
|
|
28
|
+
}, undefined>;
|
|
29
|
+
type X402PaymentRequired = v.InferOutput<typeof paymentRequiredSchema>;
|
|
30
|
+
/**
|
|
31
|
+
* Decode a 402 response's `PAYMENT-REQUIRED` challenge. Returns undefined for
|
|
32
|
+
* a 402 that carries no decodable v2 challenge (not an x402 endpoint).
|
|
33
|
+
*/
|
|
34
|
+
declare function decodePaymentRequired(response: Response): X402PaymentRequired | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Payment failure raised by this module; `reasons` carries the policy or
|
|
37
|
+
* decline detail when the server provided any, and subclasses mark the cases
|
|
38
|
+
* with a specific recovery path. Not every loop failure is one of these:
|
|
39
|
+
* a create-phase `ApiError` the loop does not absorb as a candidate skip
|
|
40
|
+
* and a "not-submitted" `IntentSubmitError` propagate unchanged. A create-phase
|
|
41
|
+
* `TimeoutError` also propagates unchanged; no payment was authorized, so
|
|
42
|
+
* re-running cannot double-pay.
|
|
43
|
+
*/
|
|
44
|
+
declare class X402PaymentError extends Error {
|
|
45
|
+
readonly reasons: readonly string[];
|
|
46
|
+
constructor(message: string, reasons?: readonly string[],
|
|
47
|
+
/**
|
|
48
|
+
* Subclasses whose message is a single actionable recovery sentence pass
|
|
49
|
+
* `false` so `reasons` stays inspection detail on the property instead of
|
|
50
|
+
* restating server text mid-sentence in the primary output.
|
|
51
|
+
*/
|
|
52
|
+
options?: {
|
|
53
|
+
appendReasonsToMessage?: boolean;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Every payable candidate in the challenge targets a network the source
|
|
58
|
+
* account is not on, so nothing could be funded. Distinct from a generic
|
|
59
|
+
* non-payable requirement: the fix is to fund the payment from a wallet
|
|
60
|
+
* account on one of `requiredNetworks`, not to change policy. Consumers can
|
|
61
|
+
* name those networks (and, for a CLI, tell the user how to switch accounts).
|
|
62
|
+
*/
|
|
63
|
+
declare class X402NetworkMismatchError extends X402PaymentError {
|
|
64
|
+
readonly accountId: string;
|
|
65
|
+
readonly requiredNetworks: readonly string[];
|
|
66
|
+
constructor(accountId: string, requiredNetworks: readonly string[], reasons?: readonly string[]);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The challenge's `payTo` is not a saved counterparty wallet rail, so the
|
|
70
|
+
* allowlist has nothing to authorize and the payment fails closed. The 402's
|
|
71
|
+
* `payTo` is attacker-controlled, so Catena never auto-adds it: the fix is a
|
|
72
|
+
* deliberate `create_counterparty` (itself policy-gated) for `payTo` on
|
|
73
|
+
* `network`, then a re-run. Consumers can name the recipient (and, for a CLI,
|
|
74
|
+
* print the exact counterparty-create command).
|
|
75
|
+
*/
|
|
76
|
+
declare class X402CounterpartyNotFoundError extends X402PaymentError {
|
|
77
|
+
readonly payTo: string;
|
|
78
|
+
readonly network: string;
|
|
79
|
+
/**
|
|
80
|
+
* The challenge's advertised service name, when present — a suggested
|
|
81
|
+
* counterparty name.
|
|
82
|
+
*/
|
|
83
|
+
readonly serviceName: string | undefined;
|
|
84
|
+
constructor(payTo: string, network: string, serviceName?: string, reasons?: readonly string[]);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The payment exceeded a policy limit and is parked for a human approval.
|
|
88
|
+
* Not a decline: once an operator approves it in the console, re-running the
|
|
89
|
+
* same request (same endpoint, same price) consumes the approval and pays.
|
|
90
|
+
* `expiresAt` is when the pending approval self-expires.
|
|
91
|
+
*/
|
|
92
|
+
declare class X402ApprovalPendingError extends X402PaymentError {
|
|
93
|
+
readonly intentId: string;
|
|
94
|
+
readonly expiresAt: string | null;
|
|
95
|
+
constructor(intentId: string, reasons: readonly string[], expiresAt: string | null);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The x402 loop failed AFTER the payment settled, either in `onPayment` or the
|
|
99
|
+
* paid retry. The paid retry may not have been issued. The receipt is the
|
|
100
|
+
* caller's recovery handle: retry the request manually with its
|
|
101
|
+
* `paymentSignature` as the `PAYMENT-SIGNATURE` header (also re-readable via
|
|
102
|
+
* `getIntent(intentId)` at `data.paymentCredential.value` while the
|
|
103
|
+
* authorization is valid) — re-running the whole loop would mint a second
|
|
104
|
+
* payment.
|
|
105
|
+
*/
|
|
106
|
+
declare class X402RetryFailedError extends X402PaymentError {
|
|
107
|
+
readonly receipt: X402PaymentReceipt;
|
|
108
|
+
constructor(receipt: X402PaymentReceipt, cause: unknown);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The submission failed after the intent was created with its outcome
|
|
112
|
+
* unknown — the server may have already claimed and settled (a response lost
|
|
113
|
+
* in transit is the canonical case). Re-running the loop would mint a second
|
|
114
|
+
* intent and risk a double-pay, so the caller must reconcile via
|
|
115
|
+
* `getIntent(intentId)`: if it completed, reuse
|
|
116
|
+
* `data.paymentCredential.value` as the `PAYMENT-SIGNATURE` header; if
|
|
117
|
+
* processing, it is mid-settlement — keep polling. Pay again only once it is
|
|
118
|
+
* terminally blocked or failed.
|
|
119
|
+
*/
|
|
120
|
+
declare class X402SubmitInterruptedError extends X402PaymentError {
|
|
121
|
+
readonly intentId: string;
|
|
122
|
+
readonly requirements: X402PaymentRequirements;
|
|
123
|
+
constructor(intentId: string, requirements: X402PaymentRequirements, cause: unknown);
|
|
124
|
+
}
|
|
125
|
+
interface X402PaymentReceipt {
|
|
126
|
+
/**
|
|
127
|
+
* The Catena intent that settled the payment; pass to `getIntent`.
|
|
128
|
+
*/
|
|
129
|
+
intentId: string;
|
|
130
|
+
/**
|
|
131
|
+
* Encoded value for the retry's `PAYMENT-SIGNATURE` header. Re-readable
|
|
132
|
+
* via `getIntent(intentId)` at `data.paymentCredential.value` while the
|
|
133
|
+
* authorization is valid.
|
|
134
|
+
*/
|
|
135
|
+
paymentSignature: string;
|
|
136
|
+
/**
|
|
137
|
+
* The challenge candidate that was paid.
|
|
138
|
+
*/
|
|
139
|
+
requirements: X402PaymentRequirements;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Pay one decoded 402 challenge from `accountId` and return the
|
|
143
|
+
* `PAYMENT-SIGNATURE` header value. Tries the challenge's exact-scheme
|
|
144
|
+
* candidates in order, skipping ones Catena reports as unpayable (wrong
|
|
145
|
+
* network/asset). When nothing is payable, the terminal error reflects the
|
|
146
|
+
* most actionable skip — an unsaved counterparty, then an unfunded network —
|
|
147
|
+
* and every skipped candidate's reason rides its `reasons`. Failures the
|
|
148
|
+
* loop does not absorb as a skip — policy blocks, counterparty rejections,
|
|
149
|
+
* signing failures — surface as `X402PaymentError` (or the underlying
|
|
150
|
+
* `ApiError` / `IntentSubmitError`). A create-phase `TimeoutError`
|
|
151
|
+
* propagates unchanged; no payment was authorized, so re-running cannot
|
|
152
|
+
* double-pay.
|
|
153
|
+
*/
|
|
154
|
+
declare function payX402Challenge(client: CatenaClient, args: {
|
|
155
|
+
accountId: string;
|
|
156
|
+
paymentRequired: X402PaymentRequired;
|
|
157
|
+
/**
|
|
158
|
+
* Display override for the challenge's own `resource` (for example a
|
|
159
|
+
* suggested counterparty name). The signed offer/receipt evidence is
|
|
160
|
+
* bound to the resolved resource's `url`, so overriding it with a URL
|
|
161
|
+
* that differs from the one the seller signed makes the seller's offer
|
|
162
|
+
* record a resource mismatch in its stored verification. The payment
|
|
163
|
+
* itself is unaffected. Omit to use the challenge's resource unchanged.
|
|
164
|
+
*/
|
|
165
|
+
resource?: X402Resource;
|
|
166
|
+
/**
|
|
167
|
+
* Per-call ceiling in atomic USDC units (6 decimals; "1000000" = $1).
|
|
168
|
+
* The org policy's spend limits still govern server-side; this lets the
|
|
169
|
+
* caller refuse a price it never meant to accept, before any intent.
|
|
170
|
+
*/
|
|
171
|
+
maxAtomicAmount?: bigint;
|
|
172
|
+
}): Promise<X402PaymentReceipt>;
|
|
173
|
+
interface X402FetchOptions {
|
|
174
|
+
/**
|
|
175
|
+
* Source wallet account the payments draw from.
|
|
176
|
+
*/
|
|
177
|
+
accountId: string;
|
|
178
|
+
baseFetch?: FetchLike;
|
|
179
|
+
/**
|
|
180
|
+
* Per-call price ceiling in atomic USDC units (6 decimals). Challenges
|
|
181
|
+
* asking for more are refused before any intent is created.
|
|
182
|
+
*/
|
|
183
|
+
maxAtomicAmount?: bigint;
|
|
184
|
+
/**
|
|
185
|
+
* Called after each successful payment, before the retry — for receipts,
|
|
186
|
+
* logging, or spend telemetry. A thrown error becomes an
|
|
187
|
+
* `X402RetryFailedError` carrying the receipt.
|
|
188
|
+
*/
|
|
189
|
+
onPayment?: (receipt: X402PaymentReceipt) => void | Promise<void>;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Wrap `fetch` so a v2 x402 402 is paid via Catena and retried once with the
|
|
193
|
+
* `PAYMENT-SIGNATURE` header. Non-402 responses (and 402s without a decodable
|
|
194
|
+
* challenge) pass through untouched. Throws `X402PaymentError` before paying
|
|
195
|
+
* when the request body cannot be replayed — pass bodies via the `init`
|
|
196
|
+
* argument as a string, `URLSearchParams`, Blob, ArrayBuffer, or typed
|
|
197
|
+
* array/DataView; a `Request` input must be bodiless. Payment failures surface
|
|
198
|
+
* as `X402PaymentError` (or a subclass documenting its recovery path); an
|
|
199
|
+
* `ApiError` the loop does not absorb as a candidate skip and a "not-submitted"
|
|
200
|
+
* `IntentSubmitError` propagate unchanged. A create-phase `TimeoutError` also
|
|
201
|
+
* propagates unchanged; no payment was authorized, so re-running cannot
|
|
202
|
+
* double-pay.
|
|
203
|
+
*/
|
|
204
|
+
declare function wrapFetchWithX402Payment(client: CatenaClient, options: X402FetchOptions): FetchLike;
|
|
205
|
+
//#endregion
|
|
206
|
+
export { X402ApprovalPendingError, X402CounterpartyNotFoundError, X402FetchOptions, X402NetworkMismatchError, X402PaymentError, X402PaymentReceipt, X402PaymentRequired, type X402PaymentRequirements, type X402Resource, X402RetryFailedError, X402SubmitInterruptedError, decodePaymentRequired, payX402Challenge, wrapFetchWithX402Payment };
|