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
|
|
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
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
this.
|
|
130
|
-
this.
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 [
|
|
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");
|
|
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.
|
|
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
|
-
//
|
|
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();
|