@catena/sdk 0.0.0-bootstrap.0 → 0.4.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 +910 -2
- package/dist/client-BSYVYJGm.mjs +1032 -0
- package/dist/client-B_Gi0aqH.d.mts +1353 -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/movements.d.mts +139 -0
- package/dist/movements.mjs +334 -0
- package/dist/mpp.d.mts +327 -0
- package/dist/mpp.mjs +563 -0
- package/dist/settlement-report-DTxaAxE3.mjs +57 -0
- package/dist/viem.d.mts +135 -0
- package/dist/viem.mjs +296 -0
- package/dist/x402.d.mts +212 -0
- package/dist/x402.mjs +215 -0
- package/package.json +64 -5
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
import { InvalidKeyError, p256KeypairFromPrivateKeyHex, p256PointFromCompressedHex, p256PublicKeyThumbprintFromPoint } from "./keypair.mjs";
|
|
2
|
+
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
import { createHash, createPrivateKey, randomBytes, sign } from "node:crypto";
|
|
5
|
+
//#region src/request-signing.ts
|
|
6
|
+
const COVERED_COMPONENTS = "(\"@method\" \"@target-uri\" \"content-digest\" \"signature-agent\")";
|
|
7
|
+
const ALGORITHM = "ecdsa-p256-sha256";
|
|
8
|
+
const IDENTITY_URL = /^https?:\/\/[\x21\x23-\x7e]{1,255}$/;
|
|
9
|
+
const NONCE = /^[\x21\x23-\x3a\x3c-\x7e]{1,128}$/;
|
|
10
|
+
function agentKeyIdentityUrl(bankUrl, keyId) {
|
|
11
|
+
return `${bankUrl.replace(/\/+$/, "")}/agent-keys/${keyId}`;
|
|
12
|
+
}
|
|
13
|
+
function createAgentRequestSigner(options) {
|
|
14
|
+
const key = buildSignerKey(options.keypair);
|
|
15
|
+
const state = {
|
|
16
|
+
...key,
|
|
17
|
+
identityUrl: validatedIdentityUrl(options.identityUrl ?? agentKeyIdentityUrl(options.baseUrl, key.keyId))
|
|
18
|
+
};
|
|
19
|
+
return { sign: async (request) => {
|
|
20
|
+
const body = new Uint8Array(await request.clone().arrayBuffer());
|
|
21
|
+
const signatureHeaders = signRequest(state, {
|
|
22
|
+
method: request.method,
|
|
23
|
+
url: request.url,
|
|
24
|
+
body
|
|
25
|
+
}, {
|
|
26
|
+
nonce: randomBytes(16).toString("base64url"),
|
|
27
|
+
createdAt: new Date()
|
|
28
|
+
});
|
|
29
|
+
const headers = new Headers(request.headers);
|
|
30
|
+
for (const [name, value] of Object.entries(signatureHeaders)) headers.set(name, value);
|
|
31
|
+
return new Request(request, { headers });
|
|
32
|
+
} };
|
|
33
|
+
}
|
|
34
|
+
function buildSignerKey(keypair) {
|
|
35
|
+
const point = p256PointFromCompressedHex(keypair.publicKeyHex);
|
|
36
|
+
if (point === null) throw new Error("signer keypair has an invalid P-256 public key");
|
|
37
|
+
return {
|
|
38
|
+
signingKey: p256SigningKey(point, keypair.privateKeyHex),
|
|
39
|
+
keyId: p256PublicKeyThumbprintFromPoint(point)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function validatedIdentityUrl(identityUrl) {
|
|
43
|
+
if (!IDENTITY_URL.test(identityUrl)) throw new Error(`identity URL is outside the signature profile: "${identityUrl}"`);
|
|
44
|
+
return identityUrl;
|
|
45
|
+
}
|
|
46
|
+
function signRequest(state, request, params) {
|
|
47
|
+
if (!NONCE.test(params.nonce)) throw new Error("nonce is outside the signature profile");
|
|
48
|
+
const contentDigest = contentDigestHeader(request.body);
|
|
49
|
+
const signatureAgent = `"${state.identityUrl}"`;
|
|
50
|
+
const created = Math.floor(params.createdAt.getTime() / 1e3);
|
|
51
|
+
const signatureParams = `${COVERED_COMPONENTS};created=${created};keyid="${state.keyId}";nonce="${params.nonce}";alg="${ALGORITHM}"`;
|
|
52
|
+
const signatureBase = [
|
|
53
|
+
`"@method": ${request.method.toUpperCase()}`,
|
|
54
|
+
`"@target-uri": ${canonicalTargetUri(new URL(request.url))}`,
|
|
55
|
+
`"content-digest": ${contentDigest}`,
|
|
56
|
+
`"signature-agent": ${signatureAgent}`,
|
|
57
|
+
`"@signature-params": ${signatureParams}`
|
|
58
|
+
].join("\n");
|
|
59
|
+
const signature = sign("sha256", Buffer.from(signatureBase, "utf8"), {
|
|
60
|
+
key: state.signingKey,
|
|
61
|
+
dsaEncoding: "ieee-p1363"
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
"Content-Digest": contentDigest,
|
|
65
|
+
"Signature-Agent": signatureAgent,
|
|
66
|
+
"Signature-Input": `sig=${signatureParams}`,
|
|
67
|
+
Signature: `sig=:${signature.toString("base64")}:`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function contentDigestHeader(body) {
|
|
71
|
+
return `sha-256=:${createHash("sha256").update(body ?? "").digest("base64")}:`;
|
|
72
|
+
}
|
|
73
|
+
function canonicalTargetUri(url) {
|
|
74
|
+
return `${url.protocol}//${stripDefaultPort(url)}${url.pathname}${url.search}`;
|
|
75
|
+
}
|
|
76
|
+
function stripDefaultPort(url) {
|
|
77
|
+
const host = url.host.toLowerCase();
|
|
78
|
+
if (url.protocol === "https:" && url.port === "443" || url.protocol === "http:" && url.port === "80") return url.hostname.toLowerCase();
|
|
79
|
+
return host;
|
|
80
|
+
}
|
|
81
|
+
function p256SigningKey(point, privateKeyHex) {
|
|
82
|
+
return createPrivateKey({
|
|
83
|
+
key: {
|
|
84
|
+
kty: "EC",
|
|
85
|
+
crv: "P-256",
|
|
86
|
+
d: Buffer.from(privateKeyHex, "hex").toString("base64url"),
|
|
87
|
+
x: point.subarray(1, 33).toString("base64url"),
|
|
88
|
+
y: point.subarray(33, 65).toString("base64url")
|
|
89
|
+
},
|
|
90
|
+
format: "jwk"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region package.json
|
|
95
|
+
var version = "0.4.0";
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/user-agent.ts
|
|
98
|
+
const HTTP_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
99
|
+
const AGENT_CLIENT_VERSION = version;
|
|
100
|
+
function assertHttpToken(value, field) {
|
|
101
|
+
if (!HTTP_TOKEN_RE.test(value)) throw new Error(`${field} must be a valid HTTP token`);
|
|
102
|
+
}
|
|
103
|
+
function assertUaCommentSafe(value, field) {
|
|
104
|
+
let hasUnrepresentableCharacter = false;
|
|
105
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
106
|
+
const code = value.charCodeAt(i);
|
|
107
|
+
if (code < 32 || code === 127 || code > 255) {
|
|
108
|
+
hasUnrepresentableCharacter = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (hasUnrepresentableCharacter || /[()\\]/.test(value)) throw new Error(`${field} must not contain parentheses, backslashes, control characters, or characters outside Latin-1`);
|
|
113
|
+
}
|
|
114
|
+
function buildUserAgent(appInfo) {
|
|
115
|
+
const sdkProduct = `catena-agent-client/${AGENT_CLIENT_VERSION}`;
|
|
116
|
+
if (!appInfo) return sdkProduct;
|
|
117
|
+
assertHttpToken(appInfo.name, "appInfo.name");
|
|
118
|
+
if (appInfo.version !== void 0) assertHttpToken(appInfo.version, "appInfo.version");
|
|
119
|
+
if (appInfo.url !== void 0) assertUaCommentSafe(appInfo.url, "appInfo.url");
|
|
120
|
+
const product = appInfo.version === void 0 ? appInfo.name : `${appInfo.name}/${appInfo.version}`;
|
|
121
|
+
return `${appInfo.url === void 0 ? product : `${product} (${appInfo.url})`} ${sdkProduct}`;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/lib/payment-credential.ts
|
|
125
|
+
const paymentCredentialDataSchema = v.object({ paymentCredential: v.object({
|
|
126
|
+
protocol: v.picklist(["mpp", "x402"]),
|
|
127
|
+
value: v.string()
|
|
128
|
+
}) });
|
|
129
|
+
function paymentCredentialFromIntentData(data, protocol) {
|
|
130
|
+
const credential = v.safeParse(paymentCredentialDataSchema, data);
|
|
131
|
+
return credential.success && credential.output.paymentCredential.protocol === protocol ? credential.output.paymentCredential.value : void 0;
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/offer-receipt.ts
|
|
135
|
+
const OFFER_RECEIPT_EXTENSION_KEY = "offer-receipt";
|
|
136
|
+
const MAX_ARTIFACT_BYTES = 8192;
|
|
137
|
+
const jwsCompactSchema = v.pipe(v.string(), v.maxLength(MAX_ARTIFACT_BYTES), v.regex(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, "Expected a JWS compact serialization"));
|
|
138
|
+
const acceptIndexSchema = v.optional(v.pipe(v.number(), v.integer(), v.minValue(0)));
|
|
139
|
+
const eip712SignatureSchema = v.pipe(v.string(), v.regex(/^0x[0-9a-fA-F]{130}$/, "Expected a 65-byte hex signature"));
|
|
140
|
+
const x402SignedArtifactSchema = v.variant("format", [v.looseObject({
|
|
141
|
+
format: v.literal("eip712"),
|
|
142
|
+
acceptIndex: acceptIndexSchema,
|
|
143
|
+
payload: v.record(v.string(), v.unknown()),
|
|
144
|
+
signature: eip712SignatureSchema
|
|
145
|
+
}), v.looseObject({
|
|
146
|
+
format: v.literal("jws"),
|
|
147
|
+
acceptIndex: acceptIndexSchema,
|
|
148
|
+
signature: jwsCompactSchema
|
|
149
|
+
})]);
|
|
150
|
+
const x402SignedReceiptSchema = v.variant("format", [v.looseObject({
|
|
151
|
+
format: v.literal("eip712"),
|
|
152
|
+
payload: v.record(v.string(), v.unknown()),
|
|
153
|
+
signature: eip712SignatureSchema
|
|
154
|
+
}), v.looseObject({
|
|
155
|
+
format: v.literal("jws"),
|
|
156
|
+
signature: jwsCompactSchema
|
|
157
|
+
})]);
|
|
158
|
+
const offersEnvelopeSchema = v.object({ [OFFER_RECEIPT_EXTENSION_KEY]: v.object({ info: v.object({ offers: v.array(v.unknown()) }) }) });
|
|
159
|
+
const receiptEnvelopeSchema = v.object({ [OFFER_RECEIPT_EXTENSION_KEY]: v.object({ info: v.object({ receipt: v.unknown() }) }) });
|
|
160
|
+
const offerTermsSchema = v.looseObject({
|
|
161
|
+
version: v.literal(1),
|
|
162
|
+
resourceUrl: v.pipe(v.string(), v.maxLength(2048)),
|
|
163
|
+
scheme: v.pipe(v.string(), v.maxLength(64)),
|
|
164
|
+
network: v.pipe(v.string(), v.maxLength(64)),
|
|
165
|
+
asset: v.pipe(v.string(), v.maxLength(128)),
|
|
166
|
+
payTo: v.pipe(v.string(), v.maxLength(128)),
|
|
167
|
+
amount: v.pipe(v.string(), v.regex(/^\d+$/), v.maxLength(78)),
|
|
168
|
+
validUntil: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0)))
|
|
169
|
+
});
|
|
170
|
+
function decodeBase64UrlJson(value) {
|
|
171
|
+
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
172
|
+
const bytes = Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0));
|
|
173
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
174
|
+
}
|
|
175
|
+
function artifactTerms(artifact) {
|
|
176
|
+
if (artifact.format === "eip712") {
|
|
177
|
+
const parsed = v.safeParse(offerTermsSchema, artifact.payload);
|
|
178
|
+
return parsed.success ? parsed.output : void 0;
|
|
179
|
+
}
|
|
180
|
+
const payloadSegment = artifact.signature.split(".")[1];
|
|
181
|
+
try {
|
|
182
|
+
const parsed = v.safeParse(offerTermsSchema, decodeBase64UrlJson(payloadSegment));
|
|
183
|
+
return parsed.success ? parsed.output : void 0;
|
|
184
|
+
} catch {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function sameAddress(a, b) {
|
|
189
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
190
|
+
}
|
|
191
|
+
function termsMatch(terms, requirement) {
|
|
192
|
+
return terms.scheme === requirement.scheme && terms.network === requirement.network && sameAddress(terms.asset, requirement.asset) && sameAddress(terms.payTo, requirement.payTo) && terms.amount === requirement.amount;
|
|
193
|
+
}
|
|
194
|
+
function fitsRelayCap(artifact) {
|
|
195
|
+
return new TextEncoder().encode(JSON.stringify(artifact)).length <= MAX_ARTIFACT_BYTES;
|
|
196
|
+
}
|
|
197
|
+
function extractRelayableOffer(extensions, requirement) {
|
|
198
|
+
const envelope = v.safeParse(offersEnvelopeSchema, extensions);
|
|
199
|
+
if (!envelope.success) return;
|
|
200
|
+
for (const entry of envelope.output[OFFER_RECEIPT_EXTENSION_KEY].info.offers) {
|
|
201
|
+
const artifact = v.safeParse(x402SignedArtifactSchema, entry);
|
|
202
|
+
if (!artifact.success) continue;
|
|
203
|
+
const terms = artifactTerms(artifact.output);
|
|
204
|
+
if (terms !== void 0 && termsMatch(terms, requirement) && fitsRelayCap(artifact.output)) return artifact.output;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function extractRelayableReceipt(extensions) {
|
|
208
|
+
const envelope = v.safeParse(receiptEnvelopeSchema, extensions);
|
|
209
|
+
if (!envelope.success) return;
|
|
210
|
+
const artifact = v.safeParse(x402SignedReceiptSchema, envelope.output[OFFER_RECEIPT_EXTENSION_KEY].info.receipt);
|
|
211
|
+
if (!artifact.success || !fitsRelayCap(artifact.output)) return;
|
|
212
|
+
return artifact.output;
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/schemas.ts
|
|
216
|
+
const nonBlankResponseEnumSchema = v.pipe(v.string(), v.regex(/\S/, "Response enum values must not be blank"));
|
|
217
|
+
function extensibleStringSchema(_known) {
|
|
218
|
+
return nonBlankResponseEnumSchema;
|
|
219
|
+
}
|
|
220
|
+
const moneyJsonSchema = v.object({
|
|
221
|
+
amount: v.string(),
|
|
222
|
+
asset_id: v.string()
|
|
223
|
+
});
|
|
224
|
+
const isoTimestampSchema = v.pipe(v.string(), v.isoTimestamp());
|
|
225
|
+
const okSuccessResponseSchema = v.object({ success: v.literal(true) });
|
|
226
|
+
const agentSchema = v.object({
|
|
227
|
+
id: v.string(),
|
|
228
|
+
name: v.string(),
|
|
229
|
+
status: v.string(),
|
|
230
|
+
organizationId: v.string()
|
|
231
|
+
});
|
|
232
|
+
const whoamiResponseSchema = v.object({ agent: agentSchema });
|
|
233
|
+
const POLICY_RULE_TYPES = [
|
|
234
|
+
"per_transaction_amount",
|
|
235
|
+
"daily_amount",
|
|
236
|
+
"weekly_amount",
|
|
237
|
+
"monthly_amount",
|
|
238
|
+
"hourly_count",
|
|
239
|
+
"daily_count"
|
|
240
|
+
];
|
|
241
|
+
const POLICY_RULE_ACTIONS = ["block", "require_approval"];
|
|
242
|
+
const ACCOUNT_AGGREGATION_SCOPES = ["per_account", "across_accounts"];
|
|
243
|
+
const ACTOR_AGGREGATION_SCOPES = ["per_agent", "per_policy"];
|
|
244
|
+
const policyRuleSchema = v.object({
|
|
245
|
+
id: v.string(),
|
|
246
|
+
ruleType: extensibleStringSchema(POLICY_RULE_TYPES),
|
|
247
|
+
thresholdAmount: v.optional(moneyJsonSchema),
|
|
248
|
+
thresholdCount: v.optional(v.number()),
|
|
249
|
+
action: extensibleStringSchema(POLICY_RULE_ACTIONS),
|
|
250
|
+
requiredApprovals: v.number(),
|
|
251
|
+
accountAggregationScope: extensibleStringSchema(ACCOUNT_AGGREGATION_SCOPES),
|
|
252
|
+
actorAggregationScope: extensibleStringSchema(ACTOR_AGGREGATION_SCOPES),
|
|
253
|
+
displayOrder: v.number()
|
|
254
|
+
});
|
|
255
|
+
const POLICY_CAPABILITIES = [
|
|
256
|
+
"query_balance",
|
|
257
|
+
"read",
|
|
258
|
+
"send",
|
|
259
|
+
"transfer"
|
|
260
|
+
];
|
|
261
|
+
const policyCapabilitySchema = v.object({
|
|
262
|
+
id: v.string(),
|
|
263
|
+
organizationId: v.string(),
|
|
264
|
+
policyId: v.string(),
|
|
265
|
+
capability: extensibleStringSchema(POLICY_CAPABILITIES),
|
|
266
|
+
accountId: v.string(),
|
|
267
|
+
rules: v.array(policyRuleSchema),
|
|
268
|
+
createdAt: isoTimestampSchema,
|
|
269
|
+
updatedAt: isoTimestampSchema
|
|
270
|
+
});
|
|
271
|
+
const COUNTERPARTY_ACTIONS = [
|
|
272
|
+
"allow",
|
|
273
|
+
"block",
|
|
274
|
+
"require_approval"
|
|
275
|
+
];
|
|
276
|
+
const COUNTERPARTY_RULE_MODES = ["open", "restricted"];
|
|
277
|
+
const counterpartyActionSchema = extensibleStringSchema(COUNTERPARTY_ACTIONS);
|
|
278
|
+
const counterpartyRulesSchema = v.object({
|
|
279
|
+
mode: extensibleStringSchema(COUNTERPARTY_RULE_MODES),
|
|
280
|
+
allowedCounterparties: v.optional(v.array(v.string())),
|
|
281
|
+
createAction: v.optional(counterpartyActionSchema),
|
|
282
|
+
requestPaymentDetailsAction: v.optional(counterpartyActionSchema),
|
|
283
|
+
allowCreate: v.optional(v.boolean())
|
|
284
|
+
});
|
|
285
|
+
const policySchema = v.object({
|
|
286
|
+
id: v.optional(v.string()),
|
|
287
|
+
description: v.optional(v.string()),
|
|
288
|
+
version: v.optional(v.number()),
|
|
289
|
+
name: v.string(),
|
|
290
|
+
capabilities: v.array(v.string()),
|
|
291
|
+
counterpartyRules: v.optional(counterpartyRulesSchema),
|
|
292
|
+
policyCapabilities: v.array(policyCapabilitySchema),
|
|
293
|
+
kind: v.optional(v.picklist(["base", "override"])),
|
|
294
|
+
expiresAt: v.optional(v.string())
|
|
295
|
+
});
|
|
296
|
+
const policyResponseSchema = v.object({ policy: policySchema });
|
|
297
|
+
const accountsResponseSchema = v.object({ accounts: v.array(v.object({
|
|
298
|
+
id: v.string(),
|
|
299
|
+
name: v.string(),
|
|
300
|
+
type: v.string(),
|
|
301
|
+
currency: v.string()
|
|
302
|
+
})) });
|
|
303
|
+
const accountBalanceResponseSchema = v.object({
|
|
304
|
+
accountId: v.string(),
|
|
305
|
+
balance: moneyJsonSchema,
|
|
306
|
+
balances: v.optional(v.object({
|
|
307
|
+
total: moneyJsonSchema,
|
|
308
|
+
available: moneyJsonSchema,
|
|
309
|
+
byNetwork: v.optional(v.array(v.object({
|
|
310
|
+
network: v.string(),
|
|
311
|
+
total: moneyJsonSchema,
|
|
312
|
+
available: moneyJsonSchema
|
|
313
|
+
})))
|
|
314
|
+
}))
|
|
315
|
+
});
|
|
316
|
+
const ACCOUNT_TRANSACTION_TYPES = [
|
|
317
|
+
"operator-send",
|
|
318
|
+
"operator-transfer",
|
|
319
|
+
"operator-deposit",
|
|
320
|
+
"operator-refund",
|
|
321
|
+
"microdeposit"
|
|
322
|
+
];
|
|
323
|
+
const ACCOUNT_TRANSACTION_STATUSES = [
|
|
324
|
+
"pending",
|
|
325
|
+
"processing",
|
|
326
|
+
"completed",
|
|
327
|
+
"failed",
|
|
328
|
+
"reversed"
|
|
329
|
+
];
|
|
330
|
+
const ACCOUNT_TRANSACTION_MOVEMENT_STATUSES = [
|
|
331
|
+
"initiated",
|
|
332
|
+
"provider_pending",
|
|
333
|
+
"processing",
|
|
334
|
+
"completed",
|
|
335
|
+
"failed",
|
|
336
|
+
"reversed",
|
|
337
|
+
"manual_review"
|
|
338
|
+
];
|
|
339
|
+
const SEND_METHODS = [
|
|
340
|
+
"ach",
|
|
341
|
+
"wire",
|
|
342
|
+
"on-chain"
|
|
343
|
+
];
|
|
344
|
+
const ACCOUNT_TRANSACTION_METHODS = [
|
|
345
|
+
"ach",
|
|
346
|
+
"wire",
|
|
347
|
+
"on-chain"
|
|
348
|
+
];
|
|
349
|
+
const accountTransactionSchema = v.object({
|
|
350
|
+
id: v.string(),
|
|
351
|
+
type: extensibleStringSchema(ACCOUNT_TRANSACTION_TYPES),
|
|
352
|
+
action: v.optional(v.string()),
|
|
353
|
+
status: extensibleStringSchema(ACCOUNT_TRANSACTION_STATUSES),
|
|
354
|
+
movementStatus: extensibleStringSchema(ACCOUNT_TRANSACTION_MOVEMENT_STATUSES),
|
|
355
|
+
direction: v.picklist(["incoming", "outgoing"]),
|
|
356
|
+
amount: moneyJsonSchema,
|
|
357
|
+
fee: moneyJsonSchema,
|
|
358
|
+
currency: v.string(),
|
|
359
|
+
network: v.optional(v.string()),
|
|
360
|
+
counterpartyName: v.optional(v.string()),
|
|
361
|
+
method: v.optional(extensibleStringSchema(ACCOUNT_TRANSACTION_METHODS)),
|
|
362
|
+
memo: v.optional(v.string()),
|
|
363
|
+
description: v.optional(v.string()),
|
|
364
|
+
txHash: v.optional(v.string()),
|
|
365
|
+
createdAt: isoTimestampSchema,
|
|
366
|
+
completedAt: v.optional(isoTimestampSchema)
|
|
367
|
+
});
|
|
368
|
+
const accountTransactionsResponseSchema = v.object({
|
|
369
|
+
accountId: v.string(),
|
|
370
|
+
transactions: v.array(accountTransactionSchema),
|
|
371
|
+
total: v.number()
|
|
372
|
+
});
|
|
373
|
+
const ACCOUNT_DEPOSIT_ADDRESS_SOURCES = ["wallet", "liquidation"];
|
|
374
|
+
const accountDepositAddressResponseSchema = v.object({
|
|
375
|
+
accountId: v.string(),
|
|
376
|
+
address: v.string(),
|
|
377
|
+
liquidationAddressId: v.optional(v.string()),
|
|
378
|
+
source: extensibleStringSchema(ACCOUNT_DEPOSIT_ADDRESS_SOURCES)
|
|
379
|
+
});
|
|
380
|
+
const walletCounterpartyRailSchema = v.object({
|
|
381
|
+
type: v.literal("wallet"),
|
|
382
|
+
walletAddress: v.custom((input) => typeof input === "string" && /^0x[0-9a-fA-F]{40}$/.test(input)),
|
|
383
|
+
network: v.custom((input) => typeof input === "string" && input.length > 0)
|
|
384
|
+
});
|
|
385
|
+
const counterpartySchema = v.object({
|
|
386
|
+
id: v.string(),
|
|
387
|
+
name: v.string(),
|
|
388
|
+
status: v.optional(v.string()),
|
|
389
|
+
rails: v.array(v.object({
|
|
390
|
+
id: v.string(),
|
|
391
|
+
type: v.string(),
|
|
392
|
+
walletAddress: v.optional(v.string()),
|
|
393
|
+
network: v.optional(v.string()),
|
|
394
|
+
bankName: v.optional(v.string()),
|
|
395
|
+
accountNumber: v.optional(v.string())
|
|
396
|
+
}))
|
|
397
|
+
});
|
|
398
|
+
const counterpartiesResponseSchema = v.object({ counterparties: v.array(counterpartySchema) });
|
|
399
|
+
const intentStatusSchema = v.picklist([
|
|
400
|
+
"pending",
|
|
401
|
+
"processing",
|
|
402
|
+
"completed",
|
|
403
|
+
"blocked",
|
|
404
|
+
"failed"
|
|
405
|
+
]);
|
|
406
|
+
const intentNextActionSchema = v.object({
|
|
407
|
+
type: v.literal("submit_stamp"),
|
|
408
|
+
signingRequestId: v.string(),
|
|
409
|
+
signer: v.object({
|
|
410
|
+
algorithm: v.literal("p256"),
|
|
411
|
+
publicKeyHex: v.string()
|
|
412
|
+
}),
|
|
413
|
+
preparedBody: v.object({
|
|
414
|
+
body: v.string(),
|
|
415
|
+
bodyHash: v.string(),
|
|
416
|
+
expiresAt: isoTimestampSchema,
|
|
417
|
+
prepareToken: v.string()
|
|
418
|
+
}),
|
|
419
|
+
movement: v.optional(v.object({
|
|
420
|
+
planId: v.string(),
|
|
421
|
+
signerAddress: v.string(),
|
|
422
|
+
digests: v.array(v.string()),
|
|
423
|
+
instructions: v.array(v.looseObject({
|
|
424
|
+
kind: v.string(),
|
|
425
|
+
network: v.string(),
|
|
426
|
+
signerAddress: v.string(),
|
|
427
|
+
digest: v.string(),
|
|
428
|
+
payload: v.record(v.string(), v.unknown())
|
|
429
|
+
}))
|
|
430
|
+
}))
|
|
431
|
+
});
|
|
432
|
+
const INTENT_ACTION_TYPES = [
|
|
433
|
+
"send",
|
|
434
|
+
"transfer",
|
|
435
|
+
"create_counterparty",
|
|
436
|
+
"request_counterparty_details",
|
|
437
|
+
"x402",
|
|
438
|
+
"mpp",
|
|
439
|
+
"policy_override"
|
|
440
|
+
];
|
|
441
|
+
const CROSS_CHAIN_STATES = [
|
|
442
|
+
"burning",
|
|
443
|
+
"attesting",
|
|
444
|
+
"minting",
|
|
445
|
+
"ready",
|
|
446
|
+
"delivering",
|
|
447
|
+
"delivered",
|
|
448
|
+
"failed",
|
|
449
|
+
"manual_review"
|
|
450
|
+
];
|
|
451
|
+
const CROSS_CHAIN_NEXT_STEPS = [
|
|
452
|
+
"wait",
|
|
453
|
+
"continue",
|
|
454
|
+
"done",
|
|
455
|
+
"blocked"
|
|
456
|
+
];
|
|
457
|
+
const crossChainSendSchema = v.object({
|
|
458
|
+
id: v.string(),
|
|
459
|
+
sourceNetwork: v.string(),
|
|
460
|
+
destinationNetwork: v.string(),
|
|
461
|
+
state: extensibleStringSchema(CROSS_CHAIN_STATES),
|
|
462
|
+
nextStep: extensibleStringSchema(CROSS_CHAIN_NEXT_STEPS),
|
|
463
|
+
readyAt: v.nullable(isoTimestampSchema),
|
|
464
|
+
deliveredAt: v.nullable(isoTimestampSchema),
|
|
465
|
+
failureReason: v.nullable(v.string())
|
|
466
|
+
});
|
|
467
|
+
const MOVEMENT_NEXT_STEPS = [
|
|
468
|
+
"wait",
|
|
469
|
+
"continue",
|
|
470
|
+
"done",
|
|
471
|
+
"blocked"
|
|
472
|
+
];
|
|
473
|
+
const movementSchema = v.object({
|
|
474
|
+
planId: v.string(),
|
|
475
|
+
nextStep: extensibleStringSchema(MOVEMENT_NEXT_STEPS)
|
|
476
|
+
});
|
|
477
|
+
const movementContinuationSchema = v.object({ planId: v.string() });
|
|
478
|
+
const continueIntentResponseSchema = v.object({
|
|
479
|
+
id: v.string(),
|
|
480
|
+
status: intentStatusSchema,
|
|
481
|
+
crossChain: v.optional(crossChainSendSchema),
|
|
482
|
+
movement: v.optional(movementContinuationSchema),
|
|
483
|
+
nextAction: intentNextActionSchema
|
|
484
|
+
});
|
|
485
|
+
const wireIntentResultSchema = v.object({
|
|
486
|
+
id: v.string(),
|
|
487
|
+
accountId: v.optional(v.string()),
|
|
488
|
+
type: extensibleStringSchema(INTENT_ACTION_TYPES),
|
|
489
|
+
status: intentStatusSchema,
|
|
490
|
+
reasons: v.array(v.string()),
|
|
491
|
+
expiresAt: v.optional(v.nullable(isoTimestampSchema), null),
|
|
492
|
+
replayed: v.optional(v.boolean(), false),
|
|
493
|
+
data: v.optional(v.nullable(v.record(v.string(), v.unknown())), null),
|
|
494
|
+
metadata: v.optional(v.object({ dataUrl: v.nullable(v.string()) })),
|
|
495
|
+
crossChain: v.optional(crossChainSendSchema),
|
|
496
|
+
movement: v.optional(movementSchema),
|
|
497
|
+
nextAction: v.optional(intentNextActionSchema)
|
|
498
|
+
});
|
|
499
|
+
function toIntentResult(wire) {
|
|
500
|
+
const { nextAction: _nextAction, ...intent } = wire;
|
|
501
|
+
return intent;
|
|
502
|
+
}
|
|
503
|
+
const legacyX402IntentDataSchema = v.object({ x402: v.object({ paymentSignature: v.string() }) });
|
|
504
|
+
function x402CredentialFromIntentData(data) {
|
|
505
|
+
const credential = paymentCredentialFromIntentData(data, "x402");
|
|
506
|
+
if (credential !== void 0) return credential;
|
|
507
|
+
const legacy = v.safeParse(legacyX402IntentDataSchema, data);
|
|
508
|
+
return legacy.success ? legacy.output.x402.paymentSignature : void 0;
|
|
509
|
+
}
|
|
510
|
+
v.picklist(SEND_METHODS);
|
|
511
|
+
const sendActionSchemasByMethod = {
|
|
512
|
+
ach: v.object({
|
|
513
|
+
type: v.literal("send"),
|
|
514
|
+
accountId: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
515
|
+
counterpartyRailId: v.string(),
|
|
516
|
+
amount: v.string(),
|
|
517
|
+
description: v.optional(v.string()),
|
|
518
|
+
method: v.literal("ach"),
|
|
519
|
+
memo: v.optional(v.string())
|
|
520
|
+
}),
|
|
521
|
+
wire: v.object({
|
|
522
|
+
type: v.literal("send"),
|
|
523
|
+
accountId: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
524
|
+
counterpartyRailId: v.string(),
|
|
525
|
+
amount: v.string(),
|
|
526
|
+
description: v.optional(v.string()),
|
|
527
|
+
method: v.literal("wire"),
|
|
528
|
+
memo: v.optional(v.string())
|
|
529
|
+
}),
|
|
530
|
+
"on-chain": v.object({
|
|
531
|
+
type: v.literal("send"),
|
|
532
|
+
accountId: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
533
|
+
counterpartyRailId: v.string(),
|
|
534
|
+
amount: v.string(),
|
|
535
|
+
description: v.optional(v.string()),
|
|
536
|
+
method: v.literal("on-chain"),
|
|
537
|
+
memo: v.optional(v.string())
|
|
538
|
+
})
|
|
539
|
+
};
|
|
540
|
+
const sendActionSchema = v.variant("method", [
|
|
541
|
+
sendActionSchemasByMethod.ach,
|
|
542
|
+
sendActionSchemasByMethod.wire,
|
|
543
|
+
sendActionSchemasByMethod["on-chain"]
|
|
544
|
+
]);
|
|
545
|
+
const transferActionSchema = v.object({
|
|
546
|
+
type: v.literal("transfer"),
|
|
547
|
+
accountId: v.string(),
|
|
548
|
+
toAccountId: v.string(),
|
|
549
|
+
amount: v.string(),
|
|
550
|
+
memo: v.optional(v.string()),
|
|
551
|
+
description: v.optional(v.string())
|
|
552
|
+
});
|
|
553
|
+
const createCounterpartyBaseFields = {
|
|
554
|
+
type: v.literal("create_counterparty"),
|
|
555
|
+
name: v.string()
|
|
556
|
+
};
|
|
557
|
+
const createCounterpartyActionSchema = v.union([v.object({
|
|
558
|
+
...createCounterpartyBaseFields,
|
|
559
|
+
email: v.optional(v.string()),
|
|
560
|
+
rail: v.nonOptional(v.unknown())
|
|
561
|
+
}), v.object({
|
|
562
|
+
...createCounterpartyBaseFields,
|
|
563
|
+
email: v.string()
|
|
564
|
+
})]);
|
|
565
|
+
const requestCounterpartyDetailsActionSchema = v.object({
|
|
566
|
+
type: v.literal("request_counterparty_details"),
|
|
567
|
+
counterpartyId: v.string(),
|
|
568
|
+
methods: v.pipe(v.object({
|
|
569
|
+
bank: v.boolean(),
|
|
570
|
+
wallet: v.boolean()
|
|
571
|
+
}), v.check(({ bank, wallet }) => bank || wallet, "Select at least one payment method to request"))
|
|
572
|
+
});
|
|
573
|
+
const x402PaymentRequirementsSchema = v.looseObject({
|
|
574
|
+
scheme: v.string(),
|
|
575
|
+
network: v.string(),
|
|
576
|
+
asset: v.string(),
|
|
577
|
+
amount: v.string(),
|
|
578
|
+
payTo: v.string(),
|
|
579
|
+
maxTimeoutSeconds: v.number()
|
|
580
|
+
});
|
|
581
|
+
const x402ResourceSchema = v.looseObject({
|
|
582
|
+
url: v.string(),
|
|
583
|
+
serviceName: v.fallback(v.optional(v.pipe(v.string(), v.maxLength(255))), void 0)
|
|
584
|
+
});
|
|
585
|
+
const x402AddressSchema = v.pipe(v.string(), v.regex(/^0x[0-9a-fA-F]{40}$/, "Expected a 20-byte hex address"));
|
|
586
|
+
const x402Uint256StringSchema = v.pipe(v.string(), v.maxLength(78), v.regex(/^(0|[1-9]\d*)$/, "Expected a canonical decimal integer string"));
|
|
587
|
+
const x402AuthorizationSchema = v.object({
|
|
588
|
+
from: x402AddressSchema,
|
|
589
|
+
to: x402AddressSchema,
|
|
590
|
+
value: x402Uint256StringSchema,
|
|
591
|
+
validAfter: x402Uint256StringSchema,
|
|
592
|
+
validBefore: x402Uint256StringSchema,
|
|
593
|
+
nonce: v.pipe(v.string(), v.regex(/^0x[0-9a-fA-F]{64}$/, "Expected a 32-byte hex nonce"))
|
|
594
|
+
});
|
|
595
|
+
const x402ActionSchema = v.object({
|
|
596
|
+
type: v.literal("x402"),
|
|
597
|
+
accountId: v.string(),
|
|
598
|
+
paymentRequirements: x402PaymentRequirementsSchema,
|
|
599
|
+
resource: v.optional(x402ResourceSchema),
|
|
600
|
+
authorization: v.optional(x402AuthorizationSchema),
|
|
601
|
+
signedOffer: v.optional(x402SignedArtifactSchema)
|
|
602
|
+
});
|
|
603
|
+
const mppChallengeSchema = v.object({
|
|
604
|
+
id: v.string(),
|
|
605
|
+
realm: v.string(),
|
|
606
|
+
method: v.picklist(["evm", "usdc"]),
|
|
607
|
+
intent: v.literal("charge"),
|
|
608
|
+
request: v.string(),
|
|
609
|
+
description: v.optional(v.string()),
|
|
610
|
+
digest: v.optional(v.string()),
|
|
611
|
+
expires: v.string(),
|
|
612
|
+
header: v.optional(v.literal("Payment-Authorization")),
|
|
613
|
+
opaque: v.optional(v.string())
|
|
614
|
+
});
|
|
615
|
+
const mppResourceSchema = v.object({
|
|
616
|
+
url: v.string(),
|
|
617
|
+
serviceName: v.optional(v.string())
|
|
618
|
+
});
|
|
619
|
+
const mppActionSchema = v.object({
|
|
620
|
+
type: v.literal("mpp"),
|
|
621
|
+
accountId: v.string(),
|
|
622
|
+
challenge: mppChallengeSchema,
|
|
623
|
+
resource: v.optional(mppResourceSchema)
|
|
624
|
+
});
|
|
625
|
+
const POLICY_OVERRIDE_CAPABILITIES = ["send", "transfer"];
|
|
626
|
+
const policyOverrideAmountOperationSchema = v.strictObject({
|
|
627
|
+
type: v.literal("increase_limit"),
|
|
628
|
+
accountId: v.pipe(v.string(), v.minLength(1)),
|
|
629
|
+
capability: v.picklist(POLICY_OVERRIDE_CAPABILITIES),
|
|
630
|
+
limitType: v.picklist([
|
|
631
|
+
"per_transaction_amount",
|
|
632
|
+
"daily_amount",
|
|
633
|
+
"weekly_amount",
|
|
634
|
+
"monthly_amount"
|
|
635
|
+
]),
|
|
636
|
+
newLimit: v.strictObject({
|
|
637
|
+
amount: v.pipe(v.string(), v.regex(/^\d+(\.\d{1,6})?$/), v.check((value) => Number(value) > 0, "The amount must be greater than zero")),
|
|
638
|
+
assetId: v.literal("USD")
|
|
639
|
+
})
|
|
640
|
+
});
|
|
641
|
+
const policyOverrideCountOperationSchema = v.strictObject({
|
|
642
|
+
type: v.literal("increase_limit"),
|
|
643
|
+
accountId: v.pipe(v.string(), v.minLength(1)),
|
|
644
|
+
capability: v.picklist(POLICY_OVERRIDE_CAPABILITIES),
|
|
645
|
+
limitType: v.picklist(["hourly_count", "daily_count"]),
|
|
646
|
+
newLimit: v.strictObject({ count: v.pipe(v.number(), v.integer(), v.minValue(1)) })
|
|
647
|
+
});
|
|
648
|
+
const policyOverrideOperationsSchema = v.pipe(v.array(v.union([policyOverrideAmountOperationSchema, policyOverrideCountOperationSchema])), v.minLength(1), v.maxLength(10), v.check((operations) => new Set(operations.map((operation) => `${operation.accountId}|${operation.capability}|${operation.limitType}`)).size === operations.length, "Each account, capability, and limit type may appear only once"));
|
|
649
|
+
const policyOverrideActionSchema = v.object({
|
|
650
|
+
type: v.literal("policy_override"),
|
|
651
|
+
operations: policyOverrideOperationsSchema,
|
|
652
|
+
durationSeconds: v.pipe(v.number(), v.integer(), v.minValue(60), v.maxValue(604800)),
|
|
653
|
+
reason: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(280))
|
|
654
|
+
});
|
|
655
|
+
const intentActionSchema = v.union([
|
|
656
|
+
sendActionSchema,
|
|
657
|
+
transferActionSchema,
|
|
658
|
+
createCounterpartyActionSchema,
|
|
659
|
+
requestCounterpartyDetailsActionSchema,
|
|
660
|
+
x402ActionSchema,
|
|
661
|
+
mppActionSchema,
|
|
662
|
+
policyOverrideActionSchema
|
|
663
|
+
]);
|
|
664
|
+
function toWireAction(action) {
|
|
665
|
+
const parsedAction = v.parse(intentActionSchema, action);
|
|
666
|
+
if (parsedAction.type === "send" || parsedAction.type === "transfer") return {
|
|
667
|
+
...parsedAction,
|
|
668
|
+
amount: {
|
|
669
|
+
amount: parsedAction.amount,
|
|
670
|
+
asset_id: "USD"
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
if (parsedAction.type === "policy_override") return {
|
|
674
|
+
...parsedAction,
|
|
675
|
+
schemaVersion: 1
|
|
676
|
+
};
|
|
677
|
+
return parsedAction;
|
|
678
|
+
}
|
|
679
|
+
const submitStampResponseSchema = v.object({ intent: wireIntentResultSchema });
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/client.ts
|
|
682
|
+
var ApiError = class extends Error {
|
|
683
|
+
status;
|
|
684
|
+
code;
|
|
685
|
+
details;
|
|
686
|
+
fields;
|
|
687
|
+
constructor(status, message, code, details, fields) {
|
|
688
|
+
const explanations = Object.entries(fields ?? {}).map(([path, explanation]) => path === "request" ? explanation : `${path}: ${explanation}`).join("; ");
|
|
689
|
+
const description = explanations ? `${message}: ${explanations}` : message;
|
|
690
|
+
super(code ? `API error ${status} [${code}]: ${description}` : `API error ${status}: ${description}`);
|
|
691
|
+
this.name = "ApiError";
|
|
692
|
+
this.status = status;
|
|
693
|
+
this.code = code;
|
|
694
|
+
this.details = details;
|
|
695
|
+
this.fields = fields;
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
var TimeoutError = class extends Error {
|
|
699
|
+
timeoutMs;
|
|
700
|
+
constructor(timeoutMs) {
|
|
701
|
+
super(`Request timed out after ${timeoutMs}ms. The server may still have processed a state-changing request; follow that operation's documented recovery procedure before retrying.`);
|
|
702
|
+
this.name = "TimeoutError";
|
|
703
|
+
this.timeoutMs = timeoutMs;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
const errorBodySchema = v.object({
|
|
707
|
+
error: v.nullish(v.string()),
|
|
708
|
+
message: v.nullish(v.string()),
|
|
709
|
+
code: v.nullish(v.string()),
|
|
710
|
+
details: v.nullish(v.record(v.string(), v.unknown())),
|
|
711
|
+
fields: v.fallback(v.nullish(v.pipe(v.unknown(), v.check((value) => !Array.isArray(value)), v.record(v.string(), v.string()))), void 0)
|
|
712
|
+
});
|
|
713
|
+
async function apiErrorFromResponse(res) {
|
|
714
|
+
const parsed = v.safeParse(errorBodySchema, await res.json().catch(() => void 0));
|
|
715
|
+
const body = parsed.success ? parsed.output : {};
|
|
716
|
+
if (body.error != null && body.message != null) return new ApiError(res.status, body.message, body.code ?? body.error, body.details ?? void 0, body.fields ?? void 0);
|
|
717
|
+
return new ApiError(res.status, body.error ?? body.message ?? res.statusText, body.code ?? void 0, body.details ?? void 0, body.fields ?? void 0);
|
|
718
|
+
}
|
|
719
|
+
async function parseApiResponse(res, schema) {
|
|
720
|
+
if (!res.ok) throw await apiErrorFromResponse(res);
|
|
721
|
+
return v.parse(schema, await res.json());
|
|
722
|
+
}
|
|
723
|
+
var IntentSubmitError = class extends Error {
|
|
724
|
+
intentId;
|
|
725
|
+
outcome;
|
|
726
|
+
constructor(params) {
|
|
727
|
+
const detail = params.cause instanceof Error ? params.cause.message : String(params.cause);
|
|
728
|
+
super(params.outcome === "not-submitted" ? `Intent ${params.intentId} was created but never submitted; no money moved: ${detail}` : `Intent ${params.intentId} submission could not be confirmed: ${detail}; poll getIntent("${params.intentId}") to a terminal status and resubmit only after blocked or failed`, { cause: params.cause });
|
|
729
|
+
this.name = "IntentSubmitError";
|
|
730
|
+
this.intentId = params.intentId;
|
|
731
|
+
this.outcome = params.outcome;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
const DEFAULT_BASE_URL = "https://api.catena.com";
|
|
735
|
+
const DEFAULT_TIMEOUT_MS = 8e4;
|
|
736
|
+
const MAX_TIMEOUT_MS = 2147483647;
|
|
737
|
+
const digestBatchBodySchema = v.looseObject({ parameters: v.looseObject({ payloads: v.pipe(v.array(v.unknown()), v.minLength(1)) }) });
|
|
738
|
+
let movementsModule;
|
|
739
|
+
let movementsLoadFailure;
|
|
740
|
+
function loadMovements() {
|
|
741
|
+
movementsModule ??= import("./movements.mjs").then((loaded) => loaded, (reason) => {
|
|
742
|
+
movementsLoadFailure = reason;
|
|
743
|
+
});
|
|
744
|
+
return movementsModule;
|
|
745
|
+
}
|
|
746
|
+
async function continuationKinds() {
|
|
747
|
+
return await loadMovements() === void 0 ? "crossChain" : "crossChain, movement";
|
|
748
|
+
}
|
|
749
|
+
const digestBatchActivitySchema = v.looseObject({ type: v.literal("ACTIVITY_TYPE_SIGN_RAW_PAYLOADS") });
|
|
750
|
+
function signsADigestBatch(body) {
|
|
751
|
+
try {
|
|
752
|
+
const parsed = JSON.parse(body);
|
|
753
|
+
return v.safeParse(digestBatchBodySchema, parsed).success || v.safeParse(digestBatchActivitySchema, parsed).success;
|
|
754
|
+
} catch {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
var CatenaClient = class {
|
|
759
|
+
#baseUrl;
|
|
760
|
+
#userAgent;
|
|
761
|
+
#defaultHeaders;
|
|
762
|
+
#keypair;
|
|
763
|
+
#signer;
|
|
764
|
+
#fetch;
|
|
765
|
+
#timeoutMs;
|
|
766
|
+
constructor(opts) {
|
|
767
|
+
const timeoutMs = opts.timeout === void 0 ? DEFAULT_TIMEOUT_MS : opts.timeout;
|
|
768
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMEOUT_MS) throw new TypeError(`timeout must be a positive integer no greater than ${MAX_TIMEOUT_MS}`);
|
|
769
|
+
this.#baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
770
|
+
this.#userAgent = buildUserAgent(opts.appInfo);
|
|
771
|
+
this.#defaultHeaders = opts.defaultHeaders ?? {};
|
|
772
|
+
this.#keypair = p256KeypairFromPrivateKeyHex(opts.privateKeyHex);
|
|
773
|
+
this.#fetch = (opts.fetch ?? globalThis.fetch).bind(globalThis);
|
|
774
|
+
this.#timeoutMs = timeoutMs;
|
|
775
|
+
this.#signer = createAgentRequestSigner({
|
|
776
|
+
keypair: this.#keypair,
|
|
777
|
+
baseUrl: this.#baseUrl,
|
|
778
|
+
identityUrl: opts.identityUrl
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
#buildRequestHeaders(requestHeaders) {
|
|
782
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
783
|
+
for (const [name, value] of Object.entries(this.#defaultHeaders)) headers.set(name, value);
|
|
784
|
+
for (const [name, value] of Object.entries(requestHeaders ?? {})) headers.set(name, value);
|
|
785
|
+
headers.set("User-Agent", this.#userAgent);
|
|
786
|
+
headers.delete("Authorization");
|
|
787
|
+
return headers;
|
|
788
|
+
}
|
|
789
|
+
async #signedRequest(options) {
|
|
790
|
+
const url = new URL(`${this.#baseUrl}/v1/agent-api${options.path}`);
|
|
791
|
+
for (const [key, value] of Object.entries(options.query ?? {})) if (value !== void 0) url.searchParams.set(key, value);
|
|
792
|
+
const request = new Request(url, {
|
|
793
|
+
method: options.method,
|
|
794
|
+
headers: this.#buildRequestHeaders(options.headers),
|
|
795
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : null
|
|
796
|
+
});
|
|
797
|
+
return this.#signer.sign(request);
|
|
798
|
+
}
|
|
799
|
+
async #request(options, schema) {
|
|
800
|
+
return this.#dispatch(await this.#signedRequest(options), schema);
|
|
801
|
+
}
|
|
802
|
+
async #dispatch(request, schema) {
|
|
803
|
+
const controller = new AbortController();
|
|
804
|
+
const timedRequest = new Request(request, { signal: controller.signal });
|
|
805
|
+
let timeoutId;
|
|
806
|
+
try {
|
|
807
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
808
|
+
timeoutId = setTimeout(() => {
|
|
809
|
+
const timeoutError = new TimeoutError(this.#timeoutMs);
|
|
810
|
+
reject(timeoutError);
|
|
811
|
+
controller.abort(timeoutError);
|
|
812
|
+
}, this.#timeoutMs);
|
|
813
|
+
});
|
|
814
|
+
const exchange = this.#fetch(timedRequest).then((response) => parseApiResponse(response, schema));
|
|
815
|
+
return await Promise.race([exchange, timeout]);
|
|
816
|
+
} finally {
|
|
817
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async whoami() {
|
|
821
|
+
return (await this.#request({
|
|
822
|
+
method: "GET",
|
|
823
|
+
path: "/me"
|
|
824
|
+
}, whoamiResponseSchema)).agent;
|
|
825
|
+
}
|
|
826
|
+
async submitFeedback(body) {
|
|
827
|
+
await this.#request({
|
|
828
|
+
method: "POST",
|
|
829
|
+
path: "/feedback",
|
|
830
|
+
body: { body }
|
|
831
|
+
}, okSuccessResponseSchema);
|
|
832
|
+
}
|
|
833
|
+
async getPolicy() {
|
|
834
|
+
return (await this.#request({
|
|
835
|
+
method: "GET",
|
|
836
|
+
path: "/policy"
|
|
837
|
+
}, policyResponseSchema)).policy;
|
|
838
|
+
}
|
|
839
|
+
async listAccounts() {
|
|
840
|
+
return this.#request({
|
|
841
|
+
method: "GET",
|
|
842
|
+
path: "/accounts"
|
|
843
|
+
}, accountsResponseSchema);
|
|
844
|
+
}
|
|
845
|
+
async getAccountBalance(accountId) {
|
|
846
|
+
return this.#request({
|
|
847
|
+
method: "GET",
|
|
848
|
+
path: `/accounts/${encodeURIComponent(accountId)}/balance`
|
|
849
|
+
}, accountBalanceResponseSchema);
|
|
850
|
+
}
|
|
851
|
+
async listAccountTransactions(accountId, params = {}) {
|
|
852
|
+
return this.#request({
|
|
853
|
+
method: "GET",
|
|
854
|
+
path: `/accounts/${encodeURIComponent(accountId)}/transactions`,
|
|
855
|
+
query: {
|
|
856
|
+
start: params.start,
|
|
857
|
+
end: params.end,
|
|
858
|
+
limit: params.limit?.toString(),
|
|
859
|
+
offset: params.offset?.toString()
|
|
860
|
+
}
|
|
861
|
+
}, accountTransactionsResponseSchema);
|
|
862
|
+
}
|
|
863
|
+
async getAccountDepositAddress(accountId, params) {
|
|
864
|
+
return this.#request({
|
|
865
|
+
method: "GET",
|
|
866
|
+
path: `/accounts/${encodeURIComponent(accountId)}/deposit-address`,
|
|
867
|
+
query: {
|
|
868
|
+
network: params.network,
|
|
869
|
+
asset: params.asset
|
|
870
|
+
}
|
|
871
|
+
}, accountDepositAddressResponseSchema);
|
|
872
|
+
}
|
|
873
|
+
async listCounterparties(params = {}) {
|
|
874
|
+
return this.#request({
|
|
875
|
+
method: "GET",
|
|
876
|
+
path: "/counterparties",
|
|
877
|
+
query: {
|
|
878
|
+
address: params.address,
|
|
879
|
+
network: params.network
|
|
880
|
+
}
|
|
881
|
+
}, counterpartiesResponseSchema);
|
|
882
|
+
}
|
|
883
|
+
async submitIntent(params) {
|
|
884
|
+
const created = await this.#createIntentRaw({
|
|
885
|
+
...params,
|
|
886
|
+
idempotencyKey: params.idempotencyKey ?? crypto.randomUUID()
|
|
887
|
+
});
|
|
888
|
+
const nextAction = created.nextAction;
|
|
889
|
+
if (!nextAction) return toIntentResult(created);
|
|
890
|
+
let submitRequest;
|
|
891
|
+
try {
|
|
892
|
+
const stamp = await this.#stampNextAction(nextAction, params.action);
|
|
893
|
+
submitRequest = await this.#signedRequest({
|
|
894
|
+
method: "POST",
|
|
895
|
+
path: `/intents/${encodeURIComponent(created.id)}/submit-stamp`,
|
|
896
|
+
body: {
|
|
897
|
+
preparedBody: nextAction.preparedBody,
|
|
898
|
+
stamp
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
} catch (cause) {
|
|
902
|
+
throw new IntentSubmitError({
|
|
903
|
+
intentId: created.id,
|
|
904
|
+
outcome: "not-submitted",
|
|
905
|
+
cause
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
return await this.#submitStamp(submitRequest, created.replayed);
|
|
910
|
+
} catch (cause) {
|
|
911
|
+
throw new IntentSubmitError({
|
|
912
|
+
intentId: created.id,
|
|
913
|
+
outcome: "unknown",
|
|
914
|
+
cause
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async #createIntentRaw(params) {
|
|
919
|
+
return this.#request({
|
|
920
|
+
method: "POST",
|
|
921
|
+
path: "/intents",
|
|
922
|
+
body: { action: toWireAction(params.action) },
|
|
923
|
+
headers: { "Idempotency-Key": params.idempotencyKey }
|
|
924
|
+
}, wireIntentResultSchema);
|
|
925
|
+
}
|
|
926
|
+
async #stampNextAction(nextAction, action) {
|
|
927
|
+
if (nextAction.signer.publicKeyHex.toLowerCase() !== this.#keypair.publicKeyHex.toLowerCase()) throw new Error(`Local signer public key does not match requested signer ${nextAction.signer.publicKeyHex}; linked credential has ${this.#keypair.publicKeyHex}`);
|
|
928
|
+
await this.#reviewMovement(nextAction, action);
|
|
929
|
+
const stamp = await new ApiKeyStamper({
|
|
930
|
+
apiPublicKey: this.#keypair.publicKeyHex,
|
|
931
|
+
apiPrivateKey: this.#keypair.privateKeyHex
|
|
932
|
+
}).stamp(nextAction.preparedBody.body);
|
|
933
|
+
return {
|
|
934
|
+
publicKeyHex: this.#keypair.publicKeyHex,
|
|
935
|
+
stampHeaderName: stamp.stampHeaderName,
|
|
936
|
+
stampHeaderValue: stamp.stampHeaderValue
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
async #reviewMovement(nextAction, action) {
|
|
940
|
+
const movement = nextAction.movement;
|
|
941
|
+
if (movement === void 0) {
|
|
942
|
+
if (signsADigestBatch(nextAction.preparedBody.body)) throw new Error("Refusing to approve this payment: it would sign pre-hashed digests with nothing describing what they commit to. Nothing was signed.");
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const movements = await loadMovements();
|
|
946
|
+
if (movements === void 0) throw new Error("Refusing to approve this payment: its digests cannot be checked without the optional `viem` peer dependency (>=2.24.0). Nothing was signed.", { cause: movementsLoadFailure });
|
|
947
|
+
const { verifyPreparedMovement } = movements;
|
|
948
|
+
const verdict = verifyPreparedMovement({
|
|
949
|
+
signerAddress: movement.signerAddress,
|
|
950
|
+
digests: movement.digests,
|
|
951
|
+
instructions: movement.instructions,
|
|
952
|
+
body: nextAction.preparedBody.body
|
|
953
|
+
}, action !== void 0 && "amount" in action ? { amount: action.amount } : void 0);
|
|
954
|
+
if (!verdict.ok) throw new Error(`Refusing to approve this payment: ${verdict.reason} Nothing was signed.`);
|
|
955
|
+
}
|
|
956
|
+
async #submitStamp(request, replayed) {
|
|
957
|
+
const intent = toIntentResult((await this.#dispatch(request, submitStampResponseSchema)).intent);
|
|
958
|
+
return replayed ? {
|
|
959
|
+
...intent,
|
|
960
|
+
replayed: true
|
|
961
|
+
} : intent;
|
|
962
|
+
}
|
|
963
|
+
async getIntent(id) {
|
|
964
|
+
return toIntentResult(await this.#request({
|
|
965
|
+
method: "GET",
|
|
966
|
+
path: `/intents/${encodeURIComponent(id)}`
|
|
967
|
+
}, wireIntentResultSchema));
|
|
968
|
+
}
|
|
969
|
+
async continueIntent(params) {
|
|
970
|
+
const prepared = await this.#request({
|
|
971
|
+
method: "POST",
|
|
972
|
+
path: `/intents/${encodeURIComponent(params.intentId)}/continue`,
|
|
973
|
+
headers: {
|
|
974
|
+
"Idempotency-Key": params.idempotencyKey ?? crypto.randomUUID(),
|
|
975
|
+
"Catena-Continuation-Kinds": await continuationKinds()
|
|
976
|
+
}
|
|
977
|
+
}, continueIntentResponseSchema);
|
|
978
|
+
let submitRequest;
|
|
979
|
+
try {
|
|
980
|
+
const stamp = await this.#stampNextAction(prepared.nextAction, params.action);
|
|
981
|
+
submitRequest = await this.#signedRequest({
|
|
982
|
+
method: "POST",
|
|
983
|
+
path: `/intents/${encodeURIComponent(params.intentId)}/submit-stamp`,
|
|
984
|
+
body: {
|
|
985
|
+
preparedBody: prepared.nextAction.preparedBody,
|
|
986
|
+
stamp
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
} catch (cause) {
|
|
990
|
+
throw new IntentSubmitError({
|
|
991
|
+
intentId: params.intentId,
|
|
992
|
+
outcome: "not-submitted",
|
|
993
|
+
cause
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
return await this.#submitStamp(submitRequest, false);
|
|
998
|
+
} catch (cause) {
|
|
999
|
+
throw new IntentSubmitError({
|
|
1000
|
+
intentId: params.intentId,
|
|
1001
|
+
outcome: "unknown",
|
|
1002
|
+
cause
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async unlinkAgent() {
|
|
1007
|
+
await this.#request({
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
path: "/unlink"
|
|
1010
|
+
}, okSuccessResponseSchema);
|
|
1011
|
+
}
|
|
1012
|
+
async reportSettlement(params) {
|
|
1013
|
+
await this.#request({
|
|
1014
|
+
method: "POST",
|
|
1015
|
+
path: `/intents/${encodeURIComponent(params.intentId)}/settlement-report`,
|
|
1016
|
+
body: {
|
|
1017
|
+
txHash: params.txHash,
|
|
1018
|
+
...params.receipt !== void 0 && { receipt: params.receipt }
|
|
1019
|
+
}
|
|
1020
|
+
}, okSuccessResponseSchema);
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
function createCatenaClient(options = {}) {
|
|
1024
|
+
const privateKeyHex = options.privateKeyHex === void 0 ? process.env.CATENA_SECRET_KEY : options.privateKeyHex;
|
|
1025
|
+
if (privateKeyHex === void 0) throw new InvalidKeyError("privateKeyHex is required; pass it explicitly or set CATENA_SECRET_KEY");
|
|
1026
|
+
return new CatenaClient({
|
|
1027
|
+
...options,
|
|
1028
|
+
privateKeyHex
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
//#endregion
|
|
1032
|
+
export { walletCounterpartyRailSchema as C, extractRelayableOffer as D, x402ResourceSchema as E, extractRelayableReceipt as O, SEND_METHODS as S, x402PaymentRequirementsSchema as T, MOVEMENT_NEXT_STEPS as _, ACCOUNT_AGGREGATION_SCOPES as a, POLICY_RULE_ACTIONS as b, ACCOUNT_TRANSACTION_MOVEMENT_STATUSES as c, ACTOR_AGGREGATION_SCOPES as d, COUNTERPARTY_ACTIONS as f, INTENT_ACTION_TYPES as g, CROSS_CHAIN_STATES as h, createCatenaClient as i, paymentCredentialFromIntentData as k, ACCOUNT_TRANSACTION_STATUSES as l, CROSS_CHAIN_NEXT_STEPS as m, IntentSubmitError as n, ACCOUNT_DEPOSIT_ADDRESS_SOURCES as o, COUNTERPARTY_RULE_MODES as p, TimeoutError as r, ACCOUNT_TRANSACTION_METHODS as s, ApiError as t, ACCOUNT_TRANSACTION_TYPES as u, POLICY_CAPABILITIES as v, x402CredentialFromIntentData as w, POLICY_RULE_TYPES as x, POLICY_OVERRIDE_CAPABILITIES as y };
|