cronos-agent-wallet 1.2.1 → 1.2.3
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/README.md +26 -0
- package/dist/AgentClient.js +1 -6
- package/dist/config.d.ts +14 -0
- package/dist/config.js +9 -0
- package/dist/internal/AgentWallet.d.ts +21 -6
- package/dist/internal/AgentWallet.js +164 -95
- package/dist/internal/CronosUsdcExecutor.d.ts +3 -0
- package/dist/internal/CronosUsdcExecutor.js +42 -21
- package/dist/internal/PolicyRegistryAdapter.d.ts +32 -0
- package/dist/internal/PolicyRegistryAdapter.js +44 -0
- package/dist/internal/models.d.ts +15 -0
- package/dist/internal/models.js +15 -0
- package/dist/internal/x402ToolClient.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
The official SDK for building AI agents that can pay for resources on the Cronos blockchain using the x402 protocol.
|
|
4
4
|
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
This SDK employs a **Hybrid "Exoskeleton" Architecture**:
|
|
8
|
+
- **Off-Chain**: Fast, free logic enforcement and state tracking (MongoDB).
|
|
9
|
+
- **On-Chain**: Immutable security anchors (Registry & Policy) to prevent tampering.
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
graph TD
|
|
13
|
+
subgraph Agent SDK
|
|
14
|
+
Local[Local Policy Engine]
|
|
15
|
+
Anchor[On-Chain Policy Anchor (read-only)]
|
|
16
|
+
Executor[x402 Payment Executor]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
subgraph Merchant Middleware
|
|
20
|
+
MRegistry[Merchant Registry (read-only)]
|
|
21
|
+
Verifier[x402 Verification]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Local -->|Checks Hash| Anchor
|
|
25
|
+
Local -->|Approves| Executor
|
|
26
|
+
Executor -->|Signed Claim| Verifier
|
|
27
|
+
Verifier -->|Verifies Identity| MRegistry
|
|
28
|
+
Verifier -->|Payment Tx| Cronos[Cronos EVM]
|
|
29
|
+
```
|
|
30
|
+
|
|
5
31
|
## 🛡️ Security Features (v1.1.1)
|
|
6
32
|
- **Zero Trust Payer:** Derives identity strictly from chain data.
|
|
7
33
|
- **Strong Replay Protection:** Enforces cryptographic binding of `merchantId + route + nonce`.
|
package/dist/AgentClient.js
CHANGED
|
@@ -28,12 +28,7 @@ class AgentClient {
|
|
|
28
28
|
// 1. Create executor (chain-specific)
|
|
29
29
|
const executor = new CronosUsdcExecutor_1.CronosUsdcExecutor(config.rpcUrl, config.privateKey, config.usdcAddress, config.chainId);
|
|
30
30
|
// 2. Create wallet (policy + persistence)
|
|
31
|
-
this.wallet = new AgentWallet_1.AgentWallet(executor.getAddress(), executor,
|
|
32
|
-
dailyLimit: config.dailyLimit,
|
|
33
|
-
maxPerTransaction: config.maxPerTransaction,
|
|
34
|
-
trustedFacilitators: config.trustedFacilitators,
|
|
35
|
-
allowedMerchants: config.allowedMerchants,
|
|
36
|
-
});
|
|
31
|
+
this.wallet = new AgentWallet_1.AgentWallet(executor.getAddress(), executor, executor.getProvider(), executor.getSigner(), config);
|
|
37
32
|
// 3. Context passed per request
|
|
38
33
|
this.context = {
|
|
39
34
|
chainId: config.chainId,
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export interface OnChainAnchors {
|
|
2
|
+
merchantRegistry?: string;
|
|
3
|
+
agentPolicyRegistry?: string;
|
|
4
|
+
policyVerifier?: string;
|
|
5
|
+
}
|
|
1
6
|
export interface AgentConfig {
|
|
2
7
|
privateKey: string;
|
|
3
8
|
rpcUrl: string;
|
|
@@ -8,5 +13,14 @@ export interface AgentConfig {
|
|
|
8
13
|
allowedMerchants?: string[];
|
|
9
14
|
trustedFacilitators?: string[];
|
|
10
15
|
analyticsUrl?: string;
|
|
16
|
+
anchors?: OnChainAnchors;
|
|
17
|
+
strictPolicy?: boolean;
|
|
11
18
|
merchantId?: string;
|
|
12
19
|
}
|
|
20
|
+
export declare const AGENT_CONFIG_DEFAULTS: {
|
|
21
|
+
anchors: {
|
|
22
|
+
merchantRegistry: string;
|
|
23
|
+
agentPolicyRegistry: string;
|
|
24
|
+
policyVerifier: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
package/dist/config.js
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGENT_CONFIG_DEFAULTS = void 0;
|
|
4
|
+
exports.AGENT_CONFIG_DEFAULTS = {
|
|
5
|
+
// Cronos Testnet Anchors
|
|
6
|
+
anchors: {
|
|
7
|
+
merchantRegistry: "0x1948175dDB81DA08a4cf17BE4E0C95B97dD11F5c",
|
|
8
|
+
agentPolicyRegistry: "0xce3b58c9ae8CA4724d6FA8684d2Cb89546FF4E43",
|
|
9
|
+
policyVerifier: "0xFCb2D2279256B62A1E4E07BCDed26B6546bBc33b"
|
|
10
|
+
}
|
|
11
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { PaymentRequest, WalletContext
|
|
1
|
+
import { PaymentRequest, WalletContext } from "./types";
|
|
2
2
|
import { PaymentExecutor } from "./executors";
|
|
3
|
+
import { AgentConfig } from "../config";
|
|
4
|
+
import { ethers } from "ethers";
|
|
3
5
|
/**
|
|
4
6
|
* AgentWallet
|
|
5
7
|
* ------------
|
|
@@ -7,11 +9,14 @@ import { PaymentExecutor } from "./executors";
|
|
|
7
9
|
* - Decides whether to pay
|
|
8
10
|
* - Delegates execution to PaymentExecutor
|
|
9
11
|
* - Enforces security + policy
|
|
10
|
-
* - Persists state
|
|
12
|
+
* - Persists state to MongoDB
|
|
11
13
|
*/
|
|
12
14
|
export declare class AgentWallet {
|
|
13
15
|
private readonly address;
|
|
14
16
|
private readonly executor;
|
|
17
|
+
private readonly provider;
|
|
18
|
+
private readonly wallet;
|
|
19
|
+
private readonly config;
|
|
15
20
|
private dailyLimit;
|
|
16
21
|
private maxPerTransaction;
|
|
17
22
|
private spentToday;
|
|
@@ -19,16 +24,26 @@ export declare class AgentWallet {
|
|
|
19
24
|
private allowedMerchants;
|
|
20
25
|
private trustedFacilitatorOrigins;
|
|
21
26
|
private stopped;
|
|
22
|
-
private
|
|
27
|
+
private isInitialized;
|
|
28
|
+
/**
|
|
29
|
+
* Replay protection
|
|
30
|
+
* key -> timestamp
|
|
31
|
+
*/
|
|
23
32
|
/**
|
|
24
33
|
* Replay protection
|
|
25
34
|
* key -> timestamp
|
|
26
35
|
*/
|
|
27
36
|
private paidRequests;
|
|
28
|
-
|
|
37
|
+
private policyRegistry?;
|
|
38
|
+
constructor(address: string, executor: PaymentExecutor, provider: ethers.Provider, wallet: ethers.Wallet, config: AgentConfig);
|
|
29
39
|
private getTodayDate;
|
|
40
|
+
init(): Promise<void>;
|
|
30
41
|
private loadState;
|
|
31
42
|
private saveState;
|
|
43
|
+
/**
|
|
44
|
+
* Checks if the local policy matches the on-chain anchor.
|
|
45
|
+
*/
|
|
46
|
+
private verifyPolicyAnchor;
|
|
32
47
|
getAddress(): string;
|
|
33
48
|
private getHeader;
|
|
34
49
|
private canonicalOrigin;
|
|
@@ -42,9 +57,9 @@ export declare class AgentWallet {
|
|
|
42
57
|
emergencyStop(): void;
|
|
43
58
|
shouldPay(request: PaymentRequest, context: WalletContext, originalChallenge?: {
|
|
44
59
|
route?: string;
|
|
45
|
-
}): {
|
|
60
|
+
}): Promise<{
|
|
46
61
|
allow: boolean;
|
|
47
62
|
reason?: string;
|
|
48
|
-
}
|
|
63
|
+
}>;
|
|
49
64
|
executePayment(request: PaymentRequest): Promise<string>;
|
|
50
65
|
}
|
|
@@ -1,57 +1,21 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
}
|
|
7
|
+
const PolicyRegistryAdapter_1 = require("./PolicyRegistryAdapter");
|
|
8
|
+
const ethers_1 = require("ethers");
|
|
9
|
+
// Helper to hash policy config
|
|
10
|
+
function hashPolicy(config) {
|
|
11
|
+
const policyData = {
|
|
12
|
+
dailyLimit: config.dailyLimit || 0,
|
|
13
|
+
maxPerTransaction: config.maxPerTransaction || 0
|
|
14
|
+
};
|
|
15
|
+
return ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(JSON.stringify(policyData)));
|
|
54
16
|
}
|
|
17
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
18
|
+
const models_1 = require("./models");
|
|
55
19
|
/**
|
|
56
20
|
* AgentWallet
|
|
57
21
|
* ------------
|
|
@@ -59,11 +23,14 @@ class BrowserPersistence {
|
|
|
59
23
|
* - Decides whether to pay
|
|
60
24
|
* - Delegates execution to PaymentExecutor
|
|
61
25
|
* - Enforces security + policy
|
|
62
|
-
* - Persists state
|
|
26
|
+
* - Persists state to MongoDB
|
|
63
27
|
*/
|
|
64
28
|
class AgentWallet {
|
|
65
29
|
address;
|
|
66
30
|
executor;
|
|
31
|
+
provider;
|
|
32
|
+
wallet;
|
|
33
|
+
config;
|
|
67
34
|
// ---------------- CONFIG ----------------
|
|
68
35
|
// Reduced limit for testing persistence (0.5 USDC)
|
|
69
36
|
dailyLimit = 0.5;
|
|
@@ -78,15 +45,24 @@ class AgentWallet {
|
|
|
78
45
|
"https://cronos-x-402-production.up.railway.app",
|
|
79
46
|
]);
|
|
80
47
|
stopped = false;
|
|
81
|
-
|
|
48
|
+
isInitialized = false;
|
|
49
|
+
/**
|
|
50
|
+
* Replay protection
|
|
51
|
+
* key -> timestamp
|
|
52
|
+
*/
|
|
82
53
|
/**
|
|
83
54
|
* Replay protection
|
|
84
55
|
* key -> timestamp
|
|
85
56
|
*/
|
|
86
57
|
paidRequests = new Map();
|
|
87
|
-
|
|
58
|
+
// On-Chain Adapter
|
|
59
|
+
policyRegistry;
|
|
60
|
+
constructor(address, executor, provider, wallet, config) {
|
|
88
61
|
this.address = address;
|
|
89
62
|
this.executor = executor;
|
|
63
|
+
this.provider = provider;
|
|
64
|
+
this.wallet = wallet;
|
|
65
|
+
this.config = config;
|
|
90
66
|
if (config) {
|
|
91
67
|
if (config.dailyLimit !== undefined)
|
|
92
68
|
this.dailyLimit = config.dailyLimit;
|
|
@@ -96,56 +72,143 @@ class AgentWallet {
|
|
|
96
72
|
this.allowedMerchants = new Set(config.allowedMerchants);
|
|
97
73
|
if (config.trustedFacilitators)
|
|
98
74
|
this.trustedFacilitatorOrigins = new Set(config.trustedFacilitators);
|
|
75
|
+
// Initialize On-Chain Adapter if configured
|
|
76
|
+
if (config.anchors?.agentPolicyRegistry) {
|
|
77
|
+
this.policyRegistry = new PolicyRegistryAdapter_1.PolicyRegistryAdapter(config.anchors.agentPolicyRegistry, provider, wallet);
|
|
78
|
+
// Non-blocking check for setup/warning
|
|
79
|
+
this.verifyPolicyAnchor().catch(err => {
|
|
80
|
+
console.error("[AgentWallet] Policy Anchor Warning:", err.message);
|
|
81
|
+
if (config.strictPolicy) {
|
|
82
|
+
process.exit(1); // Strict mode: fail fast
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
99
86
|
}
|
|
100
|
-
|
|
101
|
-
if (typeof window === "undefined") {
|
|
102
|
-
this.persistence = new NodePersistence();
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
this.persistence = new BrowserPersistence();
|
|
106
|
-
}
|
|
107
|
-
this.loadState();
|
|
87
|
+
this.lastResetDate = this.getTodayDate();
|
|
108
88
|
}
|
|
109
89
|
// ---------------- PERSISTENCE ----------------
|
|
110
90
|
getTodayDate() {
|
|
111
91
|
return new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
112
92
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
93
|
+
async init() {
|
|
94
|
+
if (this.isInitialized)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
// Auto-connect if not connected
|
|
98
|
+
if (mongoose_1.default.connection.readyState === 0) {
|
|
99
|
+
if (!process.env.MONGODB_URI) {
|
|
100
|
+
console.warn("[WALLET] MONGODB_URI missing. Persistence disabled.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await mongoose_1.default.connect(process.env.MONGODB_URI);
|
|
104
|
+
console.log("[WALLET] Connected to MongoDB");
|
|
105
|
+
}
|
|
106
|
+
await this.loadState();
|
|
107
|
+
this.isInitialized = true;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error("[WALLET] Initialization failed:", error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async loadState() {
|
|
114
|
+
try {
|
|
115
|
+
const doc = await models_1.AgentWalletModel.findOne({ address: this.address }).lean();
|
|
116
|
+
if (doc) {
|
|
117
|
+
// Helper to normalize DB Map vs POJO
|
|
118
|
+
const rawObj = doc.paidRequests || {};
|
|
119
|
+
const rawMap = rawObj instanceof Map
|
|
120
|
+
? rawObj
|
|
121
|
+
: new Map(Object.entries(rawObj));
|
|
122
|
+
// 1. Check if we need to reset for a new day
|
|
123
|
+
const today = this.getTodayDate();
|
|
124
|
+
if (doc.lastResetDate !== today) {
|
|
125
|
+
console.log(`[WALLET] New day detected! Resetting limit. (Last: ${doc.lastResetDate}, Today: ${today})`);
|
|
126
|
+
this.spentToday = 0;
|
|
127
|
+
this.lastResetDate = today;
|
|
128
|
+
// Cleanup old requests (older than 24h)
|
|
129
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
this.paidRequests = new Map();
|
|
132
|
+
for (const [key, val] of rawMap.entries()) {
|
|
133
|
+
const ts = Number(val);
|
|
134
|
+
if (!isNaN(ts) && (now - ts < MAX_AGE_MS)) {
|
|
135
|
+
this.paidRequests.set(key, ts);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Save cleanup immediately
|
|
139
|
+
await this.saveState();
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
console.log(`[WALLET] State loaded. Spent today: ${doc.spentToday.toFixed(4)} / ${this.dailyLimit}`);
|
|
143
|
+
this.spentToday = doc.spentToday;
|
|
144
|
+
this.lastResetDate = doc.lastResetDate;
|
|
145
|
+
this.paidRequests = new Map();
|
|
146
|
+
for (const [key, val] of rawMap.entries()) {
|
|
147
|
+
const ts = Number(val);
|
|
148
|
+
if (!isNaN(ts)) {
|
|
149
|
+
this.paidRequests.set(key, ts);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
126
153
|
}
|
|
127
154
|
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));
|
|
155
|
+
// Initialize fresh
|
|
156
|
+
this.lastResetDate = this.getTodayDate();
|
|
157
|
+
await this.saveState();
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
this.lastResetDate = this.getTodayDate();
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.warn("[WALLET] Failed to load state from DB", error);
|
|
140
162
|
}
|
|
141
163
|
}
|
|
142
|
-
saveState() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
async saveState() {
|
|
165
|
+
try {
|
|
166
|
+
await models_1.AgentWalletModel.findOneAndUpdate({ address: this.address }, {
|
|
167
|
+
address: this.address,
|
|
168
|
+
lastResetDate: this.lastResetDate,
|
|
169
|
+
spentToday: this.spentToday,
|
|
170
|
+
paidRequests: this.paidRequests
|
|
171
|
+
}, { upsert: true, new: true });
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
console.error("[WALLET] Failed to save state to DB", error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Checks if the local policy matches the on-chain anchor.
|
|
179
|
+
*/
|
|
180
|
+
async verifyPolicyAnchor() {
|
|
181
|
+
if (!this.policyRegistry)
|
|
182
|
+
return;
|
|
183
|
+
try {
|
|
184
|
+
console.log("[AgentWallet] Verifying policy against on-chain anchor...");
|
|
185
|
+
const onChain = await this.policyRegistry.getPolicy(this.address);
|
|
186
|
+
// Check 1: Freeze status
|
|
187
|
+
if (onChain.isFrozen) {
|
|
188
|
+
this.stopped = true;
|
|
189
|
+
throw new Error("Agent policy is FROZEN on-chain. Emergency stop activated.");
|
|
190
|
+
}
|
|
191
|
+
// Check 2: Hash mismatch
|
|
192
|
+
const localHash = hashPolicy(this.config);
|
|
193
|
+
// Note: We check if on-chain hash is set (non-zero) before enforcing
|
|
194
|
+
if (onChain.policyHash !== ethers_1.ethers.ZeroHash && onChain.policyHash !== localHash) {
|
|
195
|
+
const msg = `Policy Hash Mismatch! Local: ${localHash}, Chain: ${onChain.policyHash}`;
|
|
196
|
+
if (this.config?.strictPolicy) {
|
|
197
|
+
throw new Error(msg);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
console.warn(`[AgentWallet] WARN: ${msg}. Running in PERMISSIVE mode.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log("[AgentWallet] ✅ Policy verified on-chain.");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
console.error("[AgentWallet] Anchor check failed:", error.message);
|
|
209
|
+
if (this.config?.strictPolicy)
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
149
212
|
}
|
|
150
213
|
// ---------------- PUBLIC ----------------
|
|
151
214
|
getAddress() {
|
|
@@ -246,7 +309,10 @@ class AgentWallet {
|
|
|
246
309
|
this.stopped = true;
|
|
247
310
|
console.warn("[WALLET] Emergency stop activated!");
|
|
248
311
|
}
|
|
249
|
-
shouldPay(request, context, originalChallenge) {
|
|
312
|
+
async shouldPay(request, context, originalChallenge) {
|
|
313
|
+
// Ensure initialized
|
|
314
|
+
if (!this.isInitialized)
|
|
315
|
+
await this.init();
|
|
250
316
|
// -1. Emergency Stop
|
|
251
317
|
if (this.stopped) {
|
|
252
318
|
return { allow: false, reason: "Emergency stop active" };
|
|
@@ -256,8 +322,8 @@ class AgentWallet {
|
|
|
256
322
|
if (originalChallenge?.route && request.route !== originalChallenge.route) {
|
|
257
323
|
return { allow: false, reason: `Route mismatch: Challenge=${request.route}, Expected=${originalChallenge.route}` };
|
|
258
324
|
}
|
|
259
|
-
// 0. Currency check [
|
|
260
|
-
if (request.currency !== "USDC") {
|
|
325
|
+
// 0. Currency check [UPDATED]
|
|
326
|
+
if (request.currency !== "USDC" && request.currency !== "CRO") {
|
|
261
327
|
return { allow: false, reason: `Unsupported currency: ${request.currency}` };
|
|
262
328
|
}
|
|
263
329
|
// 1. Chain verification
|
|
@@ -287,6 +353,9 @@ class AgentWallet {
|
|
|
287
353
|
}
|
|
288
354
|
// ---------------- PAYMENT EXECUTION ----------------
|
|
289
355
|
async executePayment(request) {
|
|
356
|
+
// Ensure initialized
|
|
357
|
+
if (!this.isInitialized)
|
|
358
|
+
await this.init();
|
|
290
359
|
const key = this.paymentKey(request);
|
|
291
360
|
if (this.paidRequests.has(key)) {
|
|
292
361
|
throw new Error("Replay detected: payment already executed");
|
|
@@ -297,7 +366,7 @@ class AgentWallet {
|
|
|
297
366
|
this.spentToday += request.amount;
|
|
298
367
|
this.paidRequests.set(key, Date.now());
|
|
299
368
|
// Persist immediately
|
|
300
|
-
this.saveState();
|
|
369
|
+
await this.saveState();
|
|
301
370
|
return proof;
|
|
302
371
|
}
|
|
303
372
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
|
+
import { ethers } from "ethers";
|
|
2
3
|
import { PaymentExecutor } from "./executors";
|
|
3
4
|
import { PaymentRequest } from "./types";
|
|
4
5
|
export declare class CronosUsdcExecutor implements PaymentExecutor {
|
|
@@ -11,4 +12,6 @@ export declare class CronosUsdcExecutor implements PaymentExecutor {
|
|
|
11
12
|
private withRetry;
|
|
12
13
|
execute(request: PaymentRequest): Promise<string>;
|
|
13
14
|
getAddress(): string;
|
|
15
|
+
getProvider(): ethers.JsonRpcProvider;
|
|
16
|
+
getSigner(): ethers.Wallet;
|
|
14
17
|
}
|
|
@@ -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,11 +108,17 @@ 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() {
|
|
100
115
|
return this.wallet.address;
|
|
101
116
|
}
|
|
117
|
+
getProvider() {
|
|
118
|
+
return this.provider;
|
|
119
|
+
}
|
|
120
|
+
getSigner() {
|
|
121
|
+
return this.wallet;
|
|
122
|
+
}
|
|
102
123
|
}
|
|
103
124
|
exports.CronosUsdcExecutor = CronosUsdcExecutor;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ethers, Provider, Wallet } from "ethers";
|
|
2
|
+
export interface OnChainPolicy {
|
|
3
|
+
dailySpendLimit: bigint;
|
|
4
|
+
maxPerTransaction: bigint;
|
|
5
|
+
policyHash: string;
|
|
6
|
+
isFrozen: boolean;
|
|
7
|
+
lastUpdated: bigint;
|
|
8
|
+
}
|
|
9
|
+
export interface PolicyInput {
|
|
10
|
+
dailySpendLimit: bigint;
|
|
11
|
+
maxPerTransaction: bigint;
|
|
12
|
+
policyHash: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class PolicyRegistryAdapter {
|
|
15
|
+
private address;
|
|
16
|
+
private provider;
|
|
17
|
+
private signer?;
|
|
18
|
+
private contract;
|
|
19
|
+
constructor(address: string, provider: Provider, signer?: Wallet | undefined);
|
|
20
|
+
/**
|
|
21
|
+
* Reads the policy for the given agent from the blockchain.
|
|
22
|
+
* @param agentAddress The address to query
|
|
23
|
+
* @returns The policy struct or null if not found
|
|
24
|
+
*/
|
|
25
|
+
getPolicy(agentAddress: string): Promise<OnChainPolicy>;
|
|
26
|
+
/**
|
|
27
|
+
* Writes the policy to the blockchain.
|
|
28
|
+
* @warning This is a gas-consuming transaction.
|
|
29
|
+
* @warning Should strictly be used during SETUP or RECOVERY, not during normal agent operation.
|
|
30
|
+
*/
|
|
31
|
+
setPolicy(policy: PolicyInput): Promise<ethers.TransactionReceipt>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PolicyRegistryAdapter = void 0;
|
|
4
|
+
const ethers_1 = require("ethers");
|
|
5
|
+
const ABI = [
|
|
6
|
+
"function getPolicy(address agentAddress) external view returns (uint256 dailySpendLimit, uint256 maxPerTransaction, bytes32 policyHash, bool isFrozen, uint256 lastUpdated)",
|
|
7
|
+
"function setPolicy(uint256 dailySpendLimit, uint256 maxPerTransaction, bytes32 policyHash) external",
|
|
8
|
+
"event PolicySet(address indexed agent, uint256 dailySpendLimit, uint256 maxPerTransaction, bytes32 policyHash, uint256 timestamp)"
|
|
9
|
+
];
|
|
10
|
+
class PolicyRegistryAdapter {
|
|
11
|
+
address;
|
|
12
|
+
provider;
|
|
13
|
+
signer;
|
|
14
|
+
contract;
|
|
15
|
+
constructor(address, provider, signer) {
|
|
16
|
+
this.address = address;
|
|
17
|
+
this.provider = provider;
|
|
18
|
+
this.signer = signer;
|
|
19
|
+
this.contract = new ethers_1.Contract(address, ABI, signer || provider);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads the policy for the given agent from the blockchain.
|
|
23
|
+
* @param agentAddress The address to query
|
|
24
|
+
* @returns The policy struct or null if not found
|
|
25
|
+
*/
|
|
26
|
+
async getPolicy(agentAddress) {
|
|
27
|
+
return this.contract.getPolicy(agentAddress);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Writes the policy to the blockchain.
|
|
31
|
+
* @warning This is a gas-consuming transaction.
|
|
32
|
+
* @warning Should strictly be used during SETUP or RECOVERY, not during normal agent operation.
|
|
33
|
+
*/
|
|
34
|
+
async setPolicy(policy) {
|
|
35
|
+
if (!this.signer) {
|
|
36
|
+
throw new Error("PolicyRegistryAdapter: No signer available for setPolicy");
|
|
37
|
+
}
|
|
38
|
+
// Runtime Guard: We add a parameter or check logic to prevent accidental usage.
|
|
39
|
+
// For now, explicit naming and documentation is the primary guard, plus the requirement of a signer.
|
|
40
|
+
const tx = await this.contract.setPolicy(policy.dailySpendLimit, policy.maxPerTransaction, policy.policyHash);
|
|
41
|
+
return tx.wait();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.PolicyRegistryAdapter = PolicyRegistryAdapter;
|
|
@@ -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();
|