cronos-agent-wallet 1.2.1 → 1.2.2

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.
@@ -7,7 +7,7 @@ import { PaymentExecutor } from "./executors";
7
7
  * - Decides whether to pay
8
8
  * - Delegates execution to PaymentExecutor
9
9
  * - Enforces security + policy
10
- * - Persists state (Isomorphic: FS or LocalStorage)
10
+ * - Persists state to MongoDB
11
11
  */
12
12
  export declare class AgentWallet {
13
13
  private readonly address;
@@ -19,7 +19,7 @@ export declare class AgentWallet {
19
19
  private allowedMerchants;
20
20
  private trustedFacilitatorOrigins;
21
21
  private stopped;
22
- private persistence;
22
+ private isInitialized;
23
23
  /**
24
24
  * Replay protection
25
25
  * key -> timestamp
@@ -27,6 +27,7 @@ export declare class AgentWallet {
27
27
  private paidRequests;
28
28
  constructor(address: string, executor: PaymentExecutor, config?: AgentWalletConfig);
29
29
  private getTodayDate;
30
+ init(): Promise<void>;
30
31
  private loadState;
31
32
  private saveState;
32
33
  getAddress(): string;
@@ -42,9 +43,9 @@ export declare class AgentWallet {
42
43
  emergencyStop(): void;
43
44
  shouldPay(request: PaymentRequest, context: WalletContext, originalChallenge?: {
44
45
  route?: string;
45
- }): {
46
+ }): Promise<{
46
47
  allow: boolean;
47
48
  reason?: string;
48
- };
49
+ }>;
49
50
  executePayment(request: PaymentRequest): Promise<string>;
50
51
  }
@@ -1,57 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.AgentWallet = void 0;
4
- class NodePersistence {
5
- fs;
6
- path;
7
- statePath = "";
8
- constructor() {
9
- // Dynamic require to avoid bundling issues in browser
10
- try {
11
- this.fs = require("fs");
12
- this.path = require("path");
13
- this.statePath = this.path.resolve(__dirname, "wallet-state.json");
14
- }
15
- catch (e) { /* Browser environment */ }
16
- }
17
- load() {
18
- if (!this.fs || !this.fs.existsSync(this.statePath))
19
- return null;
20
- try {
21
- return JSON.parse(this.fs.readFileSync(this.statePath, "utf-8"));
22
- }
23
- catch (e) {
24
- console.warn("[WALLET] Failed to load local state file", e);
25
- return null;
26
- }
27
- }
28
- save(data) {
29
- if (!this.fs)
30
- return;
31
- try {
32
- const tmpPath = this.statePath + ".tmp";
33
- this.fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
34
- this.fs.renameSync(tmpPath, this.statePath);
35
- }
36
- catch (e) {
37
- console.error("[WALLET] Failed to save local state file", e);
38
- }
39
- }
40
- }
41
- class BrowserPersistence {
42
- key = "cronos_agent_wallet_state";
43
- load() {
44
- if (typeof localStorage === "undefined")
45
- return null;
46
- const item = localStorage.getItem(this.key);
47
- return item ? JSON.parse(item) : null;
48
- }
49
- save(data) {
50
- if (typeof localStorage === "undefined")
51
- return;
52
- localStorage.setItem(this.key, JSON.stringify(data));
53
- }
54
- }
7
+ const mongoose_1 = __importDefault(require("mongoose"));
8
+ const models_1 = require("./models");
55
9
  /**
56
10
  * AgentWallet
57
11
  * ------------
@@ -59,7 +13,7 @@ class BrowserPersistence {
59
13
  * - Decides whether to pay
60
14
  * - Delegates execution to PaymentExecutor
61
15
  * - Enforces security + policy
62
- * - Persists state (Isomorphic: FS or LocalStorage)
16
+ * - Persists state to MongoDB
63
17
  */
64
18
  class AgentWallet {
65
19
  address;
@@ -78,7 +32,7 @@ class AgentWallet {
78
32
  "https://cronos-x-402-production.up.railway.app",
79
33
  ]);
80
34
  stopped = false;
81
- persistence;
35
+ isInitialized = false;
82
36
  /**
83
37
  * Replay protection
84
38
  * key -> timestamp
@@ -97,55 +51,95 @@ class AgentWallet {
97
51
  if (config.trustedFacilitators)
98
52
  this.trustedFacilitatorOrigins = new Set(config.trustedFacilitators);
99
53
  }
100
- // Initialize Persistence based on env
101
- if (typeof window === "undefined") {
102
- this.persistence = new NodePersistence();
103
- }
104
- else {
105
- this.persistence = new BrowserPersistence();
106
- }
107
- this.loadState();
54
+ this.lastResetDate = this.getTodayDate();
108
55
  }
109
56
  // ---------------- PERSISTENCE ----------------
110
57
  getTodayDate() {
111
58
  return new Date().toISOString().split("T")[0]; // YYYY-MM-DD
112
59
  }
113
- loadState() {
114
- const state = this.persistence.load();
115
- if (state) {
116
- // 1. Check if we need to reset for a new day
117
- const today = this.getTodayDate();
118
- if (state.lastResetDate !== today) {
119
- console.log(`[WALLET] New day detected! Resetting limit. (Last: ${state.lastResetDate}, Today: ${today})`);
120
- this.spentToday = 0;
121
- this.lastResetDate = today;
122
- // Cleanup old requests (older than 24h) to prevent memory leak
123
- const MAX_AGE_MS = 24 * 60 * 60 * 1000;
124
- const now = Date.now();
125
- this.paidRequests = new Map(state.paidRequests.filter(([_, ts]) => now - ts < MAX_AGE_MS));
60
+ async init() {
61
+ if (this.isInitialized)
62
+ return;
63
+ try {
64
+ // Auto-connect if not connected
65
+ if (mongoose_1.default.connection.readyState === 0) {
66
+ if (!process.env.MONGODB_URI) {
67
+ console.warn("[WALLET] MONGODB_URI missing. Persistence disabled.");
68
+ return;
69
+ }
70
+ await mongoose_1.default.connect(process.env.MONGODB_URI);
71
+ console.log("[WALLET] Connected to MongoDB");
72
+ }
73
+ await this.loadState();
74
+ this.isInitialized = true;
75
+ }
76
+ catch (error) {
77
+ console.error("[WALLET] Initialization failed:", error);
78
+ }
79
+ }
80
+ async loadState() {
81
+ try {
82
+ const doc = await models_1.AgentWalletModel.findOne({ address: this.address }).lean();
83
+ if (doc) {
84
+ // Helper to normalize DB Map vs POJO
85
+ const rawObj = doc.paidRequests || {};
86
+ const rawMap = rawObj instanceof Map
87
+ ? rawObj
88
+ : new Map(Object.entries(rawObj));
89
+ // 1. Check if we need to reset for a new day
90
+ const today = this.getTodayDate();
91
+ if (doc.lastResetDate !== today) {
92
+ console.log(`[WALLET] New day detected! Resetting limit. (Last: ${doc.lastResetDate}, Today: ${today})`);
93
+ this.spentToday = 0;
94
+ this.lastResetDate = today;
95
+ // Cleanup old requests (older than 24h)
96
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
97
+ const now = Date.now();
98
+ this.paidRequests = new Map();
99
+ for (const [key, val] of rawMap.entries()) {
100
+ const ts = Number(val);
101
+ if (!isNaN(ts) && (now - ts < MAX_AGE_MS)) {
102
+ this.paidRequests.set(key, ts);
103
+ }
104
+ }
105
+ // Save cleanup immediately
106
+ await this.saveState();
107
+ }
108
+ else {
109
+ console.log(`[WALLET] State loaded. Spent today: ${doc.spentToday.toFixed(4)} / ${this.dailyLimit}`);
110
+ this.spentToday = doc.spentToday;
111
+ this.lastResetDate = doc.lastResetDate;
112
+ this.paidRequests = new Map();
113
+ for (const [key, val] of rawMap.entries()) {
114
+ const ts = Number(val);
115
+ if (!isNaN(ts)) {
116
+ this.paidRequests.set(key, ts);
117
+ }
118
+ }
119
+ }
126
120
  }
127
121
  else {
128
- console.log(`[WALLET] State loaded. Spent today: ${state.spentToday.toFixed(4)} / ${this.dailyLimit}`);
129
- this.spentToday = state.spentToday;
130
- this.lastResetDate = state.lastResetDate;
131
- // Also cleanup on every load to be safe
132
- const MAX_AGE_MS = 24 * 60 * 60 * 1000;
133
- const now = Date.now();
134
- this.paidRequests = new Map(state.paidRequests.filter(([_, ts]) => now - ts < MAX_AGE_MS));
122
+ // Initialize fresh
123
+ this.lastResetDate = this.getTodayDate();
124
+ await this.saveState();
135
125
  }
136
126
  }
137
- else {
138
- // Initialize fresh
139
- this.lastResetDate = this.getTodayDate();
127
+ catch (error) {
128
+ console.warn("[WALLET] Failed to load state from DB", error);
140
129
  }
141
130
  }
142
- saveState() {
143
- const state = {
144
- lastResetDate: this.lastResetDate,
145
- spentToday: this.spentToday,
146
- paidRequests: Array.from(this.paidRequests.entries())
147
- };
148
- this.persistence.save(state);
131
+ async saveState() {
132
+ try {
133
+ await models_1.AgentWalletModel.findOneAndUpdate({ address: this.address }, {
134
+ address: this.address,
135
+ lastResetDate: this.lastResetDate,
136
+ spentToday: this.spentToday,
137
+ paidRequests: this.paidRequests
138
+ }, { upsert: true, new: true });
139
+ }
140
+ catch (error) {
141
+ console.error("[WALLET] Failed to save state to DB", error);
142
+ }
149
143
  }
150
144
  // ---------------- PUBLIC ----------------
151
145
  getAddress() {
@@ -246,7 +240,10 @@ class AgentWallet {
246
240
  this.stopped = true;
247
241
  console.warn("[WALLET] Emergency stop activated!");
248
242
  }
249
- shouldPay(request, context, originalChallenge) {
243
+ async shouldPay(request, context, originalChallenge) {
244
+ // Ensure initialized
245
+ if (!this.isInitialized)
246
+ await this.init();
250
247
  // -1. Emergency Stop
251
248
  if (this.stopped) {
252
249
  return { allow: false, reason: "Emergency stop active" };
@@ -256,8 +253,8 @@ class AgentWallet {
256
253
  if (originalChallenge?.route && request.route !== originalChallenge.route) {
257
254
  return { allow: false, reason: `Route mismatch: Challenge=${request.route}, Expected=${originalChallenge.route}` };
258
255
  }
259
- // 0. Currency check [NEW]
260
- if (request.currency !== "USDC") {
256
+ // 0. Currency check [UPDATED]
257
+ if (request.currency !== "USDC" && request.currency !== "CRO") {
261
258
  return { allow: false, reason: `Unsupported currency: ${request.currency}` };
262
259
  }
263
260
  // 1. Chain verification
@@ -287,6 +284,9 @@ class AgentWallet {
287
284
  }
288
285
  // ---------------- PAYMENT EXECUTION ----------------
289
286
  async executePayment(request) {
287
+ // Ensure initialized
288
+ if (!this.isInitialized)
289
+ await this.init();
290
290
  const key = this.paymentKey(request);
291
291
  if (this.paidRequests.has(key)) {
292
292
  throw new Error("Replay detected: payment already executed");
@@ -297,7 +297,7 @@ class AgentWallet {
297
297
  this.spentToday += request.amount;
298
298
  this.paidRequests.set(key, Date.now());
299
299
  // Persist immediately
300
- this.saveState();
300
+ await this.saveState();
301
301
  return proof;
302
302
  }
303
303
  }
@@ -52,40 +52,55 @@ class CronosUsdcExecutor {
52
52
  throw lastError;
53
53
  }
54
54
  async execute(request) {
55
- const amount = ethers_1.ethers.parseUnits(request.amount.toString(), 6 // USDC decimals
56
- );
57
- console.log(`[CRONOS] Paying ${request.amount} USDC -> ${request.payTo}
58
- merchant = ${request.merchantId}
59
- route = ${request.route}
60
- nonce = ${request.nonce} `);
61
55
  // 0. Connection Check
62
56
  if (!this.chainId)
63
57
  await this.verifyChain(Number(request.chainId));
58
+ console.log(`[CRONOS] Paying ${request.amount} ${request.currency} -> ${request.payTo}
59
+ merchant = ${request.merchantId}
60
+ route = ${request.route}
61
+ nonce = ${request.nonce}`);
62
+ // --- PATH A: Native CRO Payment ---
63
+ if (request.currency === "CRO" || request.currency === "TCRO") {
64
+ const amount = ethers_1.ethers.parseEther(request.amount.toString());
65
+ // 1. Balance Check
66
+ const balance = await this.withRetry(() => this.provider.getBalance(this.wallet.address));
67
+ const MIN_GAS = ethers_1.ethers.parseEther("0.05"); // Reserve gas
68
+ if (balance < (amount + MIN_GAS)) {
69
+ throw new Error(`Insufficient CRO balance: have ${ethers_1.ethers.formatEther(balance)}, need ${request.amount} + gas`);
70
+ }
71
+ // 2. Send Tx
72
+ const txHandler = await this.withRetry(() => this.wallet.sendTransaction({
73
+ to: request.payTo,
74
+ value: amount
75
+ }));
76
+ console.log(`[CRONOS] CRO Tx sent: ${txHandler.hash}. Waiting for confirmation...`);
77
+ // 3. Wait
78
+ const receipt = await Promise.race([
79
+ this.withRetry(() => txHandler.wait(CONFIRMATIONS)),
80
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Transaction confirmation timeout")), TX_TIMEOUT_MS)),
81
+ ]);
82
+ if (!receipt || receipt.status !== 1)
83
+ throw new Error("CRO transfer failed");
84
+ console.log(`[CRONOS] Payment confirmed: ${receipt.hash}`);
85
+ return receipt.hash;
86
+ }
87
+ // --- PATH B: USDC Payment (Legacy) ---
88
+ const amount = ethers_1.ethers.parseUnits(request.amount.toString(), 6); // USDC decimals
64
89
  // 1. Balance check (Retryable)
65
- // Note: balanceOf returns bigint in v6
66
90
  const balance = await this.withRetry(() => this.usdc.balanceOf(this.wallet.address));
67
91
  if (balance < amount) {
68
- throw new Error(`Insufficient USDC balance: have ${ethers_1.ethers.formatUnits(balance, 6)}, need ${request.amount} `);
92
+ throw new Error(`Insufficient USDC balance: have ${ethers_1.ethers.formatUnits(balance, 6)}, need ${request.amount}`);
69
93
  }
70
- // ... inside class ...
71
94
  // 1.5 Gas Check (CRO Balance)
72
95
  const croBalance = await this.withRetry(() => this.provider.getBalance(this.wallet.address));
73
- const MIN_GAS = ethers_1.ethers.parseEther("0.1"); // Reserve 0.1 CRO for gas
96
+ const MIN_GAS = ethers_1.ethers.parseEther("0.1");
74
97
  if (croBalance < MIN_GAS) {
75
98
  throw new errors_1.AgentError(`Insufficient CRO for gas: have ${ethers_1.ethers.formatEther(croBalance)}, need > 0.1`, "INSUFFICIENT_FUNDS");
76
99
  }
77
- // 2. Gas Estimation (Safety)
78
- try {
79
- const gasEstimate = await this.withRetry(() => this.usdc.transfer.estimateGas(request.payTo, amount));
80
- // Optional: Cap gas limit if needed, usually estimate is fine
81
- }
82
- catch (e) {
83
- throw new Error(`Gas estimation failed(likely insufficient funds for gas): ${e.message} `);
84
- }
85
- // 3. Send tx
100
+ // 2. Send tx
86
101
  const tx = await this.withRetry(() => this.usdc.transfer(request.payTo, amount));
87
102
  console.log(`[CRONOS] Tx sent: ${tx.hash}. Waiting for confirmation...`);
88
- // 4. Wait with timeout + confirmations
103
+ // 3. Wait
89
104
  const receipt = await Promise.race([
90
105
  this.withRetry(() => tx.wait(CONFIRMATIONS)),
91
106
  new Promise((_, reject) => setTimeout(() => reject(new Error("Transaction confirmation timeout")), TX_TIMEOUT_MS)),
@@ -93,7 +108,7 @@ nonce = ${request.nonce} `);
93
108
  if (!receipt || receipt.status !== 1) {
94
109
  throw new Error("USDC transfer failed (Reverted)");
95
110
  }
96
- console.log(`[CRONOS] Payment confirmed: ${receipt.hash} `);
111
+ console.log(`[CRONOS] Payment confirmed: ${receipt.hash}`);
97
112
  return receipt.hash;
98
113
  }
99
114
  getAddress() {
@@ -0,0 +1,15 @@
1
+ import { Document } from "mongoose";
2
+ interface IAgentWalletState extends Document {
3
+ address: string;
4
+ lastResetDate: string;
5
+ spentToday: number;
6
+ paidRequests: Map<string, number>;
7
+ }
8
+ export declare const AgentWalletModel: import("mongoose").Model<IAgentWalletState, {}, {}, {}, Document<unknown, {}, IAgentWalletState, {}, import("mongoose").DefaultSchemaOptions> & IAgentWalletState & Required<{
9
+ _id: import("mongoose").Types.ObjectId;
10
+ }> & {
11
+ __v: number;
12
+ } & {
13
+ id: string;
14
+ }, any, IAgentWalletState>;
15
+ export {};
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentWalletModel = void 0;
4
+ const mongoose_1 = require("mongoose");
5
+ const AgentWalletStateSchema = new mongoose_1.Schema({
6
+ address: { type: String, required: true, unique: true },
7
+ lastResetDate: { type: String, required: true },
8
+ spentToday: { type: Number, required: true, default: 0 },
9
+ paidRequests: {
10
+ type: Map,
11
+ of: Number,
12
+ default: new Map()
13
+ }
14
+ });
15
+ exports.AgentWalletModel = (0, mongoose_1.model)("AgentWalletState", AgentWalletStateSchema);
@@ -76,7 +76,7 @@ async function x402Request(url, wallet, context, options = {}) {
76
76
  throw new Error(`Failed to parse 402 challenge from Headers OR Body. Header error: ${headerErr}. Body error: ${bodyErr}`);
77
77
  }
78
78
  }
79
- const decision = wallet.shouldPay(paymentRequest, context, {
79
+ const decision = await wallet.shouldPay(paymentRequest, context, {
80
80
  route: wallet.parse402Header(res.headers).route // Validate against what the server claimed in headers
81
81
  });
82
82
  const agentAddress = wallet.getAddress();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronos-agent-wallet",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [