@cavos/cli 0.0.1

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/dist/cli.js ADDED
@@ -0,0 +1,1918 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_commander = require("commander");
28
+
29
+ // src/CavosAgent.ts
30
+ var import_starknet7 = require("starknet");
31
+
32
+ // src/core/SessionKeyManager.ts
33
+ var import_starknet3 = require("starknet");
34
+
35
+ // src/utils/crypto.ts
36
+ var import_crypto = require("crypto");
37
+ function getRandomBytes(length) {
38
+ return new Uint8Array((0, import_crypto.randomBytes)(length));
39
+ }
40
+ function randomFieldElement() {
41
+ const bytes = getRandomBytes(32);
42
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
43
+ return BigInt("0x" + hex) % 2n ** 251n;
44
+ }
45
+
46
+ // src/utils/encoding.ts
47
+ var import_starknet = require("starknet");
48
+ function base64UrlToBase64(base64url) {
49
+ return base64url.replace(/-/g, "+").replace(/_/g, "/").padEnd(base64url.length + (4 - base64url.length % 4) % 4, "=");
50
+ }
51
+ function base64UrlToBytes(base64url) {
52
+ const base64 = base64UrlToBase64(base64url);
53
+ const buf = Buffer.from(base64, "base64");
54
+ return new Uint8Array(buf);
55
+ }
56
+ function bytesToU128Limbs(bytes) {
57
+ const limbs = [];
58
+ for (let i = 15; i >= 0; i--) {
59
+ let limb = 0n;
60
+ for (let j = 0; j < 16; j++) {
61
+ const byteIdx = i * 16 + j;
62
+ if (byteIdx < bytes.length) {
63
+ limb = limb * 256n + BigInt(bytes[byteIdx]);
64
+ }
65
+ }
66
+ limbs.push(import_starknet.num.toHex(limb));
67
+ }
68
+ return limbs;
69
+ }
70
+ function subToFelt(sub) {
71
+ try {
72
+ const subBigInt = BigInt(sub);
73
+ if (subBigInt < 2n ** 251n) {
74
+ return import_starknet.num.toHex(subBigInt);
75
+ }
76
+ } catch {
77
+ }
78
+ return stringToFelt(sub);
79
+ }
80
+ function stringToFelt(str) {
81
+ const bytes = Buffer.from(str, "utf-8");
82
+ let result = 0n;
83
+ for (let i = 0; i < bytes.length && i < 31; i++) {
84
+ result = result * 256n + BigInt(bytes[i]);
85
+ }
86
+ return import_starknet.num.toHex(result);
87
+ }
88
+ function parseJWT(jwt) {
89
+ const parts = jwt.split(".");
90
+ if (parts.length !== 3) {
91
+ throw new Error("Invalid JWT format");
92
+ }
93
+ const payload = JSON.parse(Buffer.from(base64UrlToBase64(parts[1]), "base64").toString("utf-8"));
94
+ return {
95
+ sub: payload.sub,
96
+ nonce: payload.nonce,
97
+ exp: payload.exp,
98
+ iss: payload.iss,
99
+ aud: Array.isArray(payload.aud) ? payload.aud[0] : payload.aud
100
+ };
101
+ }
102
+ function extractKidFromJwt(jwt) {
103
+ const parts = jwt.split(".");
104
+ const header = JSON.parse(Buffer.from(base64UrlToBase64(parts[0]), "base64").toString("utf-8"));
105
+ return header.kid || "";
106
+ }
107
+ function findClaimOffsets(jwt) {
108
+ const parts = jwt.split(".");
109
+ const headerJson = JSON.parse(Buffer.from(base64UrlToBase64(parts[0]), "base64").toString("utf-8"));
110
+ const payloadJson = JSON.parse(Buffer.from(base64UrlToBase64(parts[1]), "base64").toString("utf-8"));
111
+ const subValue = payloadJson.sub || "";
112
+ const nonceValue = payloadJson.nonce || "";
113
+ const kidValue = headerJson.kid || "";
114
+ const decodedPayload = Buffer.from(base64UrlToBase64(parts[1]), "base64").toString("utf-8");
115
+ const decodedHeader = Buffer.from(base64UrlToBase64(parts[0]), "base64").toString("utf-8");
116
+ const findClaimValueOffset = (decoded, key, value) => {
117
+ const exactPattern = `"${key}":"${value}"`;
118
+ let idx = decoded.indexOf(exactPattern);
119
+ if (idx >= 0) return idx + key.length + 4;
120
+ const spacedPattern = `"${key}": "${value}"`;
121
+ idx = decoded.indexOf(spacedPattern);
122
+ if (idx >= 0) return idx + key.length + 5;
123
+ const keyPattern = `"${key}"`;
124
+ idx = decoded.indexOf(keyPattern);
125
+ if (idx >= 0) {
126
+ const colonIdx = decoded.indexOf(":", idx + key.length + 2);
127
+ if (colonIdx >= 0) {
128
+ const valueQuoteIdx = decoded.indexOf('"', colonIdx + 1);
129
+ if (valueQuoteIdx >= 0) return valueQuoteIdx + 1;
130
+ }
131
+ }
132
+ return -1;
133
+ };
134
+ const subValueStart = findClaimValueOffset(decodedPayload, "sub", subValue);
135
+ if (subValueStart < 0) throw new Error("Failed to find sub claim in JWT payload");
136
+ const nonceValueStart = findClaimValueOffset(decodedPayload, "nonce", nonceValue);
137
+ if (nonceValueStart < 0) throw new Error("Failed to find nonce claim in JWT payload");
138
+ const kidValueStart = findClaimValueOffset(decodedHeader, "kid", kidValue);
139
+ if (kidValueStart < 0) throw new Error("Failed to find kid claim in JWT header");
140
+ return {
141
+ sub_offset: subValueStart,
142
+ sub_len: subValue.length,
143
+ nonce_offset: nonceValueStart,
144
+ nonce_len: nonceValue.length,
145
+ kid_offset: kidValueStart,
146
+ kid_len: kidValue.length
147
+ };
148
+ }
149
+
150
+ // src/core/MerkleTree.ts
151
+ var import_starknet2 = require("starknet");
152
+ function computeMerkleRoot(contracts) {
153
+ if (contracts.length === 0) return "0x0";
154
+ let leaves = contracts.map(
155
+ (c) => import_starknet2.hash.computePoseidonHashOnElements([import_starknet2.num.toHex(c)])
156
+ );
157
+ leaves.sort((a, b) => {
158
+ const aBig = BigInt(a);
159
+ const bBig = BigInt(b);
160
+ if (aBig < bBig) return -1;
161
+ if (aBig > bBig) return 1;
162
+ return 0;
163
+ });
164
+ while (leaves.length > 1) {
165
+ const nextLevel = [];
166
+ for (let i = 0; i < leaves.length; i += 2) {
167
+ if (i + 1 < leaves.length) {
168
+ const left = leaves[i];
169
+ const right = leaves[i + 1];
170
+ const leftBig = BigInt(left);
171
+ const rightBig = BigInt(right);
172
+ if (leftBig < rightBig) {
173
+ nextLevel.push(import_starknet2.hash.computePoseidonHashOnElements([left, right]));
174
+ } else {
175
+ nextLevel.push(import_starknet2.hash.computePoseidonHashOnElements([right, left]));
176
+ }
177
+ } else {
178
+ nextLevel.push(leaves[i]);
179
+ }
180
+ }
181
+ leaves = nextLevel;
182
+ }
183
+ return leaves[0];
184
+ }
185
+ function computeMerkleProof(contracts, targetContract) {
186
+ if (contracts.length === 0) return [];
187
+ let leaves = contracts.map(
188
+ (c) => import_starknet2.hash.computePoseidonHashOnElements([import_starknet2.num.toHex(c)])
189
+ );
190
+ leaves.sort((a, b) => {
191
+ const aBig = BigInt(a);
192
+ const bBig = BigInt(b);
193
+ if (aBig < bBig) return -1;
194
+ if (aBig > bBig) return 1;
195
+ return 0;
196
+ });
197
+ const targetLeaf = import_starknet2.hash.computePoseidonHashOnElements([import_starknet2.num.toHex(targetContract)]);
198
+ let targetIdx = leaves.indexOf(targetLeaf);
199
+ if (targetIdx === -1) return [];
200
+ const proof = [];
201
+ let currentLevel = [...leaves];
202
+ while (currentLevel.length > 1) {
203
+ const nextLevel = [];
204
+ let nextTargetIdx = -1;
205
+ for (let i = 0; i < currentLevel.length; i += 2) {
206
+ if (i + 1 < currentLevel.length) {
207
+ const left = currentLevel[i];
208
+ const right = currentLevel[i + 1];
209
+ if (i === targetIdx || i + 1 === targetIdx) {
210
+ proof.push(i === targetIdx ? right : left);
211
+ nextTargetIdx = Math.floor(i / 2);
212
+ }
213
+ const leftBig = BigInt(left);
214
+ const rightBig = BigInt(right);
215
+ if (leftBig < rightBig) {
216
+ nextLevel.push(import_starknet2.hash.computePoseidonHashOnElements([left, right]));
217
+ } else {
218
+ nextLevel.push(import_starknet2.hash.computePoseidonHashOnElements([right, left]));
219
+ }
220
+ } else {
221
+ if (i === targetIdx) {
222
+ nextTargetIdx = Math.floor(i / 2);
223
+ }
224
+ nextLevel.push(currentLevel[i]);
225
+ }
226
+ }
227
+ currentLevel = nextLevel;
228
+ targetIdx = nextTargetIdx;
229
+ }
230
+ return proof;
231
+ }
232
+
233
+ // src/utils/constants.ts
234
+ var OAUTH_JWT_V1_MAGIC = "0x4f415554485f4a57545f5631";
235
+ var SESSION_V1_MAGIC = "0x53455353494f4e5f5631";
236
+ var STARK_CURVE_ORDER = BigInt("0x800000000000010ffffffffffffffffb781126dcae7b2321e66a241adc64d2f");
237
+ var TOKENS_SEPOLIA = {
238
+ STRK: "0x04718f5a0Fc34cC1AF16A1cdee98fFB20C31f5cD61D6Ab07201858f4287c938D",
239
+ ETH: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"
240
+ };
241
+ var TOKENS_MAINNET = {
242
+ STRK: "0x04718f5a0Fc34cC1AF16A1cdee98fFB20C31f5cD61D6Ab07201858f4287c938D",
243
+ ETH: "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"
244
+ };
245
+ var DEFAULT_RPC = {
246
+ mainnet: "https://starknet-mainnet.g.alchemy.com/starknet/version/rpc/v0_10/dql5pMT88iueZWl7L0yzT56uVk0EBU4L",
247
+ sepolia: "https://starknet-sepolia.g.alchemy.com/starknet/version/rpc/v0_10/dql5pMT88iueZWl7L0yzT56uVk0EBU4L"
248
+ };
249
+ var DEFAULT_OAUTH_CONFIG_SEPOLIA = {
250
+ jwksRegistryAddress: "0x05a19f14719dec9e27eb2aa38c5b68277bdb5c41570e548504722f737a3da6c6",
251
+ cavosAccountClassHash: "0x40f4075372d7b9b964910755dcdf96935280c8b675272f656b2d43d1ae4bbf4"
252
+ };
253
+ var DEFAULT_OAUTH_CONFIG_MAINNET = {
254
+ jwksRegistryAddress: "0x07787f624d6869ae306dc17b49174b284dbadd1e999c1c8733ce72eb7ac518c2",
255
+ cavosAccountClassHash: "0x40f4075372d7b9b964910755dcdf96935280c8b675272f656b2d43d1ae4bbf4"
256
+ };
257
+ var DEFAULT_BACKEND_URL = "https://cavos.xyz";
258
+ var DEFAULT_PAYMASTER_KEY = "c37c52b7-ea5a-4426-8121-329a78354b0b";
259
+
260
+ // src/core/SessionKeyManager.ts
261
+ function generateSessionKeyPair() {
262
+ const randomBytes2 = getRandomBytes(32);
263
+ let pk = BigInt("0x" + Array.from(randomBytes2).map((b) => b.toString(16).padStart(2, "0")).join(""));
264
+ pk = pk % (STARK_CURVE_ORDER - 1n) + 1n;
265
+ const privateKey = "0x" + pk.toString(16);
266
+ const publicKey = import_starknet3.ec.starkCurve.getStarkKey(privateKey);
267
+ return { privateKey, publicKey };
268
+ }
269
+ function buildSessionSignature(transactionHash, sessionPrivateKey, sessionPubKey, calls, policy2) {
270
+ const signature = import_starknet3.ec.starkCurve.sign(transactionHash, sessionPrivateKey);
271
+ const sig = [
272
+ SESSION_V1_MAGIC,
273
+ import_starknet3.num.toHex(signature.r),
274
+ import_starknet3.num.toHex(signature.s),
275
+ sessionPubKey
276
+ ];
277
+ if (calls && policy2?.allowedContracts?.length) {
278
+ for (const call of calls) {
279
+ const proof = computeMerkleProof(policy2.allowedContracts, call.contractAddress);
280
+ sig.push(import_starknet3.num.toHex(proof.length));
281
+ sig.push(...proof);
282
+ }
283
+ }
284
+ return sig;
285
+ }
286
+ async function buildJWTSignatureData(transactionHash, session2, salt, backendUrl) {
287
+ const { jwt, sessionPrivateKey, sessionPubKey, nonceParams, jwtClaims } = session2;
288
+ const signature = import_starknet3.ec.starkCurve.sign(transactionHash, sessionPrivateKey);
289
+ const jwtParts = jwt.split(".");
290
+ const rsaSignature = base64UrlToBytes(jwtParts[2]);
291
+ const rsaLimbs = bytesToU128Limbs(rsaSignature);
292
+ const signedData = `${jwtParts[0]}.${jwtParts[1]}`;
293
+ const signedDataBytes = Buffer.from(signedData, "utf-8");
294
+ const offsets = findClaimOffsets(jwt);
295
+ const jwt_sub_felt = subToFelt(jwtClaims.sub);
296
+ const salt_hex = import_starknet3.num.toHex(salt);
297
+ const packedBytes = [];
298
+ const PACK_SIZE = 31;
299
+ for (let i = 0; i < signedDataBytes.length; i += PACK_SIZE) {
300
+ let chunk = 0n;
301
+ const end = Math.min(i + PACK_SIZE, signedDataBytes.length);
302
+ for (let j = i; j < end; j++) {
303
+ chunk = chunk * 256n + BigInt(signedDataBytes[j]);
304
+ }
305
+ packedBytes.push(import_starknet3.num.toHex(chunk));
306
+ }
307
+ const kid = extractKidFromJwt(jwt);
308
+ const iss = jwtClaims.iss;
309
+ const modulusLimbs = await fetchModulusForKid(kid, iss, backendUrl);
310
+ const { n_prime, r_sq } = calculateMontgomeryConstants(modulusLimbs);
311
+ const sig = [
312
+ OAUTH_JWT_V1_MAGIC,
313
+ import_starknet3.num.toHex(signature.r),
314
+ import_starknet3.num.toHex(signature.s),
315
+ sessionPubKey,
316
+ import_starknet3.num.toHex(nonceParams.validUntil),
317
+ import_starknet3.num.toHex(nonceParams.randomness),
318
+ jwt_sub_felt,
319
+ session2.nonce,
320
+ import_starknet3.num.toHex(jwtClaims.exp),
321
+ stringToFelt(kid),
322
+ stringToFelt(jwtClaims.iss),
323
+ stringToFelt(jwtClaims.aud),
324
+ salt_hex,
325
+ import_starknet3.num.toHex(offsets.sub_offset),
326
+ import_starknet3.num.toHex(offsets.sub_len),
327
+ import_starknet3.num.toHex(offsets.nonce_offset),
328
+ import_starknet3.num.toHex(offsets.nonce_len),
329
+ import_starknet3.num.toHex(offsets.kid_offset),
330
+ import_starknet3.num.toHex(offsets.kid_len),
331
+ import_starknet3.num.toHex(16),
332
+ ...rsaLimbs,
333
+ import_starknet3.num.toHex(16),
334
+ ...n_prime,
335
+ import_starknet3.num.toHex(16),
336
+ ...r_sq,
337
+ import_starknet3.num.toHex(signedDataBytes.length),
338
+ ...packedBytes
339
+ ];
340
+ sig.push(import_starknet3.num.toHex(nonceParams.validAfter));
341
+ const policy2 = session2.sessionPolicy;
342
+ if (policy2) {
343
+ const merkleRoot = policy2.allowedContracts.length > 0 ? computeMerkleRoot(policy2.allowedContracts) : "0x0";
344
+ sig.push(merkleRoot);
345
+ sig.push(import_starknet3.num.toHex(policy2.maxCallsPerTx));
346
+ sig.push(import_starknet3.num.toHex(policy2.spendingLimits.length));
347
+ for (const limit of policy2.spendingLimits) {
348
+ sig.push(import_starknet3.num.toHex(limit.token));
349
+ const limitBig = BigInt(limit.limit);
350
+ sig.push(import_starknet3.num.toHex(limitBig & (1n << 128n) - 1n));
351
+ sig.push(import_starknet3.num.toHex(limitBig >> 128n));
352
+ }
353
+ } else {
354
+ sig.push("0x0");
355
+ sig.push(import_starknet3.num.toHex(10));
356
+ sig.push(import_starknet3.num.toHex(0));
357
+ }
358
+ return sig;
359
+ }
360
+ async function fetchModulusForKid(kid, issuer, backendUrl) {
361
+ let jwksUrl = "https://www.googleapis.com/oauth2/v3/certs";
362
+ if (issuer === "https://appleid.apple.com") {
363
+ jwksUrl = "https://appleid.apple.com/auth/keys";
364
+ } else if (issuer === "https://cavos.app/firebase") {
365
+ jwksUrl = `${backendUrl}/api/jwks/firebase`;
366
+ }
367
+ const response = await fetch(jwksUrl);
368
+ const data = await response.json();
369
+ const jwks = data.jwks || data;
370
+ if (!jwks.keys || !Array.isArray(jwks.keys)) {
371
+ throw new Error(`Invalid JWKS response from ${jwksUrl}`);
372
+ }
373
+ const key = jwks.keys.find((k) => k.kid === kid);
374
+ if (!key || !key.n) {
375
+ throw new Error(`Key not found for kid: ${kid}`);
376
+ }
377
+ const modulusBytes = base64UrlToBytes(key.n);
378
+ const limbs = bytesToU128Limbs(modulusBytes);
379
+ return limbs.map((l) => BigInt(l));
380
+ }
381
+ function calculateMontgomeryConstants(n_limbs) {
382
+ let n = 0n;
383
+ for (let i = 0; i < n_limbs.length; i++) {
384
+ n += n_limbs[i] * (1n << BigInt(i) * 128n);
385
+ }
386
+ const R = 1n << 2048n;
387
+ function modInverse(a, mod) {
388
+ let t = 0n, newt = 1n, r = mod, newr = a;
389
+ while (newr !== 0n) {
390
+ const q = r / newr;
391
+ [t, newt] = [newt, t - q * newt];
392
+ [r, newr] = [newr, r - q * newr];
393
+ }
394
+ if (r > 1n) throw new Error("n is not invertible");
395
+ if (t < 0n) t += mod;
396
+ return t;
397
+ }
398
+ const n_inv = modInverse(n, R);
399
+ const n_prime_val = (R - n_inv) % R;
400
+ const r_sq_val = R * R % n;
401
+ const toLimbs = (val) => {
402
+ const limbs = [];
403
+ for (let i = 0; i < 16; i++) {
404
+ const limb = val >> BigInt(i) * 128n & (1n << 128n) - 1n;
405
+ limbs.push(import_starknet3.num.toHex(limb));
406
+ }
407
+ return limbs;
408
+ };
409
+ return { n_prime: toLimbs(n_prime_val), r_sq: toLimbs(r_sq_val) };
410
+ }
411
+
412
+ // src/core/NonceManager.ts
413
+ var import_starknet4 = require("starknet");
414
+ function computeNonce(params) {
415
+ return import_starknet4.hash.computePoseidonHashOnElements([
416
+ params.sessionPubKey,
417
+ import_starknet4.num.toHex(params.validUntil),
418
+ import_starknet4.num.toHex(params.randomness)
419
+ ]);
420
+ }
421
+ function generateNonceParams(sessionPubKey, currentTimestamp, sessionDurationSeconds = 86400n, renewalGraceSeconds = 172800n) {
422
+ const randomness = randomFieldElement();
423
+ return {
424
+ sessionPubKey,
425
+ validAfter: currentTimestamp,
426
+ validUntil: currentTimestamp + sessionDurationSeconds,
427
+ renewalDeadline: currentTimestamp + renewalGraceSeconds,
428
+ randomness
429
+ };
430
+ }
431
+
432
+ // src/core/AddressSeedManager.ts
433
+ var import_starknet5 = require("starknet");
434
+ function computeAddressSeed(sub, salt, walletName) {
435
+ const subFeltVal = subToFelt(sub);
436
+ let saltFelt = import_starknet5.num.toHex(salt);
437
+ if (walletName) {
438
+ const nameFelt = stringToFelt(walletName);
439
+ saltFelt = import_starknet5.hash.computePoseidonHashOnElements([saltFelt, nameFelt]);
440
+ }
441
+ return import_starknet5.hash.computePoseidonHashOnElements([subFeltVal, saltFelt]);
442
+ }
443
+ function computeContractAddress(sub, salt, classHash, jwksRegistryAddress, walletName) {
444
+ const addressSeed = computeAddressSeed(sub, salt, walletName);
445
+ const constructorCalldata = [addressSeed, jwksRegistryAddress];
446
+ return import_starknet5.hash.calculateContractAddressFromHash(
447
+ addressSeed,
448
+ classHash,
449
+ constructorCalldata,
450
+ 0
451
+ );
452
+ }
453
+
454
+ // src/core/TransactionManager.ts
455
+ var import_starknet6 = require("starknet");
456
+ async function isDeployed(provider, address) {
457
+ try {
458
+ const classHash = await provider.getClassHashAt(address, "latest");
459
+ return !!classHash;
460
+ } catch {
461
+ return false;
462
+ }
463
+ }
464
+ async function getSessionStatus(provider, walletAddress, sessionPubKey) {
465
+ try {
466
+ const result = await provider.callContract({
467
+ contractAddress: walletAddress,
468
+ entrypoint: "get_session",
469
+ calldata: [sessionPubKey]
470
+ }, "latest");
471
+ const nonce = BigInt(result[0]);
472
+ const validUntil = BigInt(result[2]);
473
+ const renewalDeadline = BigInt(result[3]);
474
+ const registered = nonce !== 0n;
475
+ if (!registered) {
476
+ return { registered: false, expired: false, canRenew: false };
477
+ }
478
+ const block = await provider.getBlock("latest");
479
+ const now = BigInt(block.timestamp);
480
+ const expired = now >= validUntil;
481
+ const canRenew = expired && now < renewalDeadline;
482
+ return { registered, expired, canRenew, validUntil, renewalDeadline };
483
+ } catch {
484
+ return { registered: false, expired: false, canRenew: false };
485
+ }
486
+ }
487
+ async function deployAccount(provider, session2, classHash, jwksRegistryAddress, salt, paymasterApiKey, network, backendUrl) {
488
+ const deployed = await isDeployed(provider, session2.walletAddress);
489
+ if (deployed) return "already-deployed";
490
+ const constructorCalldata = [
491
+ import_starknet6.num.toHex(session2.addressSeed),
492
+ import_starknet6.num.toHex(jwksRegistryAddress)
493
+ ];
494
+ const baseUrl = network === "mainnet" ? "https://starknet.api.avnu.fi" : "https://sepolia.api.avnu.fi";
495
+ const buildResponse = await fetch(`${baseUrl}/paymaster/v1/build-typed-data`, {
496
+ method: "POST",
497
+ headers: {
498
+ "Content-Type": "application/json",
499
+ "api-key": paymasterApiKey
500
+ },
501
+ body: JSON.stringify({
502
+ userAddress: session2.walletAddress,
503
+ calls: [],
504
+ accountClassHash: classHash,
505
+ accountCalldata: constructorCalldata
506
+ })
507
+ });
508
+ if (!buildResponse.ok) {
509
+ const errText = await buildResponse.text();
510
+ if (errText.includes("already deployed")) return "already-deployed";
511
+ throw new Error(`Deploy build-typed-data failed: ${errText}`);
512
+ }
513
+ const paymasterTypedData = await buildResponse.json();
514
+ const messageHash = computeTypedDataHash(paymasterTypedData, session2.walletAddress);
515
+ const signature = await buildJWTSignatureData(messageHash, session2, salt, backendUrl);
516
+ const executeResponse = await fetch(`${baseUrl}/paymaster/v1/execute`, {
517
+ method: "POST",
518
+ headers: {
519
+ "Content-Type": "application/json",
520
+ "api-key": paymasterApiKey
521
+ },
522
+ body: JSON.stringify({
523
+ userAddress: session2.walletAddress,
524
+ typedData: JSON.stringify(paymasterTypedData),
525
+ signature
526
+ })
527
+ });
528
+ if (!executeResponse.ok) {
529
+ const errText = await executeResponse.text();
530
+ if (errText.includes("already deployed")) return "already-deployed";
531
+ throw new Error(`Deploy execute failed: ${errText}`);
532
+ }
533
+ const result = await executeResponse.json();
534
+ await provider.waitForTransaction(result.transactionHash);
535
+ return result.transactionHash;
536
+ }
537
+ async function execute(provider, session2, calls, salt, paymasterApiKey, network, backendUrl) {
538
+ const callsArray = Array.isArray(calls) ? calls : [calls];
539
+ const status = await getSessionStatus(provider, session2.walletAddress, session2.sessionPubKey);
540
+ if (!status.registered) {
541
+ return executeWithAVNU(provider, session2, callsArray, salt, paymasterApiKey, network, backendUrl, true);
542
+ }
543
+ if (status.expired && status.canRenew) {
544
+ throw new Error("SESSION_RENEWABLE: Session expired but can be renewed. Call renewSession() first.");
545
+ }
546
+ if (status.expired && !status.canRenew) {
547
+ throw new Error("SESSION_EXPIRED: Session expired outside grace period. Please login again.");
548
+ }
549
+ return executeWithAVNU(provider, session2, callsArray, salt, paymasterApiKey, network, backendUrl, false);
550
+ }
551
+ async function executeWithAVNU(provider, session2, calls, salt, paymasterApiKey, network, backendUrl, forceJWT) {
552
+ const baseUrl = network === "mainnet" ? "https://starknet.api.avnu.fi" : "https://sepolia.api.avnu.fi";
553
+ const formattedCalls = calls.map((call) => ({
554
+ contractAddress: call.contractAddress,
555
+ entrypoint: call.entrypoint,
556
+ calldata: call.calldata ? call.calldata.map((c) => import_starknet6.num.toHex(c)) : []
557
+ }));
558
+ const buildResponse = await fetch(`${baseUrl}/paymaster/v1/build-typed-data`, {
559
+ method: "POST",
560
+ headers: {
561
+ "Content-Type": "application/json",
562
+ "api-key": paymasterApiKey
563
+ },
564
+ body: JSON.stringify({
565
+ userAddress: session2.walletAddress,
566
+ calls: formattedCalls
567
+ })
568
+ });
569
+ if (!buildResponse.ok) {
570
+ throw new Error(`Build typed data failed: ${await buildResponse.text()}`);
571
+ }
572
+ const paymasterTypedData = await buildResponse.json();
573
+ const messageHash = computeTypedDataHash(paymasterTypedData, session2.walletAddress);
574
+ const signature = forceJWT ? await buildJWTSignatureData(messageHash, session2, salt, backendUrl) : buildSessionSignature(
575
+ messageHash,
576
+ session2.sessionPrivateKey,
577
+ session2.sessionPubKey,
578
+ calls.map((c) => ({ contractAddress: c.contractAddress })),
579
+ session2.sessionPolicy
580
+ );
581
+ const executeResponse = await fetch(`${baseUrl}/paymaster/v1/execute`, {
582
+ method: "POST",
583
+ headers: {
584
+ "Content-Type": "application/json",
585
+ "api-key": paymasterApiKey
586
+ },
587
+ body: JSON.stringify({
588
+ userAddress: session2.walletAddress,
589
+ typedData: JSON.stringify(paymasterTypedData),
590
+ signature
591
+ })
592
+ });
593
+ if (!executeResponse.ok) {
594
+ const errorText = await executeResponse.text();
595
+ if (errorText.includes("Session expired")) {
596
+ throw new Error("SESSION_EXPIRED: Session has expired. Call renewSession() first.");
597
+ }
598
+ throw new Error(`Execute failed: ${errorText}`);
599
+ }
600
+ const result = await executeResponse.json();
601
+ return result.transactionHash;
602
+ }
603
+ async function renewSession(provider, oldSession, newSession, paymasterApiKey, network) {
604
+ const policy2 = newSession.sessionPolicy;
605
+ const allowedContractsRoot = policy2?.allowedContracts?.length ? computeMerkleRoot(policy2.allowedContracts) : "0x0";
606
+ const maxCallsPerTx = policy2?.maxCallsPerTx ?? 10;
607
+ const message = import_starknet6.hash.computePoseidonHashOnElements([
608
+ newSession.sessionPubKey,
609
+ newSession.nonce,
610
+ import_starknet6.num.toHex(newSession.nonceParams.validAfter),
611
+ import_starknet6.num.toHex(newSession.nonceParams.validUntil),
612
+ import_starknet6.num.toHex(newSession.nonceParams.renewalDeadline),
613
+ allowedContractsRoot,
614
+ import_starknet6.num.toHex(maxCallsPerTx)
615
+ ]);
616
+ const oldSignature = import_starknet6.ec.starkCurve.sign(message, oldSession.sessionPrivateKey);
617
+ const spendingCalldata = [];
618
+ if (policy2?.spendingLimits?.length) {
619
+ spendingCalldata.push(import_starknet6.num.toHex(policy2.spendingLimits.length));
620
+ for (const limit of policy2.spendingLimits) {
621
+ spendingCalldata.push(import_starknet6.num.toHex(limit.token));
622
+ const limitBig = BigInt(limit.limit);
623
+ spendingCalldata.push(import_starknet6.num.toHex(limitBig & (1n << 128n) - 1n));
624
+ spendingCalldata.push(import_starknet6.num.toHex(limitBig >> 128n));
625
+ }
626
+ } else {
627
+ spendingCalldata.push(import_starknet6.num.toHex(0));
628
+ }
629
+ const renewCall = {
630
+ contractAddress: oldSession.walletAddress,
631
+ entrypoint: "renew_session",
632
+ calldata: [
633
+ oldSession.sessionPubKey,
634
+ import_starknet6.num.toHex(oldSignature.r),
635
+ import_starknet6.num.toHex(oldSignature.s),
636
+ newSession.sessionPubKey,
637
+ newSession.nonce,
638
+ import_starknet6.num.toHex(newSession.nonceParams.validAfter),
639
+ import_starknet6.num.toHex(newSession.nonceParams.validUntil),
640
+ import_starknet6.num.toHex(newSession.nonceParams.renewalDeadline),
641
+ allowedContractsRoot,
642
+ import_starknet6.num.toHex(maxCallsPerTx),
643
+ ...spendingCalldata
644
+ ]
645
+ };
646
+ const baseUrl = network === "mainnet" ? "https://starknet.api.avnu.fi" : "https://sepolia.api.avnu.fi";
647
+ const buildResponse = await fetch(`${baseUrl}/paymaster/v1/build-typed-data`, {
648
+ method: "POST",
649
+ headers: {
650
+ "Content-Type": "application/json",
651
+ "api-key": paymasterApiKey
652
+ },
653
+ body: JSON.stringify({
654
+ userAddress: oldSession.walletAddress,
655
+ calls: [{
656
+ contractAddress: renewCall.contractAddress,
657
+ entrypoint: renewCall.entrypoint,
658
+ calldata: renewCall.calldata
659
+ }]
660
+ })
661
+ });
662
+ if (!buildResponse.ok) {
663
+ throw new Error(`Renew build-typed-data failed: ${await buildResponse.text()}`);
664
+ }
665
+ const paymasterTypedData = await buildResponse.json();
666
+ const messageHash = computeTypedDataHash(paymasterTypedData, oldSession.walletAddress);
667
+ const signature = buildSessionSignature(
668
+ messageHash,
669
+ oldSession.sessionPrivateKey,
670
+ oldSession.sessionPubKey
671
+ );
672
+ const executeResponse = await fetch(`${baseUrl}/paymaster/v1/execute`, {
673
+ method: "POST",
674
+ headers: {
675
+ "Content-Type": "application/json",
676
+ "api-key": paymasterApiKey
677
+ },
678
+ body: JSON.stringify({
679
+ userAddress: oldSession.walletAddress,
680
+ typedData: JSON.stringify(paymasterTypedData),
681
+ signature
682
+ })
683
+ });
684
+ if (!executeResponse.ok) {
685
+ const errorText = await executeResponse.text();
686
+ if (errorText.includes("Renewal period expired")) {
687
+ throw new Error("Grace period expired. Please login again.");
688
+ }
689
+ throw new Error(`Renew session failed: ${errorText}`);
690
+ }
691
+ const result = await executeResponse.json();
692
+ return result.transactionHash;
693
+ }
694
+ async function revokeSession(provider, session2, sessionKeyToRevoke, salt, paymasterApiKey, network, backendUrl) {
695
+ const revokeCall = {
696
+ contractAddress: session2.walletAddress,
697
+ entrypoint: "revoke_session",
698
+ calldata: [sessionKeyToRevoke]
699
+ };
700
+ return execute(provider, session2, [revokeCall], salt, paymasterApiKey, network, backendUrl);
701
+ }
702
+ async function emergencyRevokeAllSessions(provider, session2, salt, paymasterApiKey, network, backendUrl) {
703
+ const revokeCall = {
704
+ contractAddress: session2.walletAddress,
705
+ entrypoint: "emergency_revoke",
706
+ calldata: []
707
+ };
708
+ return execute(provider, session2, [revokeCall], salt, paymasterApiKey, network, backendUrl);
709
+ }
710
+ async function getBalance(provider, tokenAddress, walletAddress) {
711
+ const result = await provider.callContract({
712
+ contractAddress: tokenAddress,
713
+ entrypoint: "balanceOf",
714
+ calldata: [walletAddress]
715
+ });
716
+ const low = BigInt(result[0]);
717
+ const high = BigInt(result[1]);
718
+ return low + (high << 128n);
719
+ }
720
+ function computeTypedDataHash(paymasterTypedData, address) {
721
+ return import_starknet6.typedData.getMessageHash(paymasterTypedData, address);
722
+ }
723
+
724
+ // src/auth/FirebaseAuth.ts
725
+ async function firebaseLogin(backendUrl, appId, email, password, nonce) {
726
+ const response = await fetch(`${backendUrl}/api/oauth/firebase/login`, {
727
+ method: "POST",
728
+ headers: { "Content-Type": "application/json" },
729
+ body: JSON.stringify({ email, password, nonce, app_id: appId })
730
+ });
731
+ if (!response.ok) {
732
+ const error = await response.json().catch(() => ({ error: "Login failed" }));
733
+ if (error.error === "email_not_verified") {
734
+ throw new Error("Email not verified. Please verify your email on agent.cavos.xyz first.");
735
+ }
736
+ throw new Error(error.error || "Login failed");
737
+ }
738
+ const { jwt } = await response.json();
739
+ const claims = parseJWT(jwt);
740
+ if (claims.nonce !== nonce) {
741
+ throw new Error("JWT nonce mismatch. Possible replay attack.");
742
+ }
743
+ return { jwt, claims };
744
+ }
745
+ async function validateApp(backendUrl, appId, network) {
746
+ try {
747
+ const response = await fetch(
748
+ `${backendUrl}/api/apps/${appId}/validate?network=${network}`
749
+ );
750
+ if (!response.ok) return { allowed: true };
751
+ const result = await response.json();
752
+ return { allowed: result.allowed !== false, appSalt: result.app_salt };
753
+ } catch {
754
+ return { allowed: true };
755
+ }
756
+ }
757
+
758
+ // src/storage/FileStorage.ts
759
+ var fs = __toESM(require("fs"));
760
+ var path = __toESM(require("path"));
761
+ var os = __toESM(require("os"));
762
+ var CAVOS_DIR = path.join(os.homedir(), ".cavos");
763
+ var SESSIONS_DIR = path.join(CAVOS_DIR, "sessions");
764
+ var CONFIG_FILE = path.join(CAVOS_DIR, "config.json");
765
+ function ensureDir(dir) {
766
+ if (!fs.existsSync(dir)) {
767
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
768
+ }
769
+ }
770
+ function saveSession(appId, session2) {
771
+ ensureDir(SESSIONS_DIR);
772
+ const stored = {
773
+ sessionPrivateKey: session2.sessionPrivateKey,
774
+ sessionPubKey: session2.sessionPubKey,
775
+ nonceParams: {
776
+ sessionPubKey: session2.nonceParams.sessionPubKey,
777
+ validAfter: session2.nonceParams.validAfter.toString(),
778
+ validUntil: session2.nonceParams.validUntil.toString(),
779
+ renewalDeadline: session2.nonceParams.renewalDeadline.toString(),
780
+ randomness: session2.nonceParams.randomness.toString()
781
+ },
782
+ nonce: session2.nonce,
783
+ jwt: session2.jwt,
784
+ jwtClaims: session2.jwtClaims,
785
+ walletAddress: session2.walletAddress,
786
+ addressSeed: session2.addressSeed,
787
+ sessionPolicy: session2.sessionPolicy ? {
788
+ spendingLimits: session2.sessionPolicy.spendingLimits.map((sl) => ({
789
+ token: sl.token,
790
+ limit: sl.limit.toString()
791
+ })),
792
+ allowedContracts: session2.sessionPolicy.allowedContracts,
793
+ maxCallsPerTx: session2.sessionPolicy.maxCallsPerTx
794
+ } : void 0,
795
+ appSalt: session2.appSalt,
796
+ walletName: session2.walletName
797
+ };
798
+ const filePath = path.join(SESSIONS_DIR, `${appId}.json`);
799
+ fs.writeFileSync(filePath, JSON.stringify(stored, null, 2), { mode: 384 });
800
+ }
801
+ function loadSession(appId) {
802
+ const filePath = path.join(SESSIONS_DIR, `${appId}.json`);
803
+ if (!fs.existsSync(filePath)) return null;
804
+ try {
805
+ const stored = JSON.parse(fs.readFileSync(filePath, "utf-8"));
806
+ return {
807
+ sessionPrivateKey: stored.sessionPrivateKey,
808
+ sessionPubKey: stored.sessionPubKey,
809
+ nonceParams: {
810
+ sessionPubKey: stored.nonceParams.sessionPubKey,
811
+ validAfter: BigInt(stored.nonceParams.validAfter),
812
+ validUntil: BigInt(stored.nonceParams.validUntil),
813
+ renewalDeadline: BigInt(stored.nonceParams.renewalDeadline),
814
+ randomness: BigInt(stored.nonceParams.randomness)
815
+ },
816
+ nonce: stored.nonce,
817
+ jwt: stored.jwt,
818
+ jwtClaims: stored.jwtClaims,
819
+ walletAddress: stored.walletAddress,
820
+ addressSeed: stored.addressSeed,
821
+ sessionPolicy: stored.sessionPolicy ? {
822
+ spendingLimits: stored.sessionPolicy.spendingLimits.map((sl) => ({
823
+ token: sl.token,
824
+ limit: BigInt(sl.limit)
825
+ })),
826
+ allowedContracts: stored.sessionPolicy.allowedContracts,
827
+ maxCallsPerTx: stored.sessionPolicy.maxCallsPerTx
828
+ } : void 0,
829
+ appSalt: stored.appSalt,
830
+ walletName: stored.walletName
831
+ };
832
+ } catch {
833
+ return null;
834
+ }
835
+ }
836
+ function deleteSession(appId) {
837
+ const filePath = path.join(SESSIONS_DIR, `${appId}.json`);
838
+ if (fs.existsSync(filePath)) {
839
+ fs.unlinkSync(filePath);
840
+ }
841
+ }
842
+ var POLICY_FILE = path.join(CAVOS_DIR, "policy.json");
843
+ function loadPolicy() {
844
+ if (!fs.existsSync(POLICY_FILE)) return void 0;
845
+ try {
846
+ const stored = JSON.parse(fs.readFileSync(POLICY_FILE, "utf-8"));
847
+ return {
848
+ spendingLimits: (stored.spendingLimits || []).map((sl) => ({
849
+ token: sl.token,
850
+ limit: BigInt(sl.limit)
851
+ })),
852
+ allowedContracts: stored.allowedContracts || [],
853
+ maxCallsPerTx: stored.maxCallsPerTx || 10
854
+ };
855
+ } catch {
856
+ return void 0;
857
+ }
858
+ }
859
+ function savePolicy(policy2) {
860
+ ensureDir(CAVOS_DIR);
861
+ const stored = {
862
+ spendingLimits: policy2.spendingLimits.map((sl) => ({
863
+ token: sl.token,
864
+ limit: sl.limit.toString()
865
+ })),
866
+ allowedContracts: policy2.allowedContracts,
867
+ maxCallsPerTx: policy2.maxCallsPerTx
868
+ };
869
+ fs.writeFileSync(POLICY_FILE, JSON.stringify(stored, null, 2), { mode: 384 });
870
+ }
871
+ function saveConfig(config) {
872
+ ensureDir(CAVOS_DIR);
873
+ const existing = loadConfig();
874
+ const merged = { ...existing, ...config };
875
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 384 });
876
+ }
877
+ function loadConfig() {
878
+ if (!fs.existsSync(CONFIG_FILE)) return {};
879
+ try {
880
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
881
+ } catch {
882
+ return {};
883
+ }
884
+ }
885
+
886
+ // src/CavosAgent.ts
887
+ var CavosAgent = class {
888
+ constructor(config) {
889
+ this.session = null;
890
+ this.appSalt = null;
891
+ const network = config.network ?? "sepolia";
892
+ const backendUrl = config.backendUrl ?? DEFAULT_BACKEND_URL;
893
+ const rpcUrl = config.starknetRpcUrl ?? DEFAULT_RPC[network];
894
+ this.config = { ...config, network, backendUrl };
895
+ this.provider = new import_starknet7.RpcProvider({ nodeUrl: rpcUrl });
896
+ const envToken = process.env.CAVOS_TOKEN || process.env.CAVOS_SESSION_TOKEN;
897
+ if (envToken) {
898
+ try {
899
+ const sessionData = JSON.parse(Buffer.from(envToken, "base64").toString());
900
+ this.session = sessionData;
901
+ this.appSalt = sessionData.appSalt ?? null;
902
+ } catch (e) {
903
+ console.warn(`[CavosAgent] Failed to parse session token: ${e}`);
904
+ }
905
+ } else {
906
+ this.restoreSession();
907
+ }
908
+ }
909
+ // ============ Auth ============
910
+ /**
911
+ * Login with Firebase email/password.
912
+ * Generates session keys, authenticates, and persists the session.
913
+ */
914
+ async login(email, password, walletName) {
915
+ const { appId, network, backendUrl } = this.config;
916
+ const { allowed, appSalt } = await validateApp(backendUrl, appId, network);
917
+ if (!allowed) {
918
+ throw new Error("App not allowed or subscription limit reached.");
919
+ }
920
+ this.appSalt = appSalt ?? "0x0";
921
+ const { privateKey, publicKey } = generateSessionKeyPair();
922
+ const now = BigInt(Math.floor(Date.now() / 1e3));
923
+ const duration = BigInt(this.config.sessionDuration ?? 86400);
924
+ const grace = BigInt(this.config.renewalGracePeriod ?? 172800);
925
+ const nonceParams = generateNonceParams(publicKey, now, duration, grace);
926
+ const nonce = computeNonce(nonceParams);
927
+ const { jwt, claims } = await firebaseLogin(backendUrl, appId, email, password, nonce);
928
+ const oauthConfig = network === "mainnet" ? DEFAULT_OAUTH_CONFIG_MAINNET : DEFAULT_OAUTH_CONFIG_SEPOLIA;
929
+ const addressSeed = computeAddressSeed(claims.sub, this.appSalt, walletName);
930
+ const walletAddress = computeContractAddress(
931
+ claims.sub,
932
+ this.appSalt,
933
+ oauthConfig.cavosAccountClassHash,
934
+ oauthConfig.jwksRegistryAddress,
935
+ walletName
936
+ );
937
+ this.session = {
938
+ jwt,
939
+ sessionPrivateKey: privateKey,
940
+ sessionPubKey: publicKey,
941
+ nonce,
942
+ nonceParams,
943
+ jwtClaims: claims,
944
+ walletAddress,
945
+ addressSeed,
946
+ appSalt: this.appSalt,
947
+ sessionPolicy: this.config.policy,
948
+ walletName
949
+ };
950
+ saveSession(appId, this.session);
951
+ saveConfig({ defaultAppId: appId, network });
952
+ }
953
+ /**
954
+ * Login using a pre-existing JWT token.
955
+ * Useful for non-interactive agents or CI/CD.
956
+ */
957
+ async loginWithJWT(jwt, walletName) {
958
+ const { appId, network, backendUrl } = this.config;
959
+ const { allowed, appSalt } = await validateApp(backendUrl, appId, network);
960
+ if (!allowed) {
961
+ throw new Error("App not allowed or subscription limit reached.");
962
+ }
963
+ this.appSalt = appSalt ?? "0x0";
964
+ const claims = parseJWT(jwt);
965
+ const { privateKey, publicKey } = generateSessionKeyPair();
966
+ const now = BigInt(Math.floor(Date.now() / 1e3));
967
+ const duration = BigInt(this.config.sessionDuration ?? 86400);
968
+ const grace = BigInt(this.config.renewalGracePeriod ?? 172800);
969
+ const nonceParams = generateNonceParams(publicKey, now, duration, grace);
970
+ const nonce = computeNonce(nonceParams);
971
+ const oauthConfig = network === "mainnet" ? DEFAULT_OAUTH_CONFIG_MAINNET : DEFAULT_OAUTH_CONFIG_SEPOLIA;
972
+ const addressSeed = computeAddressSeed(claims.sub, this.appSalt, walletName);
973
+ const walletAddress = computeContractAddress(
974
+ claims.sub,
975
+ this.appSalt,
976
+ oauthConfig.cavosAccountClassHash,
977
+ oauthConfig.jwksRegistryAddress,
978
+ walletName
979
+ );
980
+ this.session = {
981
+ jwt,
982
+ sessionPrivateKey: privateKey,
983
+ sessionPubKey: publicKey,
984
+ nonce,
985
+ nonceParams,
986
+ jwtClaims: claims,
987
+ walletAddress,
988
+ addressSeed,
989
+ appSalt: this.appSalt,
990
+ sessionPolicy: this.config.policy,
991
+ walletName
992
+ };
993
+ saveSession(appId, this.session);
994
+ saveConfig({ defaultAppId: appId, network });
995
+ }
996
+ /**
997
+ * Check if the agent has a valid session.
998
+ */
999
+ isAuthenticated() {
1000
+ return this.session !== null;
1001
+ }
1002
+ /**
1003
+ * Get the wallet address.
1004
+ */
1005
+ getAddress() {
1006
+ return this.session?.walletAddress ?? null;
1007
+ }
1008
+ /**
1009
+ * Logout — clear the persisted session.
1010
+ */
1011
+ logout() {
1012
+ deleteSession(this.config.appId);
1013
+ this.session = null;
1014
+ this.appSalt = null;
1015
+ }
1016
+ // ============ Transactions ============
1017
+ /**
1018
+ * Execute one or more calls via paymaster.
1019
+ * Handles session registration automatically on first call.
1020
+ */
1021
+ async execute(calls) {
1022
+ this.ensureSession();
1023
+ return execute(
1024
+ this.provider,
1025
+ this.session,
1026
+ calls,
1027
+ this.getSalt(),
1028
+ this.getPaymasterKey(),
1029
+ this.config.network,
1030
+ this.config.backendUrl
1031
+ );
1032
+ }
1033
+ /**
1034
+ * Transfer ERC-20 tokens.
1035
+ */
1036
+ async transfer(tokenAddress, to, amount) {
1037
+ const low = import_starknet7.num.toHex(amount & (1n << 128n) - 1n);
1038
+ const high = import_starknet7.num.toHex(amount >> 128n);
1039
+ return this.execute({
1040
+ contractAddress: tokenAddress,
1041
+ entrypoint: "transfer",
1042
+ calldata: [to, low, high]
1043
+ });
1044
+ }
1045
+ /**
1046
+ * Approve ERC-20 spending.
1047
+ */
1048
+ async approve(tokenAddress, spender, amount) {
1049
+ const low = import_starknet7.num.toHex(amount & (1n << 128n) - 1n);
1050
+ const high = import_starknet7.num.toHex(amount >> 128n);
1051
+ return this.execute({
1052
+ contractAddress: tokenAddress,
1053
+ entrypoint: "approve",
1054
+ calldata: [spender, low, high]
1055
+ });
1056
+ }
1057
+ /**
1058
+ * Deploy the account contract.
1059
+ */
1060
+ async deploy() {
1061
+ this.ensureSession();
1062
+ const oauthConfig = this.config.network === "mainnet" ? DEFAULT_OAUTH_CONFIG_MAINNET : DEFAULT_OAUTH_CONFIG_SEPOLIA;
1063
+ return deployAccount(
1064
+ this.provider,
1065
+ this.session,
1066
+ oauthConfig.cavosAccountClassHash,
1067
+ oauthConfig.jwksRegistryAddress,
1068
+ this.getSalt(),
1069
+ this.getPaymasterKey(),
1070
+ this.config.network,
1071
+ this.config.backendUrl
1072
+ );
1073
+ }
1074
+ // ============ Queries ============
1075
+ /**
1076
+ * Get ERC-20 balance.
1077
+ */
1078
+ async getBalance(tokenAddress) {
1079
+ this.ensureSession();
1080
+ const tokens = this.config.network === "mainnet" ? TOKENS_MAINNET : TOKENS_SEPOLIA;
1081
+ const token = tokenAddress ?? tokens.STRK;
1082
+ return getBalance(this.provider, token, this.session.walletAddress);
1083
+ }
1084
+ /**
1085
+ * Check if the account is deployed.
1086
+ */
1087
+ async isDeployed() {
1088
+ this.ensureSession();
1089
+ return isDeployed(this.provider, this.session.walletAddress);
1090
+ }
1091
+ /**
1092
+ * Get on-chain session status.
1093
+ */
1094
+ async getSessionStatus() {
1095
+ this.ensureSession();
1096
+ return getSessionStatus(this.provider, this.session.walletAddress, this.session.sessionPubKey);
1097
+ }
1098
+ // ============ Session Management ============
1099
+ /**
1100
+ * Renew the current session (if in grace period).
1101
+ */
1102
+ async renewSession() {
1103
+ this.ensureSession();
1104
+ const oldSession = this.session;
1105
+ const { privateKey, publicKey } = generateSessionKeyPair();
1106
+ const now = BigInt(Math.floor(Date.now() / 1e3));
1107
+ const duration = BigInt(this.config.sessionDuration ?? 86400);
1108
+ const grace = BigInt(this.config.renewalGracePeriod ?? 172800);
1109
+ const nonceParams = generateNonceParams(publicKey, now, duration, grace);
1110
+ const nonce = computeNonce(nonceParams);
1111
+ const txHash = await renewSession(
1112
+ this.provider,
1113
+ oldSession,
1114
+ { sessionPubKey: publicKey, nonce, nonceParams, sessionPolicy: this.config.policy },
1115
+ this.getPaymasterKey(),
1116
+ this.config.network
1117
+ );
1118
+ this.session = {
1119
+ ...oldSession,
1120
+ sessionPrivateKey: privateKey,
1121
+ sessionPubKey: publicKey,
1122
+ nonce,
1123
+ nonceParams,
1124
+ sessionPolicy: this.config.policy
1125
+ };
1126
+ saveSession(this.config.appId, this.session);
1127
+ return txHash;
1128
+ }
1129
+ /**
1130
+ * Revoke a specific session key (defaults to current).
1131
+ */
1132
+ async revokeSession(sessionKey) {
1133
+ this.ensureSession();
1134
+ const keyToRevoke = sessionKey ?? this.session.sessionPubKey;
1135
+ return revokeSession(
1136
+ this.provider,
1137
+ this.session,
1138
+ keyToRevoke,
1139
+ this.getSalt(),
1140
+ this.getPaymasterKey(),
1141
+ this.config.network,
1142
+ this.config.backendUrl
1143
+ );
1144
+ }
1145
+ /**
1146
+ * Emergency revoke all sessions.
1147
+ */
1148
+ async emergencyRevokeAll() {
1149
+ this.ensureSession();
1150
+ return emergencyRevokeAllSessions(
1151
+ this.provider,
1152
+ this.session,
1153
+ this.getSalt(),
1154
+ this.getPaymasterKey(),
1155
+ this.config.network,
1156
+ this.config.backendUrl
1157
+ );
1158
+ }
1159
+ // ============ Internals ============
1160
+ ensureSession() {
1161
+ if (!this.session) {
1162
+ throw new Error("Not authenticated. Call login() first.");
1163
+ }
1164
+ }
1165
+ getSalt() {
1166
+ return this.appSalt ?? "0x0";
1167
+ }
1168
+ getPaymasterKey() {
1169
+ return this.config.paymasterApiKey ?? DEFAULT_PAYMASTER_KEY;
1170
+ }
1171
+ restoreSession() {
1172
+ const stored = loadSession(this.config.appId);
1173
+ if (!stored) return;
1174
+ this.session = {
1175
+ jwt: stored.jwt ?? "",
1176
+ sessionPrivateKey: stored.sessionPrivateKey,
1177
+ sessionPubKey: stored.sessionPubKey,
1178
+ nonce: stored.nonce,
1179
+ nonceParams: stored.nonceParams,
1180
+ jwtClaims: stored.jwtClaims ?? { sub: "", nonce: "", exp: 0, iss: "", aud: "" },
1181
+ walletAddress: stored.walletAddress ?? "",
1182
+ addressSeed: stored.addressSeed ?? "",
1183
+ sessionPolicy: stored.sessionPolicy,
1184
+ appSalt: stored.appSalt,
1185
+ walletName: stored.walletName
1186
+ };
1187
+ this.appSalt = stored.appSalt ?? null;
1188
+ }
1189
+ };
1190
+
1191
+ // src/utils/tools.json
1192
+ var tools_default = [
1193
+ {
1194
+ type: "function",
1195
+ function: {
1196
+ name: "cavos_whoami",
1197
+ description: "Show current session info: wallet address, deployment status, and on-chain session validity. Call this first to confirm the agent is authenticated and ready.",
1198
+ parameters: {
1199
+ type: "object",
1200
+ properties: {}
1201
+ }
1202
+ }
1203
+ },
1204
+ {
1205
+ type: "function",
1206
+ function: {
1207
+ name: "cavos_balance",
1208
+ description: "Show token balance for the current wallet. Returns ETH and STRK balances by default, or a specific token if provided. Always check balance before attempting a transfer.",
1209
+ parameters: {
1210
+ type: "object",
1211
+ properties: {
1212
+ token: {
1213
+ type: "string",
1214
+ description: "Token symbol (STRK, ETH) or full contract address. Omit to show both ETH and STRK."
1215
+ }
1216
+ }
1217
+ }
1218
+ }
1219
+ },
1220
+ {
1221
+ type: "function",
1222
+ function: {
1223
+ name: "cavos_transfer",
1224
+ description: "Transfer ERC-20 tokens to another Starknet address. Amounts are in human-readable units (e.g. '1.5' for 1.5 STRK). The session must be active on-chain. Verify balance first.",
1225
+ parameters: {
1226
+ type: "object",
1227
+ properties: {
1228
+ to: {
1229
+ type: "string",
1230
+ description: "Recipient Starknet address (hex, e.g. 0x...)"
1231
+ },
1232
+ amount: {
1233
+ type: "string",
1234
+ description: "Amount in human-readable units (e.g. '1.5', '0.01')"
1235
+ },
1236
+ token: {
1237
+ type: "string",
1238
+ description: "Token symbol (STRK, ETH) or contract address. Defaults to STRK.",
1239
+ default: "STRK"
1240
+ }
1241
+ },
1242
+ required: [
1243
+ "to",
1244
+ "amount"
1245
+ ]
1246
+ }
1247
+ }
1248
+ },
1249
+ {
1250
+ type: "function",
1251
+ function: {
1252
+ name: "cavos_approve",
1253
+ description: "Approve a spender contract to spend up to a specified amount of tokens on behalf of the wallet. Required before interacting with DeFi protocols.",
1254
+ parameters: {
1255
+ type: "object",
1256
+ properties: {
1257
+ spender: {
1258
+ type: "string",
1259
+ description: "Starknet address of the contract being approved to spend tokens"
1260
+ },
1261
+ amount: {
1262
+ type: "string",
1263
+ description: "Maximum amount to approve in human-readable units (e.g. '100')"
1264
+ },
1265
+ token: {
1266
+ type: "string",
1267
+ description: "Token symbol (STRK, ETH) or contract address. Defaults to STRK.",
1268
+ default: "STRK"
1269
+ }
1270
+ },
1271
+ required: [
1272
+ "spender",
1273
+ "amount"
1274
+ ]
1275
+ }
1276
+ }
1277
+ },
1278
+ {
1279
+ type: "function",
1280
+ function: {
1281
+ name: "cavos_execute",
1282
+ description: "Execute an arbitrary contract call on Starknet. Use this for any contract interaction not covered by transfer or approve. Calldata values are comma-separated hex or decimal strings.",
1283
+ parameters: {
1284
+ type: "object",
1285
+ properties: {
1286
+ contract: {
1287
+ type: "string",
1288
+ description: "Target contract address (hex)"
1289
+ },
1290
+ entrypoint: {
1291
+ type: "string",
1292
+ description: "Name of the function/entrypoint to call (e.g. 'swap', 'deposit')"
1293
+ },
1294
+ calldata: {
1295
+ type: "string",
1296
+ description: "Comma-separated calldata values (hex or decimal). Omit if the function takes no arguments."
1297
+ }
1298
+ },
1299
+ required: [
1300
+ "contract",
1301
+ "entrypoint"
1302
+ ]
1303
+ }
1304
+ }
1305
+ },
1306
+ {
1307
+ type: "function",
1308
+ function: {
1309
+ name: "cavos_session_status",
1310
+ description: "Show on-chain session status for the current session key: whether it is registered, active, expired, or renewable. Use this to diagnose transaction failures.",
1311
+ parameters: {
1312
+ type: "object",
1313
+ properties: {}
1314
+ }
1315
+ }
1316
+ },
1317
+ {
1318
+ type: "function",
1319
+ function: {
1320
+ name: "cavos_multicall",
1321
+ description: "Execute multiple contract calls atomically in a single transaction. All calls succeed or all fail together. Use this instead of calling cavos_execute multiple times when you need to batch operations (e.g., approve + swap in one tx). More efficient and safer than sequential calls.",
1322
+ parameters: {
1323
+ type: "object",
1324
+ properties: {
1325
+ calls: {
1326
+ type: "array",
1327
+ description: "Array of contract calls to execute atomically",
1328
+ items: {
1329
+ type: "object",
1330
+ properties: {
1331
+ contract: {
1332
+ type: "string",
1333
+ description: "Target contract address (hex)"
1334
+ },
1335
+ entrypoint: {
1336
+ type: "string",
1337
+ description: "Name of the function/entrypoint to call"
1338
+ },
1339
+ calldata: {
1340
+ type: "string",
1341
+ description: "Comma-separated calldata values (hex or decimal). Omit if the function takes no arguments."
1342
+ }
1343
+ },
1344
+ required: [
1345
+ "contract",
1346
+ "entrypoint"
1347
+ ]
1348
+ },
1349
+ minItems: 2
1350
+ }
1351
+ },
1352
+ required: [
1353
+ "calls"
1354
+ ]
1355
+ }
1356
+ }
1357
+ },
1358
+ {
1359
+ type: "function",
1360
+ function: {
1361
+ name: "cavos_policy_show",
1362
+ description: "Show the local spending policy: allowed contracts and per-token spending limits. The agent will refuse transactions that violate this policy.",
1363
+ parameters: {
1364
+ type: "object",
1365
+ properties: {}
1366
+ }
1367
+ }
1368
+ },
1369
+ {
1370
+ type: "function",
1371
+ function: {
1372
+ name: "cavos_call",
1373
+ description: "Execute a read-only contract call on Starknet. Use this to query state (e.g. balance, allowance, owner) without spending gas.",
1374
+ parameters: {
1375
+ type: "object",
1376
+ properties: {
1377
+ contract: {
1378
+ type: "string",
1379
+ description: "Target contract address (hex)"
1380
+ },
1381
+ entrypoint: {
1382
+ type: "string",
1383
+ description: "Name of the view function to call"
1384
+ },
1385
+ calldata: {
1386
+ type: "string",
1387
+ description: "Comma-separated arguments or JSON array string"
1388
+ }
1389
+ },
1390
+ required: [
1391
+ "contract",
1392
+ "entrypoint"
1393
+ ]
1394
+ }
1395
+ }
1396
+ },
1397
+ {
1398
+ type: "function",
1399
+ function: {
1400
+ name: "cavos_simulate",
1401
+ description: "Simulate a transaction execution to check for errors and gas usage before sending it. Highly recommended for complex interactions.",
1402
+ parameters: {
1403
+ type: "object",
1404
+ properties: {
1405
+ contract: {
1406
+ type: "string",
1407
+ description: "Target contract address (hex)"
1408
+ },
1409
+ entrypoint: {
1410
+ type: "string",
1411
+ description: "Name of the function to simulate"
1412
+ },
1413
+ calldata: {
1414
+ type: "string",
1415
+ description: "Comma-separated arguments or JSON array string"
1416
+ }
1417
+ },
1418
+ required: [
1419
+ "contract",
1420
+ "entrypoint"
1421
+ ]
1422
+ }
1423
+ }
1424
+ },
1425
+ {
1426
+ type: "function",
1427
+ function: {
1428
+ name: "cavos_estimate",
1429
+ description: "Estimate grid fees for a transaction. Useful for checking if the wallet has enough funds to cover gas.",
1430
+ parameters: {
1431
+ type: "object",
1432
+ properties: {
1433
+ contract: {
1434
+ type: "string",
1435
+ description: "Target contract address (hex)"
1436
+ },
1437
+ entrypoint: {
1438
+ type: "string",
1439
+ description: "Name of the function to estimate"
1440
+ },
1441
+ calldata: {
1442
+ type: "string",
1443
+ description: "Comma-separated arguments or JSON array string"
1444
+ }
1445
+ },
1446
+ required: [
1447
+ "contract",
1448
+ "entrypoint"
1449
+ ]
1450
+ }
1451
+ }
1452
+ }
1453
+ ];
1454
+
1455
+ // src/cli.ts
1456
+ var program = new import_commander.Command();
1457
+ program.name("cavos").description("Cavos CLI \u2014 AI agent wallet toolkit for Starknet").version("0.1.0");
1458
+ var DEFAULT_APP_ID = "0c3ff58a-1968-41c2-b0e0-a5c47309e77d";
1459
+ var DEFAULT_NETWORK = "mainnet";
1460
+ function getAgent(opts) {
1461
+ const config = loadConfig();
1462
+ const appId = opts.appId || config.defaultAppId || process.env.CAVOS_APP_ID || DEFAULT_APP_ID;
1463
+ const network = opts.network || config.network || process.env.CAVOS_NETWORK || DEFAULT_NETWORK;
1464
+ const policy2 = loadPolicy();
1465
+ return new CavosAgent({ appId, network, policy: policy2 });
1466
+ }
1467
+ function resolveToken(symbol, network) {
1468
+ if (!symbol) return network === "mainnet" ? TOKENS_MAINNET.STRK : TOKENS_SEPOLIA.STRK;
1469
+ const upper = symbol.toUpperCase();
1470
+ const tokens = network === "mainnet" ? TOKENS_MAINNET : TOKENS_SEPOLIA;
1471
+ if (upper === "STRK") return tokens.STRK;
1472
+ if (upper === "ETH") return tokens.ETH;
1473
+ return symbol;
1474
+ }
1475
+ function formatBalance(raw, decimals = 18) {
1476
+ const whole = raw / BigInt(10 ** decimals);
1477
+ const remainder = raw % BigInt(10 ** decimals);
1478
+ const fractional = remainder.toString().padStart(decimals, "0").slice(0, 6);
1479
+ return `${whole}.${fractional}`;
1480
+ }
1481
+ function out(json, data, humanFn) {
1482
+ if (json) {
1483
+ console.log(JSON.stringify(data, null, 2));
1484
+ } else {
1485
+ humanFn();
1486
+ }
1487
+ }
1488
+ function outError(json, message, code = "Error") {
1489
+ if (json) {
1490
+ console.error(JSON.stringify({ status: "error", error_code: code, message }));
1491
+ } else {
1492
+ console.error(`${code}: ${message}`);
1493
+ }
1494
+ process.exit(1);
1495
+ }
1496
+ async function waitForTx(agent, txHash, timeoutMs = 12e4) {
1497
+ const start = Date.now();
1498
+ while (Date.now() - start < timeoutMs) {
1499
+ try {
1500
+ const receipt = await agent.provider.getTransactionReceipt(txHash);
1501
+ if (receipt) {
1502
+ const status = receipt.execution_status || receipt.finality_status || receipt.status;
1503
+ if (status === "SUCCEEDED" || status === "ACCEPTED_ON_L2" || status === "ACCEPTED_ON_L1") return;
1504
+ if (status === "REVERTED") throw new Error(`Transaction ${txHash} was reverted`);
1505
+ }
1506
+ } catch (e) {
1507
+ if (e.message?.includes("reverted")) throw e;
1508
+ }
1509
+ await new Promise((r) => setTimeout(r, 3e3));
1510
+ }
1511
+ throw new Error(`Transaction confirmation timeout after ${timeoutMs / 1e3}s`);
1512
+ }
1513
+ program.command("whoami").description("Show current session info").option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--json", "Output as JSON").action(async (opts) => {
1514
+ try {
1515
+ const agent = getAgent(opts);
1516
+ const mode = process.env.CAVOS_TOKEN || process.env.CAVOS_SESSION_TOKEN ? "env" : "disk";
1517
+ if (!agent.isAuthenticated()) {
1518
+ out(opts.json, { status: "unauthenticated", mode }, () => {
1519
+ console.log("Status: Not authenticated.");
1520
+ });
1521
+ return;
1522
+ }
1523
+ const address = agent.getAddress();
1524
+ const deployed = await agent.isDeployed();
1525
+ let sessionInfo = { registered: false };
1526
+ if (deployed) {
1527
+ const s = await agent.getSessionStatus();
1528
+ sessionInfo = {
1529
+ registered: s.registered,
1530
+ expired: s.expired,
1531
+ can_renew: s.canRenew,
1532
+ valid_until: s.validUntil ? new Date(Number(s.validUntil) * 1e3).toISOString() : null,
1533
+ renewal_deadline: s.renewalDeadline ? new Date(Number(s.renewalDeadline) * 1e3).toISOString() : null
1534
+ };
1535
+ }
1536
+ out(opts.json, { status: "ok", mode, address, deployed, session: sessionInfo }, () => {
1537
+ console.log(`Mode: ${mode}`);
1538
+ console.log(`Address: ${address}`);
1539
+ console.log(`Deployed: ${deployed ? "Yes" : "No"}`);
1540
+ if (deployed) {
1541
+ const s = sessionInfo;
1542
+ console.log(`Session: ${s.registered ? s.expired ? s.can_renew ? "Renewable" : "Expired" : "Active" : "Not registered"}`);
1543
+ if (s.valid_until) console.log(`Valid until: ${s.valid_until}`);
1544
+ }
1545
+ });
1546
+ } catch (err) {
1547
+ outError(opts.json, err.message);
1548
+ }
1549
+ });
1550
+ program.command("transfer").description("Transfer ERC-20 tokens").requiredOption("--to <address>", "Recipient address").requiredOption("--amount <amount>", "Amount in human-readable units (e.g. 1.5)").option("--token <token>", "Token symbol (STRK, ETH) or address", "STRK").option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--wait", "Wait for on-chain confirmation").option("--json", "Output as JSON").action(async (opts) => {
1551
+ try {
1552
+ const agent = getAgent(opts);
1553
+ if (!agent.isAuthenticated()) outError(opts.json, "No session. Import one: cavos session import <token>", "NoSession");
1554
+ const config = loadConfig();
1555
+ const network = opts.network || config.network || DEFAULT_NETWORK;
1556
+ const tokenAddress = resolveToken(opts.token, network);
1557
+ const amount = BigInt(Math.floor(parseFloat(opts.amount) * 1e18));
1558
+ if (!opts.json) console.log(`Transferring ${opts.amount} ${opts.token} to ${opts.to}...`);
1559
+ const txHash = await agent.transfer(tokenAddress, opts.to, amount);
1560
+ if (opts.wait) {
1561
+ if (!opts.json) console.log("Waiting for confirmation...");
1562
+ await waitForTx(agent, txHash);
1563
+ }
1564
+ const explorerBase = network === "mainnet" ? "https://voyager.online/tx" : "https://sepolia.voyager.online/tx";
1565
+ out(opts.json, { status: "ok", transaction_hash: txHash, explorer: `${explorerBase}/${txHash}` }, () => {
1566
+ console.log(`Transaction: ${txHash}`);
1567
+ console.log(`Explorer: ${explorerBase}/${txHash}`);
1568
+ });
1569
+ } catch (err) {
1570
+ outError(opts.json, err.message, "TransferFailed");
1571
+ }
1572
+ });
1573
+ program.command("approve").description("Approve ERC-20 spending").requiredOption("--spender <address>", "Spender address").requiredOption("--amount <amount>", "Amount in human-readable units").option("--token <token>", "Token symbol or address", "STRK").option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--wait", "Wait for on-chain confirmation").option("--json", "Output as JSON").action(async (opts) => {
1574
+ try {
1575
+ const agent = getAgent(opts);
1576
+ if (!agent.isAuthenticated()) outError(opts.json, "No session. Import one: cavos session import <token>", "NoSession");
1577
+ const config = loadConfig();
1578
+ const network = opts.network || config.network || DEFAULT_NETWORK;
1579
+ const tokenAddress = resolveToken(opts.token, network);
1580
+ const amount = BigInt(Math.floor(parseFloat(opts.amount) * 1e18));
1581
+ if (!opts.json) console.log(`Approving ${opts.amount} ${opts.token} for ${opts.spender}...`);
1582
+ const txHash = await agent.approve(tokenAddress, opts.spender, amount);
1583
+ if (opts.wait) {
1584
+ if (!opts.json) console.log("Waiting for confirmation...");
1585
+ await waitForTx(agent, txHash);
1586
+ }
1587
+ const explorerBase = network === "mainnet" ? "https://voyager.online/tx" : "https://sepolia.voyager.online/tx";
1588
+ out(opts.json, { status: "ok", transaction_hash: txHash, explorer: `${explorerBase}/${txHash}` }, () => {
1589
+ console.log(`Transaction: ${txHash}`);
1590
+ console.log(`Explorer: ${explorerBase}/${txHash}`);
1591
+ });
1592
+ } catch (err) {
1593
+ outError(opts.json, err.message, "ApproveFailed");
1594
+ }
1595
+ });
1596
+ program.command("execute").description("Execute a raw contract call").requiredOption("--contract <address>", "Contract address").requiredOption("--entrypoint <name>", "Entrypoint name").option("--calldata <data>", "Comma-separated calldata values").option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--wait", "Wait for on-chain confirmation").option("--json", "Output as JSON").action(async (opts) => {
1597
+ try {
1598
+ const agent = getAgent(opts);
1599
+ if (!agent.isAuthenticated()) outError(opts.json, "No session. Import one: cavos session import <token>", "NoSession");
1600
+ const calldata = opts.calldata ? opts.calldata.split(",").map((s) => s.trim()) : [];
1601
+ const config = loadConfig();
1602
+ const network = opts.network || config.network || DEFAULT_NETWORK;
1603
+ if (!opts.json) console.log(`Executing ${opts.entrypoint} on ${opts.contract}...`);
1604
+ const txHash = await agent.execute({ contractAddress: opts.contract, entrypoint: opts.entrypoint, calldata });
1605
+ if (opts.wait) {
1606
+ if (!opts.json) console.log("Waiting for confirmation...");
1607
+ await waitForTx(agent, txHash);
1608
+ }
1609
+ const explorerBase = network === "mainnet" ? "https://voyager.online/tx" : "https://sepolia.voyager.online/tx";
1610
+ out(opts.json, { status: "ok", transaction_hash: txHash, explorer: `${explorerBase}/${txHash}` }, () => {
1611
+ console.log(`Transaction: ${txHash}`);
1612
+ console.log(`Explorer: ${explorerBase}/${txHash}`);
1613
+ });
1614
+ } catch (err) {
1615
+ outError(opts.json, err.message, "ExecuteFailed");
1616
+ }
1617
+ });
1618
+ program.command("multicall").description("Execute multiple contract calls in one transaction").requiredOption("--calls <json>", 'JSON array of calls: [{"contractAddress":"0x...","entrypoint":"fn","calldata":["0x..."]}]').option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--wait", "Wait for on-chain confirmation").option("--json", "Output as JSON").action(async (opts) => {
1619
+ try {
1620
+ const agent = getAgent(opts);
1621
+ if (!agent.isAuthenticated()) outError(opts.json, "No session. Import one: cavos session import <token>", "NoSession");
1622
+ let calls;
1623
+ try {
1624
+ calls = JSON.parse(opts.calls);
1625
+ if (!Array.isArray(calls) || calls.length === 0) throw new Error("calls must be a non-empty JSON array");
1626
+ } catch (e) {
1627
+ outError(opts.json, `Invalid --calls JSON: ${e.message}`, "InvalidInput");
1628
+ return;
1629
+ }
1630
+ const config = loadConfig();
1631
+ const network = opts.network || config.network || DEFAULT_NETWORK;
1632
+ if (!opts.json) console.log(`Executing ${calls.length} calls...`);
1633
+ const txHash = await agent.execute(calls);
1634
+ if (opts.wait) {
1635
+ if (!opts.json) console.log("Waiting for confirmation...");
1636
+ await waitForTx(agent, txHash);
1637
+ }
1638
+ const explorerBase = network === "mainnet" ? "https://voyager.online/tx" : "https://sepolia.voyager.online/tx";
1639
+ out(opts.json, { status: "ok", transaction_hash: txHash, call_count: calls.length, explorer: `${explorerBase}/${txHash}` }, () => {
1640
+ console.log(`Transaction: ${txHash}`);
1641
+ console.log(`Explorer: ${explorerBase}/${txHash}`);
1642
+ });
1643
+ } catch (err) {
1644
+ outError(opts.json, err.message, "MulticallFailed");
1645
+ }
1646
+ });
1647
+ program.command("deploy").description("Deploy the account contract").option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--json", "Output as JSON").action(async (opts) => {
1648
+ try {
1649
+ const agent = getAgent(opts);
1650
+ if (!agent.isAuthenticated()) outError(opts.json, "No session. Import one: cavos session import <token>", "NoSession");
1651
+ if (!opts.json) console.log("Deploying account...");
1652
+ const result = await agent.deploy();
1653
+ if (result === "already-deployed") {
1654
+ out(opts.json, { status: "ok", deployed: true, message: "Account already deployed" }, () => {
1655
+ console.log("Account is already deployed.");
1656
+ });
1657
+ } else {
1658
+ out(opts.json, { status: "ok", deployed: true, transaction_hash: result }, () => {
1659
+ console.log(`Deployed: ${result}`);
1660
+ });
1661
+ }
1662
+ } catch (err) {
1663
+ outError(opts.json, err.message, "DeployFailed");
1664
+ }
1665
+ });
1666
+ program.command("balance").description("Show token balance").option("--token <token>", "Token symbol or address").option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--json", "Output as JSON").action(async (opts) => {
1667
+ try {
1668
+ const agent = getAgent(opts);
1669
+ if (!agent.isAuthenticated()) outError(opts.json, "No session. Import one: cavos session import <token>", "NoSession");
1670
+ const config = loadConfig();
1671
+ const network = opts.network || config.network || DEFAULT_NETWORK;
1672
+ const tokens = network === "mainnet" ? TOKENS_MAINNET : TOKENS_SEPOLIA;
1673
+ if (opts.token) {
1674
+ const tokenAddress = resolveToken(opts.token, network);
1675
+ const balance = await agent.getBalance(tokenAddress);
1676
+ out(opts.json, { status: "ok", token: opts.token, balance: formatBalance(balance), raw: balance.toString() }, () => {
1677
+ console.log(`${opts.token}: ${formatBalance(balance)}`);
1678
+ });
1679
+ } else {
1680
+ const [ethBal, strkBal] = await Promise.all([agent.getBalance(tokens.ETH), agent.getBalance(tokens.STRK)]);
1681
+ out(opts.json, {
1682
+ status: "ok",
1683
+ balances: {
1684
+ ETH: { balance: formatBalance(ethBal), raw: ethBal.toString() },
1685
+ STRK: { balance: formatBalance(strkBal), raw: strkBal.toString() }
1686
+ }
1687
+ }, () => {
1688
+ console.log(`ETH: ${formatBalance(ethBal)}`);
1689
+ console.log(`STRK: ${formatBalance(strkBal)}`);
1690
+ });
1691
+ }
1692
+ } catch (err) {
1693
+ outError(opts.json, err.message);
1694
+ }
1695
+ });
1696
+ program.command("session-status").description("Show on-chain session status").option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--json", "Output as JSON").action(async (opts) => {
1697
+ try {
1698
+ const agent = getAgent(opts);
1699
+ if (!agent.isAuthenticated()) outError(opts.json, "No session. Import one: cavos session import <token>", "NoSession");
1700
+ const s = await agent.getSessionStatus();
1701
+ const state = !s.registered ? "not_registered" : s.expired ? s.canRenew ? "renewable" : "expired" : "active";
1702
+ out(opts.json, {
1703
+ status: "ok",
1704
+ state,
1705
+ registered: s.registered,
1706
+ expired: s.expired,
1707
+ can_renew: s.canRenew,
1708
+ valid_until: s.validUntil ? new Date(Number(s.validUntil) * 1e3).toISOString() : null,
1709
+ renewal_deadline: s.renewalDeadline ? new Date(Number(s.renewalDeadline) * 1e3).toISOString() : null
1710
+ }, () => {
1711
+ console.log(`State: ${state}`);
1712
+ if (s.validUntil) console.log(`Valid until: ${new Date(Number(s.validUntil) * 1e3).toISOString()}`);
1713
+ if (s.renewalDeadline) console.log(`Renewal deadline: ${new Date(Number(s.renewalDeadline) * 1e3).toISOString()}`);
1714
+ });
1715
+ } catch (err) {
1716
+ outError(opts.json, err.message);
1717
+ }
1718
+ });
1719
+ var session = program.command("session").description("Manage agent sessions");
1720
+ session.command("import").description("Import an agent session token provisioned from the Dashboard").argument("<token>", "Base64 encoded session token").option("--app-id <appId>", "App ID to associate with this session").option("--network <network>", "Network (mainnet | sepolia)").option("--json", "Output as JSON").action((token, opts) => {
1721
+ try {
1722
+ const decoded = Buffer.from(token, "base64").toString();
1723
+ const sessionData = JSON.parse(decoded);
1724
+ if (!sessionData.walletAddress || !sessionData.sessionPrivateKey) {
1725
+ throw new Error("Invalid session token format.");
1726
+ }
1727
+ const config = loadConfig();
1728
+ const appId = opts.appId || config.defaultAppId || DEFAULT_APP_ID;
1729
+ const network = opts.network || config.network || DEFAULT_NETWORK;
1730
+ saveSession(appId, sessionData);
1731
+ saveConfig({ defaultAppId: appId, network });
1732
+ out(opts.json, { status: "ok", address: sessionData.walletAddress, app_id: appId, network }, () => {
1733
+ console.log(`Session imported: ${sessionData.walletAddress}`);
1734
+ console.log(`App ID: ${appId} | Network: ${network}`);
1735
+ });
1736
+ } catch (err) {
1737
+ if (opts.json) {
1738
+ console.error(JSON.stringify({ status: "error", error_code: "ImportFailed", message: err.message }));
1739
+ } else {
1740
+ console.error(`Import failed: ${err.message}`);
1741
+ }
1742
+ }
1743
+ });
1744
+ program.command("revoke-session").description("Revoke a session key (requires active session)").option("--key <sessionKey>", "Specific session key to revoke (defaults to current)").option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--json", "Output as JSON").action(async (opts) => {
1745
+ try {
1746
+ const agent = getAgent(opts);
1747
+ if (!agent.isAuthenticated()) outError(opts.json, "No session. Import one: cavos session import <token>", "NoSession");
1748
+ if (!opts.json) console.log("Revoking session...");
1749
+ const txHash = await agent.revokeSession(opts.key);
1750
+ out(opts.json, { status: "ok", transaction_hash: txHash }, () => {
1751
+ console.log(`Session revoked: ${txHash}`);
1752
+ });
1753
+ } catch (err) {
1754
+ const code = err.message?.includes("Session not registered") ? "SessionNotRegistered" : "RevokeFailed";
1755
+ outError(opts.json, err.message, code);
1756
+ }
1757
+ });
1758
+ program.command("emergency-revoke").description("Emergency revoke all sessions").option("--app-id <appId>", "App ID").option("--network <network>", "Network").option("--json", "Output as JSON").action(async (opts) => {
1759
+ try {
1760
+ const agent = getAgent(opts);
1761
+ if (!agent.isAuthenticated()) outError(opts.json, "No session. Import one: cavos session import <token>", "NoSession");
1762
+ if (!opts.json) console.log("Emergency revoking all sessions...");
1763
+ const txHash = await agent.emergencyRevokeAll();
1764
+ out(opts.json, { status: "ok", transaction_hash: txHash }, () => {
1765
+ console.log(`All sessions revoked: ${txHash}`);
1766
+ });
1767
+ } catch (err) {
1768
+ outError(opts.json, err.message, "EmergencyRevokeFailed");
1769
+ }
1770
+ });
1771
+ var policy = program.command("policy").description("Manage local spending policies");
1772
+ policy.command("import").description("Import a policy JSON exported from the Dashboard").argument("<json>", "Policy JSON string").action((jsonStr) => {
1773
+ try {
1774
+ const imported = JSON.parse(jsonStr);
1775
+ if (!imported.spendingLimits || !imported.allowedContracts) {
1776
+ throw new Error("Invalid policy format. Missing spendingLimits or allowedContracts.");
1777
+ }
1778
+ const policyParams = {
1779
+ ...imported,
1780
+ spendingLimits: imported.spendingLimits.map((sl) => ({ ...sl, limit: BigInt(sl.limit) }))
1781
+ };
1782
+ savePolicy(policyParams);
1783
+ console.log("Policy imported successfully.");
1784
+ } catch (err) {
1785
+ console.error(`Import failed: ${err.message}`);
1786
+ }
1787
+ });
1788
+ policy.command("clear").description("Clear the local policy").action(() => {
1789
+ savePolicy({ spendingLimits: [], allowedContracts: [], maxCallsPerTx: 10 });
1790
+ console.log("Policy cleared.");
1791
+ });
1792
+ policy.command("show").description("Show the current local policy").option("--json", "Output as JSON").action((opts) => {
1793
+ const p = loadPolicy();
1794
+ if (!p || p.spendingLimits.length === 0 && p.allowedContracts.length === 0) {
1795
+ out(opts.json, { status: "ok", policy: null, message: "No local policy defined." }, () => {
1796
+ console.log("No local policy defined. The agent will follow default app constraints.");
1797
+ });
1798
+ return;
1799
+ }
1800
+ out(opts.json, {
1801
+ status: "ok",
1802
+ policy: {
1803
+ spending_limits: p.spendingLimits.map((sl) => ({ token: sl.token, limit: formatBalance(sl.limit) })),
1804
+ allowed_contracts: p.allowedContracts,
1805
+ max_calls_per_tx: p.maxCallsPerTx
1806
+ }
1807
+ }, () => {
1808
+ console.log("--- Current Local Policy ---");
1809
+ console.log("Spending Limits:");
1810
+ if (p.spendingLimits.length === 0) console.log(" (None)");
1811
+ p.spendingLimits.forEach((sl) => console.log(` ${sl.token}: ${formatBalance(sl.limit)}`));
1812
+ console.log("\nAllowed Contracts:");
1813
+ if (p.allowedContracts.length === 0) console.log(" (None \u2014 all allowed)");
1814
+ p.allowedContracts.forEach((addr) => console.log(` ${addr}`));
1815
+ });
1816
+ });
1817
+ var tools = program.command("tools").description("AI Agent tool definitions");
1818
+ tools.command("list").description("List tools in OpenAI-compatible JSON format").action(() => {
1819
+ console.log(JSON.stringify(tools_default, null, 2));
1820
+ });
1821
+ program.command("call").description("Call a contract entrypoint (read-only)").requiredOption("--contract <address>", "Contract address").requiredOption("--entrypoint <name>", "Entrypoint name").option("--calldata <data>", "Comma-separated calldata or JSON array", "[]").option("--block <blockId>", "Block identifier (default: latest)", "latest").option("--json", "Output as JSON").action(async (opts) => {
1822
+ try {
1823
+ const agent = getAgent(opts);
1824
+ let calldata = [];
1825
+ if (opts.calldata.startsWith("[")) {
1826
+ calldata = JSON.parse(opts.calldata);
1827
+ } else {
1828
+ calldata = opts.calldata.split(",").filter((x) => x);
1829
+ }
1830
+ const result = await agent.provider.callContract({
1831
+ contractAddress: opts.contract,
1832
+ entrypoint: opts.entrypoint,
1833
+ calldata
1834
+ }, opts.block);
1835
+ out(opts.json, { success: true, result }, () => {
1836
+ console.log("Result:", result);
1837
+ });
1838
+ } catch (e) {
1839
+ outError(opts.json, e.message);
1840
+ }
1841
+ });
1842
+ program.command("simulate").description("Simulate a transaction execution").requiredOption("--contract <address>", "Contract address").requiredOption("--entrypoint <name>", "Entrypoint name").option("--calldata <data>", "Comma-separated calldata or JSON array", "[]").option("--json", "Output as JSON").action(async (opts) => {
1843
+ try {
1844
+ const agent = getAgent(opts);
1845
+ let calldata = [];
1846
+ if (opts.calldata.startsWith("[")) {
1847
+ calldata = JSON.parse(opts.calldata);
1848
+ } else {
1849
+ calldata = opts.calldata.split(",").filter((x) => x);
1850
+ }
1851
+ if (!agent.isAuthenticated()) {
1852
+ throw new Error("Authentication required for simulation");
1853
+ }
1854
+ const account = agent.sessionAccount || agent.account;
1855
+ const invocation = {
1856
+ contractAddress: opts.contract,
1857
+ entrypoint: opts.entrypoint,
1858
+ calldata
1859
+ };
1860
+ const simulation = await account.simulateTransaction([invocation]);
1861
+ out(opts.json, { success: true, simulation }, () => {
1862
+ console.log("Simulation Check:", simulation);
1863
+ console.log("Logs:", simulation[0].transaction_trace.execution_resources);
1864
+ });
1865
+ } catch (e) {
1866
+ outError(opts.json, e.message);
1867
+ }
1868
+ });
1869
+ program.command("estimate").description("Estimate fee for a transaction").requiredOption("--contract <address>", "Contract address").requiredOption("--entrypoint <name>", "Entrypoint name").option("--calldata <data>", "Comma-separated calldata or JSON array", "[]").option("--json", "Output as JSON").action(async (opts) => {
1870
+ try {
1871
+ const agent = getAgent(opts);
1872
+ let calldata = [];
1873
+ if (opts.calldata.startsWith("[")) {
1874
+ calldata = JSON.parse(opts.calldata);
1875
+ } else {
1876
+ calldata = opts.calldata.split(",").filter((x) => x);
1877
+ }
1878
+ if (!agent.isAuthenticated()) {
1879
+ throw new Error("Authentication required for estimation");
1880
+ }
1881
+ const account = agent.sessionAccount || agent.account;
1882
+ const invocation = {
1883
+ contractAddress: opts.contract,
1884
+ entrypoint: opts.entrypoint,
1885
+ calldata
1886
+ };
1887
+ const estimate = await account.estimateFee([invocation]);
1888
+ out(opts.json, { success: true, estimate }, () => {
1889
+ console.log(`Estimated Fee: ${formatBalance(estimate.amount)} ETH`);
1890
+ console.log(`Gas Usage: ${estimate.overall_fee}`);
1891
+ });
1892
+ } catch (e) {
1893
+ outError(opts.json, e.message);
1894
+ }
1895
+ });
1896
+ program.command("policy").description("Manage agent spending policy").argument("[action]", "Action: show, set-limit, reset", "show").option("--json", "Output as JSON").action((action, opts) => {
1897
+ const agent = getAgent(opts);
1898
+ const policy2 = agent.policy || loadPolicy() || {};
1899
+ if (action === "show") {
1900
+ out(opts.json, {
1901
+ policy: policy2,
1902
+ status: {
1903
+ is_loaded: !!agent.policy,
1904
+ source: process.env.CAVOS_POLICY ? "env" : "disk"
1905
+ }
1906
+ }, () => {
1907
+ console.log("--- Agent Policy ---");
1908
+ console.log(`Allowed Contracts: ${policy2.allowedContracts?.length || 0}`);
1909
+ console.log(`Spending Limits:`);
1910
+ policy2.spendingLimits?.forEach((sl) => {
1911
+ console.log(` - ${sl.token}: ${sl.limit}`);
1912
+ });
1913
+ console.log(`Max Calls/Tx: ${policy2.maxCallsPerTx || "Unlimited"}`);
1914
+ });
1915
+ }
1916
+ });
1917
+ program.parse();
1918
+ //# sourceMappingURL=cli.js.map