cronos-agent-wallet 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/dist/AgentClient.d.ts +46 -0
- package/dist/AgentClient.js +76 -0
- package/dist/check_autovvs.d.ts +1 -0
- package/dist/check_autovvs.js +44 -0
- package/dist/check_contract.d.ts +1 -0
- package/dist/check_contract.js +48 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +2 -0
- package/dist/demo.d.ts +1 -0
- package/dist/demo.js +70 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +21 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/internal/AgentWallet.d.ts +50 -0
- package/dist/internal/AgentWallet.js +304 -0
- package/dist/internal/CronosUsdcExecutor.d.ts +14 -0
- package/dist/internal/CronosUsdcExecutor.js +103 -0
- package/dist/internal/VvsYieldExecutor.d.ts +18 -0
- package/dist/internal/VvsYieldExecutor.js +48 -0
- package/dist/internal/YieldExecutor.d.ts +4 -0
- package/dist/internal/YieldExecutor.js +2 -0
- package/dist/internal/executors.d.ts +7 -0
- package/dist/internal/executors.js +11 -0
- package/dist/internal/types.d.ts +21 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/wallet-state.json +14 -0
- package/dist/internal/x402ToolClient.d.ts +20 -0
- package/dist/internal/x402ToolClient.js +147 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Cronos Merchant SDK
|
|
2
|
+
|
|
3
|
+
The official SDK for building AI agents that can pay for resources on the Cronos blockchain using the x402 protocol.
|
|
4
|
+
|
|
5
|
+
## 🛡️ Security Features (v1.1.1)
|
|
6
|
+
- **Zero Trust Payer:** Derives identity strictly from chain data.
|
|
7
|
+
- **Strong Replay Protection:** Enforces cryptographic binding of `merchantId + route + nonce`.
|
|
8
|
+
- **Anti-Spoofing:** Validates payment challenges against requested routes before paying.
|
|
9
|
+
- **Fail-Safe:** Supports "Fail Closed" mode for maximum security.
|
|
10
|
+
|
|
11
|
+
Key features:
|
|
12
|
+
* **Automatic 402 Payments**: Handles "Payment Required" challenges seamlessly.
|
|
13
|
+
* **Multi-Chain**: Auto-negotiates with backends (EVM/Solana support).
|
|
14
|
+
* **Safe**: Built-in daily spending limits and policy controls.
|
|
15
|
+
* **Type-Safe**: Generic `fetch` and structured `AgentError` handling.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @cronos-merchant/sdk ethers
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { AgentClient, AgentError } from "@cronos-merchant/sdk";
|
|
27
|
+
|
|
28
|
+
// 1. Initialize
|
|
29
|
+
const agent = new AgentClient({
|
|
30
|
+
privateKey: process.env.AGENT_KEY,
|
|
31
|
+
rpcUrl: "https://evm-t3.cronos.org", // Cronos Testnet
|
|
32
|
+
chainId: 338,
|
|
33
|
+
usdcAddress: "0xc01..." // Your payment token
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
try {
|
|
38
|
+
// 2. Fetch paid resources (just like axios/fetch)
|
|
39
|
+
const response = await agent.fetch<{ answer: string }>("http://localhost:3000/premium", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
body: { prompt: "Hello World" }
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log("Success:", response.answer);
|
|
45
|
+
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
// 3. Handle Errors
|
|
48
|
+
if (err instanceof AgentError) {
|
|
49
|
+
console.error(`Status: ${err.status}`); // 402, 500
|
|
50
|
+
console.error(`Code: ${err.code}`); // POLICY_REJECTED, NETWORK_ERROR
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
`new AgentClient(config)`
|
|
59
|
+
|
|
60
|
+
| Option | Type | Required | Description |
|
|
61
|
+
| :--- | :--- | :--- | :--- |
|
|
62
|
+
| `privateKey` | `string` | Yes | EVM private key for the agent wallet. |
|
|
63
|
+
| `rpcUrl` | `string` | Yes | RPC Endpoint (e.g., Cronos Testnet). |
|
|
64
|
+
| `chainId` | `number` | Yes | Chain ID (e.g., 338). Sent to backend for negotiation. |
|
|
65
|
+
| `usdcAddress` | `string` | Yes | ERC20 Token Address used for payment. |
|
|
66
|
+
| `dailyLimit` | `number` | No | Max USDC allowed to spend per 24h. Default: 1.0 |
|
|
67
|
+
| `allowedMerchants` | `string[]` | No | List of Merchant IDs to trust. If empty, allows all. |
|
|
68
|
+
| `trustedFacilitators` | `string[]` | No | List of Gateway URLs to trust (e.g., localhost). |
|
|
69
|
+
|
|
70
|
+
## API Reference
|
|
71
|
+
|
|
72
|
+
### `agent.fetch<T>(url, options): Promise<T>`
|
|
73
|
+
|
|
74
|
+
Performs an HTTP request. If the server responds with **402 Payment Required**:
|
|
75
|
+
1. SDK parses the payment request (from Headers or Body).
|
|
76
|
+
2. Checks spending policies (Daily Limit, Whitelist).
|
|
77
|
+
3. Executes the payment on-chain.
|
|
78
|
+
4. Retries the original request with the Payment Proof.
|
|
79
|
+
|
|
80
|
+
**Generics**:
|
|
81
|
+
Pass the expected response type `<T>` for TypeScript autocomplete.
|
|
82
|
+
|
|
83
|
+
**Options**:
|
|
84
|
+
* `method`: "GET" | "POST"
|
|
85
|
+
* `headers`: Dictionary of headers.
|
|
86
|
+
* `body`: JSON object or string.
|
|
87
|
+
* `allowBodyFallback`: `boolean`. Defaults to `false`. Set to `true` to allow parsing 402 details from the response body if headers are missing. DANGEROUS: Only use with trusted facilitators.
|
|
88
|
+
|
|
89
|
+
### `AgentError`
|
|
90
|
+
|
|
91
|
+
Thrown when a request fails or is blocked by policy.
|
|
92
|
+
|
|
93
|
+
* `err.status`: The HTTP status code (e.g., 400, 402, 500).
|
|
94
|
+
* `err.code`: A machine-readable string:
|
|
95
|
+
* `POLICY_REJECTED`: Blocked by daily limit or whitelist.
|
|
96
|
+
* `INSUFFICIENT_FUNDS`: Not enough USDC/Gas.
|
|
97
|
+
* `HTTP_ERROR`: Server returned an error (details in `err.details`).
|
|
98
|
+
* `err.details`: The raw response body from the server.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AgentConfig } from "./config";
|
|
2
|
+
/**
|
|
3
|
+
* AgentClient
|
|
4
|
+
* -----------
|
|
5
|
+
* Public SDK entry.
|
|
6
|
+
* - One instance per user/session
|
|
7
|
+
* - Wraps wallet + executor + x402 flow
|
|
8
|
+
*/
|
|
9
|
+
export declare class AgentClient {
|
|
10
|
+
private wallet;
|
|
11
|
+
private context;
|
|
12
|
+
constructor(config: AgentConfig);
|
|
13
|
+
/**
|
|
14
|
+
* agent.fetch()
|
|
15
|
+
* --------------
|
|
16
|
+
* Drop-in replacement for fetch/axios
|
|
17
|
+
* Handles:
|
|
18
|
+
* - free APIs
|
|
19
|
+
* - x402 paid APIs
|
|
20
|
+
* - auto-pay + retry
|
|
21
|
+
*/
|
|
22
|
+
fetch<T>(url: string, options?: {
|
|
23
|
+
method?: "GET" | "POST";
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
body?: any;
|
|
26
|
+
allowBodyFallback?: boolean;
|
|
27
|
+
}): Promise<T>;
|
|
28
|
+
fetchWithDetails<T>(url: string, options?: {
|
|
29
|
+
method?: "GET" | "POST";
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
body?: any;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
allowBodyFallback?: boolean;
|
|
34
|
+
}): Promise<{
|
|
35
|
+
data: T;
|
|
36
|
+
payment: any;
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Emergency stop
|
|
40
|
+
*/
|
|
41
|
+
stop(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Wallet address (agent wallet)
|
|
44
|
+
*/
|
|
45
|
+
getAddress(): string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentClient = void 0;
|
|
4
|
+
// AgentClient.ts
|
|
5
|
+
const AgentWallet_1 = require("./internal/AgentWallet");
|
|
6
|
+
const CronosUsdcExecutor_1 = require("./internal/CronosUsdcExecutor");
|
|
7
|
+
const x402ToolClient_1 = require("./internal/x402ToolClient");
|
|
8
|
+
/**
|
|
9
|
+
* AgentClient
|
|
10
|
+
* -----------
|
|
11
|
+
* Public SDK entry.
|
|
12
|
+
* - One instance per user/session
|
|
13
|
+
* - Wraps wallet + executor + x402 flow
|
|
14
|
+
*/
|
|
15
|
+
class AgentClient {
|
|
16
|
+
wallet;
|
|
17
|
+
context;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
if (!config.privateKey) {
|
|
20
|
+
throw new Error("AgentClient: privateKey is required");
|
|
21
|
+
}
|
|
22
|
+
if (!config.rpcUrl) {
|
|
23
|
+
throw new Error("AgentClient: rpcUrl is required");
|
|
24
|
+
}
|
|
25
|
+
if (!config.chainId) {
|
|
26
|
+
throw new Error("AgentClient: chainId is required");
|
|
27
|
+
}
|
|
28
|
+
// 1. Create executor (chain-specific)
|
|
29
|
+
const executor = new CronosUsdcExecutor_1.CronosUsdcExecutor(config.rpcUrl, config.privateKey, config.usdcAddress, config.chainId);
|
|
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
|
+
});
|
|
37
|
+
// 3. Context passed per request
|
|
38
|
+
this.context = {
|
|
39
|
+
chainId: config.chainId,
|
|
40
|
+
merchantId: config.merchantId,
|
|
41
|
+
analyticsUrl: config.analyticsUrl
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* agent.fetch()
|
|
46
|
+
* --------------
|
|
47
|
+
* Drop-in replacement for fetch/axios
|
|
48
|
+
* Handles:
|
|
49
|
+
* - free APIs
|
|
50
|
+
* - x402 paid APIs
|
|
51
|
+
* - auto-pay + retry
|
|
52
|
+
*/
|
|
53
|
+
async fetch(url, options) {
|
|
54
|
+
const res = await this.fetchWithDetails(url, options);
|
|
55
|
+
return res.data;
|
|
56
|
+
}
|
|
57
|
+
async fetchWithDetails(url, options) {
|
|
58
|
+
console.log("[SDK] AgentClient.fetch called");
|
|
59
|
+
console.log(`[SDK] URL: ${url}`);
|
|
60
|
+
console.log(`[SDK] Options:`, options);
|
|
61
|
+
return (0, x402ToolClient_1.x402Request)(url, this.wallet, this.context, options);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Emergency stop
|
|
65
|
+
*/
|
|
66
|
+
stop() {
|
|
67
|
+
this.wallet.emergencyStop();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Wallet address (agent wallet)
|
|
71
|
+
*/
|
|
72
|
+
getAddress() {
|
|
73
|
+
return this.wallet.getAddress();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.AgentClient = AgentClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ethers_1 = require("ethers");
|
|
4
|
+
const RPC = "https://evm.cronos.org";
|
|
5
|
+
const ADDR = "0xA6fF77fC8E839679D4F7408E8988B564dE1A2dcD";
|
|
6
|
+
const USER = "0x2149AbA8305029f8Db6dBC858dadf4112832D0ff";
|
|
7
|
+
async function check() {
|
|
8
|
+
const provider = new ethers_1.ethers.JsonRpcProvider(RPC);
|
|
9
|
+
// Probe userInfo structure and balanceOf (no args)
|
|
10
|
+
const abi = [
|
|
11
|
+
"function userInfo(address) view returns (uint256 shares, uint256 lastDepositedTime, uint256 cakeAtLastUserAction, uint256 lastUserActionTime)", // Standard VVS Vault pattern guess
|
|
12
|
+
"function balanceOf() view returns (uint256)",
|
|
13
|
+
"function getPricePerFullShare() view returns (uint256)"
|
|
14
|
+
];
|
|
15
|
+
const contract = new ethers_1.ethers.Contract(ADDR, abi, provider);
|
|
16
|
+
try {
|
|
17
|
+
console.log("Calling balanceOf()..."); // Total vault balance
|
|
18
|
+
const bal = await contract.balanceOf();
|
|
19
|
+
console.log("✅ Total Vault Balance:", bal.toString());
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
console.log("⚠️ balanceOf() failed:", e.reason || e.message);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
console.log(`Calling userInfo(${USER})...`);
|
|
26
|
+
// We try to fetch it as an array/object to see what comes back
|
|
27
|
+
const info = await contract.userInfo(USER);
|
|
28
|
+
console.log("✅ userInfo result:", info);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
console.log("⚠️ userInfo failed:", e.reason || e.message);
|
|
32
|
+
// Fallback: try simpler ABI if the struct guess failed
|
|
33
|
+
try {
|
|
34
|
+
const abiSimple = ["function userInfo(address) view returns (uint256, uint256, uint256, uint256)"];
|
|
35
|
+
const c2 = new ethers_1.ethers.Contract(ADDR, abiSimple, provider);
|
|
36
|
+
const info2 = await c2.userInfo(USER);
|
|
37
|
+
console.log("✅ userInfo (generic) result:", info2);
|
|
38
|
+
}
|
|
39
|
+
catch (ex) {
|
|
40
|
+
console.log("❌ userInfo generic failed:", ex.reason || ex.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
check();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ethers_1 = require("ethers");
|
|
4
|
+
const RPC = "https://evm.cronos.org";
|
|
5
|
+
const ADDR = "0xA6fF77fC8E839679D4F7408E8988B564dE1A2dcD";
|
|
6
|
+
async function check() {
|
|
7
|
+
const provider = new ethers_1.ethers.JsonRpcProvider(RPC);
|
|
8
|
+
console.log(`Checking code at ${ADDR} on ${RPC}...`);
|
|
9
|
+
const code = await provider.getCode(ADDR);
|
|
10
|
+
console.log(`Code length: ${code.length}`);
|
|
11
|
+
if (code === "0x") {
|
|
12
|
+
console.log("❌ No code found (EOA or empty).");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
console.log("✅ Contract code found.");
|
|
16
|
+
const abi = [
|
|
17
|
+
"function name() view returns (string)",
|
|
18
|
+
"function symbol() view returns (string)",
|
|
19
|
+
"function poolLength() view returns (uint256)",
|
|
20
|
+
"function balanceOf(address) view returns (uint256)"
|
|
21
|
+
];
|
|
22
|
+
const contract = new ethers_1.ethers.Contract(ADDR, abi, provider);
|
|
23
|
+
try {
|
|
24
|
+
console.log("Probing name()...");
|
|
25
|
+
const name = await contract.name();
|
|
26
|
+
console.log("✅ Name:", name);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.log("⚠️ name() failed");
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
console.log("Probing poolLength()...");
|
|
33
|
+
const len = await contract.poolLength();
|
|
34
|
+
console.log("✅ poolLength:", len.toString());
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.log("⚠️ poolLength() failed");
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
console.log("Probing balanceOf(random)...");
|
|
41
|
+
const bal = await contract.balanceOf("0x000000000000000000000000000000000000dead");
|
|
42
|
+
console.log("✅ balanceOf:", bal.toString());
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.log("⚠️ balanceOf() failed");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
check();
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface AgentConfig {
|
|
2
|
+
privateKey: string;
|
|
3
|
+
rpcUrl: string;
|
|
4
|
+
chainId: number;
|
|
5
|
+
usdcAddress: string;
|
|
6
|
+
dailyLimit?: number;
|
|
7
|
+
maxPerTransaction?: number;
|
|
8
|
+
allowedMerchants?: string[];
|
|
9
|
+
trustedFacilitators?: string[];
|
|
10
|
+
analyticsUrl?: string;
|
|
11
|
+
merchantId?: string;
|
|
12
|
+
}
|
package/dist/config.js
ADDED
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "dotenv/config";
|
package/dist/demo.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// demo.ts
|
|
4
|
+
require("dotenv/config");
|
|
5
|
+
const AgentWallet_1 = require("./internal/AgentWallet");
|
|
6
|
+
const CronosUsdcExecutor_1 = require("./internal/CronosUsdcExecutor");
|
|
7
|
+
const x402ToolClient_1 = require("./internal/x402ToolClient");
|
|
8
|
+
const ethers_1 = require("ethers");
|
|
9
|
+
const VvsYieldExecutor_1 = require("./internal/VvsYieldExecutor");
|
|
10
|
+
async function run() {
|
|
11
|
+
// --------------------------------------------------
|
|
12
|
+
// 1. Merchant API
|
|
13
|
+
// --------------------------------------------------
|
|
14
|
+
const MERCHANT_API_URL = "http://localhost:3001";
|
|
15
|
+
const TARGET_PATH = "/photos";
|
|
16
|
+
const FULL_URL = `${MERCHANT_API_URL}${TARGET_PATH}`;
|
|
17
|
+
// --------------------------------------------------
|
|
18
|
+
// 2. RPCs
|
|
19
|
+
// --------------------------------------------------
|
|
20
|
+
const MAINNET_RPC = "https://evm.cronos.org";
|
|
21
|
+
const TESTNET_RPC = process.env.CRONOS_RPC_URL || "https://evm-t3.cronos.org";
|
|
22
|
+
const USDC_ADDR = process.env.USDC_CONTRACT_ADDRESS ||
|
|
23
|
+
"0xc01efAaF7C5C61bEbFAeb358E1161b537b8bC0e0";
|
|
24
|
+
const PRIVATE_KEY = process.env.AGENT_WALLET_PRIVATE_KEY;
|
|
25
|
+
if (!PRIVATE_KEY)
|
|
26
|
+
throw new Error("Missing AGENT_WALLET_PRIVATE_KEY");
|
|
27
|
+
// --------------------------------------------------
|
|
28
|
+
// 3. Agent (payments)
|
|
29
|
+
// --------------------------------------------------
|
|
30
|
+
const paymentExecutor = new CronosUsdcExecutor_1.CronosUsdcExecutor(TESTNET_RPC, PRIVATE_KEY, USDC_ADDR, 338);
|
|
31
|
+
const wallet = new AgentWallet_1.AgentWallet(new ethers_1.ethers.Wallet(PRIVATE_KEY).address, paymentExecutor);
|
|
32
|
+
// --------------------------------------------------
|
|
33
|
+
// 4. AutoVVS Yield Sensor (Mainnet)
|
|
34
|
+
// --------------------------------------------------
|
|
35
|
+
const provider = new ethers_1.ethers.JsonRpcProvider(MAINNET_RPC);
|
|
36
|
+
const ethersWallet = new ethers_1.ethers.Wallet(PRIVATE_KEY, provider);
|
|
37
|
+
const yieldExec = new VvsYieldExecutor_1.VvsYieldExecutor(provider, ethersWallet);
|
|
38
|
+
console.log("--------------------------------------------------");
|
|
39
|
+
console.log(`🤖 AGENT: ${ethersWallet.address}`);
|
|
40
|
+
console.log("🌱 AutoVVS Vault: read-only position");
|
|
41
|
+
console.log("--------------------------------------------------");
|
|
42
|
+
try {
|
|
43
|
+
const pos = await yieldExec.getVaultPosition();
|
|
44
|
+
console.log("📊 Vault Position (RAW):", {
|
|
45
|
+
shares: pos.shares.toString(),
|
|
46
|
+
pricePerShare: pos.pricePerShare.toString(),
|
|
47
|
+
underlyingValue: pos.underlyingValue.toString()
|
|
48
|
+
});
|
|
49
|
+
console.log("📊 Underlying (formatted, 18d):", ethers_1.ethers.formatUnits(pos.underlyingValue, 18));
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error("❌ AutoVVS sensor error:", err.message);
|
|
53
|
+
}
|
|
54
|
+
// --------------------------------------------------
|
|
55
|
+
// 5. x402 flow (unchanged)
|
|
56
|
+
// --------------------------------------------------
|
|
57
|
+
console.log(`[x402] Negotiating access for: ${TARGET_PATH}...`);
|
|
58
|
+
try {
|
|
59
|
+
const result = await (0, x402ToolClient_1.x402Request)(FULL_URL, wallet, {
|
|
60
|
+
chainId: 338,
|
|
61
|
+
merchantId: "b9805b9e-fa6c-4640-8470-f5b230dee6d4"
|
|
62
|
+
}, {});
|
|
63
|
+
console.log("✅ Success! Data Received:");
|
|
64
|
+
console.dir(result, { depth: null });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(`❌ Failed: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
run();
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type AgentErrorCode = "POLICY_REJECTED" | "INSUFFICIENT_FUNDS" | "UNTRUSTED_FACILITATOR" | "DAILY_LIMIT_EXCEEDED" | "NETWORK_ERROR" | "HTTP_ERROR" | "PROTOCOL_ERROR" | "UNKNOWN_ERROR";
|
|
2
|
+
export declare class AgentError extends Error {
|
|
3
|
+
message: string;
|
|
4
|
+
code: AgentErrorCode;
|
|
5
|
+
status?: number | undefined;
|
|
6
|
+
details?: any | undefined;
|
|
7
|
+
constructor(message: string, code?: AgentErrorCode, status?: number | undefined, // HTTP Status (400, 402, 500)
|
|
8
|
+
details?: any | undefined);
|
|
9
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// errors.ts
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.AgentError = void 0;
|
|
5
|
+
class AgentError extends Error {
|
|
6
|
+
message;
|
|
7
|
+
code;
|
|
8
|
+
status;
|
|
9
|
+
details;
|
|
10
|
+
constructor(message, code = "UNKNOWN_ERROR", status, // HTTP Status (400, 402, 500)
|
|
11
|
+
details // Full response body or context
|
|
12
|
+
) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.message = message;
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.details = details;
|
|
18
|
+
this.name = "AgentError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.AgentError = AgentError;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentError = exports.AgentClient = void 0;
|
|
4
|
+
var AgentClient_1 = require("./AgentClient");
|
|
5
|
+
Object.defineProperty(exports, "AgentClient", { enumerable: true, get: function () { return AgentClient_1.AgentClient; } });
|
|
6
|
+
var errors_1 = require("./errors");
|
|
7
|
+
Object.defineProperty(exports, "AgentError", { enumerable: true, get: function () { return errors_1.AgentError; } });
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PaymentRequest, WalletContext, AgentWalletConfig } from "./types";
|
|
2
|
+
import { PaymentExecutor } from "./executors";
|
|
3
|
+
/**
|
|
4
|
+
* AgentWallet
|
|
5
|
+
* ------------
|
|
6
|
+
* - Owns identity (address)
|
|
7
|
+
* - Decides whether to pay
|
|
8
|
+
* - Delegates execution to PaymentExecutor
|
|
9
|
+
* - Enforces security + policy
|
|
10
|
+
* - Persists state (Isomorphic: FS or LocalStorage)
|
|
11
|
+
*/
|
|
12
|
+
export declare class AgentWallet {
|
|
13
|
+
private readonly address;
|
|
14
|
+
private readonly executor;
|
|
15
|
+
private dailyLimit;
|
|
16
|
+
private maxPerTransaction;
|
|
17
|
+
private spentToday;
|
|
18
|
+
private lastResetDate;
|
|
19
|
+
private allowedMerchants;
|
|
20
|
+
private trustedFacilitatorOrigins;
|
|
21
|
+
private stopped;
|
|
22
|
+
private persistence;
|
|
23
|
+
/**
|
|
24
|
+
* Replay protection
|
|
25
|
+
* key -> timestamp
|
|
26
|
+
*/
|
|
27
|
+
private paidRequests;
|
|
28
|
+
constructor(address: string, executor: PaymentExecutor, config?: AgentWalletConfig);
|
|
29
|
+
private getTodayDate;
|
|
30
|
+
private loadState;
|
|
31
|
+
private saveState;
|
|
32
|
+
getAddress(): string;
|
|
33
|
+
private getHeader;
|
|
34
|
+
private canonicalOrigin;
|
|
35
|
+
/**
|
|
36
|
+
* Strong uniqueness:
|
|
37
|
+
* merchant + route + nonce
|
|
38
|
+
*/
|
|
39
|
+
private paymentKey;
|
|
40
|
+
parse402Header(headers: any): PaymentRequest;
|
|
41
|
+
parse402Body(body: any, defaultMerchantId: string, requestUrl: string): PaymentRequest;
|
|
42
|
+
emergencyStop(): void;
|
|
43
|
+
shouldPay(request: PaymentRequest, context: WalletContext, originalChallenge?: {
|
|
44
|
+
route?: string;
|
|
45
|
+
}): {
|
|
46
|
+
allow: boolean;
|
|
47
|
+
reason?: string;
|
|
48
|
+
};
|
|
49
|
+
executePayment(request: PaymentRequest): Promise<string>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
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
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* AgentWallet
|
|
57
|
+
* ------------
|
|
58
|
+
* - Owns identity (address)
|
|
59
|
+
* - Decides whether to pay
|
|
60
|
+
* - Delegates execution to PaymentExecutor
|
|
61
|
+
* - Enforces security + policy
|
|
62
|
+
* - Persists state (Isomorphic: FS or LocalStorage)
|
|
63
|
+
*/
|
|
64
|
+
class AgentWallet {
|
|
65
|
+
address;
|
|
66
|
+
executor;
|
|
67
|
+
// ---------------- CONFIG ----------------
|
|
68
|
+
// Reduced limit for testing persistence (0.5 USDC)
|
|
69
|
+
dailyLimit = 0.5;
|
|
70
|
+
maxPerTransaction = 0.5; // Default safe limit
|
|
71
|
+
spentToday = 0.0;
|
|
72
|
+
lastResetDate = "";
|
|
73
|
+
allowedMerchants = new Set([]);
|
|
74
|
+
trustedFacilitatorOrigins = new Set([
|
|
75
|
+
"http://localhost:5000",
|
|
76
|
+
"http://localhost:3000",
|
|
77
|
+
"https://agentx402.onrender.com",
|
|
78
|
+
"https://cronos-x-402-production.up.railway.app",
|
|
79
|
+
]);
|
|
80
|
+
stopped = false;
|
|
81
|
+
persistence;
|
|
82
|
+
/**
|
|
83
|
+
* Replay protection
|
|
84
|
+
* key -> timestamp
|
|
85
|
+
*/
|
|
86
|
+
paidRequests = new Map();
|
|
87
|
+
constructor(address, executor, config) {
|
|
88
|
+
this.address = address;
|
|
89
|
+
this.executor = executor;
|
|
90
|
+
if (config) {
|
|
91
|
+
if (config.dailyLimit !== undefined)
|
|
92
|
+
this.dailyLimit = config.dailyLimit;
|
|
93
|
+
if (config.maxPerTransaction !== undefined)
|
|
94
|
+
this.maxPerTransaction = config.maxPerTransaction;
|
|
95
|
+
if (config.allowedMerchants)
|
|
96
|
+
this.allowedMerchants = new Set(config.allowedMerchants);
|
|
97
|
+
if (config.trustedFacilitators)
|
|
98
|
+
this.trustedFacilitatorOrigins = new Set(config.trustedFacilitators);
|
|
99
|
+
}
|
|
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();
|
|
108
|
+
}
|
|
109
|
+
// ---------------- PERSISTENCE ----------------
|
|
110
|
+
getTodayDate() {
|
|
111
|
+
return new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
112
|
+
}
|
|
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));
|
|
126
|
+
}
|
|
127
|
+
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));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Initialize fresh
|
|
139
|
+
this.lastResetDate = this.getTodayDate();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
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);
|
|
149
|
+
}
|
|
150
|
+
// ---------------- PUBLIC ----------------
|
|
151
|
+
getAddress() {
|
|
152
|
+
return this.address;
|
|
153
|
+
}
|
|
154
|
+
// ---------------- HELPERS ----------------
|
|
155
|
+
getHeader(headers, key) {
|
|
156
|
+
return headers[key] ?? headers[key.toLowerCase()];
|
|
157
|
+
}
|
|
158
|
+
canonicalOrigin(url) {
|
|
159
|
+
try {
|
|
160
|
+
return new URL(url).origin;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Strong uniqueness:
|
|
168
|
+
* merchant + route + nonce
|
|
169
|
+
*/
|
|
170
|
+
paymentKey(req) {
|
|
171
|
+
return `${req.merchantId}:${req.route}:${req.nonce}`;
|
|
172
|
+
}
|
|
173
|
+
// ---------------- x402 PARSER ----------------
|
|
174
|
+
parse402Header(headers) {
|
|
175
|
+
const amount = this.getHeader(headers, "x-payment-amount");
|
|
176
|
+
const currency = this.getHeader(headers, "x-payment-currency");
|
|
177
|
+
const payTo = this.getHeader(headers, "x-payment-payto");
|
|
178
|
+
const merchantId = this.getHeader(headers, "x-merchant-id");
|
|
179
|
+
const facilitatorUrl = this.getHeader(headers, "x-facilitator-url");
|
|
180
|
+
const chainId = this.getHeader(headers, "x-chain-id");
|
|
181
|
+
const route = this.getHeader(headers, "x-route");
|
|
182
|
+
const nonce = this.getHeader(headers, "x-nonce");
|
|
183
|
+
if (!amount ||
|
|
184
|
+
!currency ||
|
|
185
|
+
!payTo ||
|
|
186
|
+
!merchantId ||
|
|
187
|
+
!facilitatorUrl ||
|
|
188
|
+
!chainId ||
|
|
189
|
+
!route ||
|
|
190
|
+
!nonce) {
|
|
191
|
+
throw new Error("Malformed x402 payment headers");
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
amount: Number(amount),
|
|
195
|
+
currency,
|
|
196
|
+
payTo,
|
|
197
|
+
merchantId,
|
|
198
|
+
facilitatorUrl,
|
|
199
|
+
chainId: Number(chainId),
|
|
200
|
+
route,
|
|
201
|
+
nonce,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
parse402Body(body, defaultMerchantId, requestUrl) {
|
|
205
|
+
// Expecting body structure:
|
|
206
|
+
// { paymentRequest: { chainId, currency, receiver, amount, token ... } }
|
|
207
|
+
// Support both wrapped { paymentRequest: ... } and raw { ... } structures
|
|
208
|
+
// Robust strategy: Check if 'paymentRequest' key exists, otherwise assume body IS the request if it has 'amount'
|
|
209
|
+
let req = body?.paymentRequest || body;
|
|
210
|
+
// Double check deep nesting (some frameworks might wrap twice)
|
|
211
|
+
if (req?.paymentRequest)
|
|
212
|
+
req = req.paymentRequest;
|
|
213
|
+
if (!req) {
|
|
214
|
+
throw new Error("Missing 'paymentRequest' in response body");
|
|
215
|
+
}
|
|
216
|
+
const amount = req.amount;
|
|
217
|
+
const currency = req.currency;
|
|
218
|
+
const payTo = req.receiver;
|
|
219
|
+
// Body usually doesn't repeat merchantId
|
|
220
|
+
const merchantId = defaultMerchantId;
|
|
221
|
+
const facilitatorUrl = requestUrl;
|
|
222
|
+
const chainId = req.chainId;
|
|
223
|
+
const route = "premium";
|
|
224
|
+
// [SECURITY] NONCE AUTHORITY
|
|
225
|
+
// Server MUST provide nonce. No fallbacks allowed.
|
|
226
|
+
const nonce = req.nonce;
|
|
227
|
+
if (!nonce) {
|
|
228
|
+
throw new Error("Missing 'nonce' in 402 body - Server is the sole nonce authority.");
|
|
229
|
+
}
|
|
230
|
+
if (!amount || !currency || !payTo || !chainId) {
|
|
231
|
+
throw new Error(`Incomplete 402 body: ${JSON.stringify(req)}`);
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
amount: Number(amount),
|
|
235
|
+
currency,
|
|
236
|
+
payTo,
|
|
237
|
+
merchantId,
|
|
238
|
+
facilitatorUrl, // Contextual
|
|
239
|
+
chainId: Number(chainId),
|
|
240
|
+
route,
|
|
241
|
+
nonce,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// ---------------- RULES ENGINE ----------------
|
|
245
|
+
emergencyStop() {
|
|
246
|
+
this.stopped = true;
|
|
247
|
+
console.warn("[WALLET] Emergency stop activated!");
|
|
248
|
+
}
|
|
249
|
+
shouldPay(request, context, originalChallenge) {
|
|
250
|
+
// -1. Emergency Stop
|
|
251
|
+
if (this.stopped) {
|
|
252
|
+
return { allow: false, reason: "Emergency stop active" };
|
|
253
|
+
}
|
|
254
|
+
// [SECURITY] Spoofing Check
|
|
255
|
+
// Ensure the route we are paying for matches the invalid route (if known)
|
|
256
|
+
if (originalChallenge?.route && request.route !== originalChallenge.route) {
|
|
257
|
+
return { allow: false, reason: `Route mismatch: Challenge=${request.route}, Expected=${originalChallenge.route}` };
|
|
258
|
+
}
|
|
259
|
+
// 0. Currency check [NEW]
|
|
260
|
+
if (request.currency !== "USDC") {
|
|
261
|
+
return { allow: false, reason: `Unsupported currency: ${request.currency}` };
|
|
262
|
+
}
|
|
263
|
+
// 1. Chain verification
|
|
264
|
+
if (request.chainId !== context.chainId) {
|
|
265
|
+
return { allow: false, reason: "ChainId mismatch" };
|
|
266
|
+
}
|
|
267
|
+
// 2. Facilitator trust
|
|
268
|
+
const origin = this.canonicalOrigin(request.facilitatorUrl);
|
|
269
|
+
if (!this.trustedFacilitatorOrigins.has(origin)) {
|
|
270
|
+
return { allow: false, reason: "Untrusted facilitator" };
|
|
271
|
+
}
|
|
272
|
+
// 3. Merchant allowlist
|
|
273
|
+
// Enabled for safety test
|
|
274
|
+
if (this.allowedMerchants.size > 0 && !this.allowedMerchants.has(request.merchantId)) {
|
|
275
|
+
return { allow: false, reason: `Merchant not allowlisted: ${request.merchantId}` };
|
|
276
|
+
}
|
|
277
|
+
// 4. Daily spend limit
|
|
278
|
+
// Floating point fix: Use a small epsilon or round
|
|
279
|
+
if ((this.spentToday + request.amount) > (this.dailyLimit + 0.0001)) {
|
|
280
|
+
return { allow: false, reason: `Daily limit exceeded (${this.spentToday.toFixed(2)} + ${request.amount} > ${this.dailyLimit})` };
|
|
281
|
+
}
|
|
282
|
+
// 5. Max Per Transaction Check
|
|
283
|
+
if (request.amount > (this.maxPerTransaction + 0.0001)) {
|
|
284
|
+
return { allow: false, reason: `Transaction limit exceeded (${request.amount} > ${this.maxPerTransaction})` };
|
|
285
|
+
}
|
|
286
|
+
return { allow: true };
|
|
287
|
+
}
|
|
288
|
+
// ---------------- PAYMENT EXECUTION ----------------
|
|
289
|
+
async executePayment(request) {
|
|
290
|
+
const key = this.paymentKey(request);
|
|
291
|
+
if (this.paidRequests.has(key)) {
|
|
292
|
+
throw new Error("Replay detected: payment already executed");
|
|
293
|
+
}
|
|
294
|
+
// Execute chain tx
|
|
295
|
+
const proof = await this.executor.execute(request);
|
|
296
|
+
// Update state
|
|
297
|
+
this.spentToday += request.amount;
|
|
298
|
+
this.paidRequests.set(key, Date.now());
|
|
299
|
+
// Persist immediately
|
|
300
|
+
this.saveState();
|
|
301
|
+
return proof;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
exports.AgentWallet = AgentWallet;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { PaymentExecutor } from "./executors";
|
|
3
|
+
import { PaymentRequest } from "./types";
|
|
4
|
+
export declare class CronosUsdcExecutor implements PaymentExecutor {
|
|
5
|
+
private provider;
|
|
6
|
+
private wallet;
|
|
7
|
+
private usdc;
|
|
8
|
+
private chainId;
|
|
9
|
+
constructor(rpcUrl: string, privateKey: string, usdcAddress: string, expectedChainId?: number);
|
|
10
|
+
private verifyChain;
|
|
11
|
+
private withRetry;
|
|
12
|
+
execute(request: PaymentRequest): Promise<string>;
|
|
13
|
+
getAddress(): string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CronosUsdcExecutor = void 0;
|
|
4
|
+
require("dotenv/config");
|
|
5
|
+
const ethers_1 = require("ethers");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const ERC20_ABI = [
|
|
8
|
+
"function transfer(address to, uint256 amount) returns (bool)",
|
|
9
|
+
"function balanceOf(address owner) view returns (uint256)",
|
|
10
|
+
];
|
|
11
|
+
const CONFIRMATIONS = 1; // safe for Cronos testnet
|
|
12
|
+
const TX_TIMEOUT_MS = 60_000; // 60s timeout
|
|
13
|
+
class CronosUsdcExecutor {
|
|
14
|
+
provider;
|
|
15
|
+
wallet;
|
|
16
|
+
usdc;
|
|
17
|
+
chainId;
|
|
18
|
+
constructor(rpcUrl, privateKey, usdcAddress, expectedChainId) {
|
|
19
|
+
this.provider = new ethers_1.ethers.JsonRpcProvider(rpcUrl);
|
|
20
|
+
const signingKey = new ethers_1.ethers.SigningKey(privateKey);
|
|
21
|
+
this.wallet = new ethers_1.ethers.Wallet(signingKey, this.provider);
|
|
22
|
+
this.usdc = new ethers_1.ethers.Contract(usdcAddress, ERC20_ABI, this.wallet);
|
|
23
|
+
// Optional but recommended
|
|
24
|
+
if (expectedChainId) {
|
|
25
|
+
this.verifyChain(expectedChainId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// ---------------- SAFETY ----------------
|
|
29
|
+
async verifyChain(expectedChainId) {
|
|
30
|
+
const network = await this.provider.getNetwork();
|
|
31
|
+
this.chainId = Number(network.chainId);
|
|
32
|
+
if (this.chainId !== expectedChainId) {
|
|
33
|
+
throw new Error(`ChainId mismatch: expected ${expectedChainId}, got ${this.chainId} `);
|
|
34
|
+
}
|
|
35
|
+
console.log(`[CRONOS] Connected to chainId ${this.chainId} `);
|
|
36
|
+
}
|
|
37
|
+
// ---------------- EXECUTION ----------------
|
|
38
|
+
async withRetry(fn, retries = 3) {
|
|
39
|
+
let lastError;
|
|
40
|
+
for (let i = 0; i < retries; i++) {
|
|
41
|
+
try {
|
|
42
|
+
return await fn();
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
lastError = e;
|
|
46
|
+
// Don't retry revert errors
|
|
47
|
+
if (e.code === 'CALL_EXCEPTION' || e.code === 'INSUFFICIENT_FUNDS')
|
|
48
|
+
throw e;
|
|
49
|
+
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw lastError;
|
|
53
|
+
}
|
|
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
|
+
// 0. Connection Check
|
|
62
|
+
if (!this.chainId)
|
|
63
|
+
await this.verifyChain(Number(request.chainId));
|
|
64
|
+
// 1. Balance check (Retryable)
|
|
65
|
+
// Note: balanceOf returns bigint in v6
|
|
66
|
+
const balance = await this.withRetry(() => this.usdc.balanceOf(this.wallet.address));
|
|
67
|
+
if (balance < amount) {
|
|
68
|
+
throw new Error(`Insufficient USDC balance: have ${ethers_1.ethers.formatUnits(balance, 6)}, need ${request.amount} `);
|
|
69
|
+
}
|
|
70
|
+
// ... inside class ...
|
|
71
|
+
// 1.5 Gas Check (CRO Balance)
|
|
72
|
+
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
|
|
74
|
+
if (croBalance < MIN_GAS) {
|
|
75
|
+
throw new errors_1.AgentError(`Insufficient CRO for gas: have ${ethers_1.ethers.formatEther(croBalance)}, need > 0.1`, "INSUFFICIENT_FUNDS");
|
|
76
|
+
}
|
|
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
|
|
86
|
+
const tx = await this.withRetry(() => this.usdc.transfer(request.payTo, amount));
|
|
87
|
+
console.log(`[CRONOS] Tx sent: ${tx.hash}. Waiting for confirmation...`);
|
|
88
|
+
// 4. Wait with timeout + confirmations
|
|
89
|
+
const receipt = await Promise.race([
|
|
90
|
+
this.withRetry(() => tx.wait(CONFIRMATIONS)),
|
|
91
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Transaction confirmation timeout")), TX_TIMEOUT_MS)),
|
|
92
|
+
]);
|
|
93
|
+
if (!receipt || receipt.status !== 1) {
|
|
94
|
+
throw new Error("USDC transfer failed (Reverted)");
|
|
95
|
+
}
|
|
96
|
+
console.log(`[CRONOS] Payment confirmed: ${receipt.hash} `);
|
|
97
|
+
return receipt.hash;
|
|
98
|
+
}
|
|
99
|
+
getAddress() {
|
|
100
|
+
return this.wallet.address;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.CronosUsdcExecutor = CronosUsdcExecutor;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import { YieldExecutor } from "./YieldExecutor";
|
|
3
|
+
export declare class VvsYieldExecutor implements YieldExecutor {
|
|
4
|
+
private readonly provider;
|
|
5
|
+
private readonly wallet;
|
|
6
|
+
private vault;
|
|
7
|
+
constructor(provider: ethers.JsonRpcProvider, wallet: ethers.Wallet, vaultAddress?: string);
|
|
8
|
+
/**
|
|
9
|
+
* READ-ONLY SENSOR (Phase 5A)
|
|
10
|
+
* All values are RAW bigint
|
|
11
|
+
*/
|
|
12
|
+
getVaultPosition(): Promise<{
|
|
13
|
+
shares: bigint;
|
|
14
|
+
pricePerShare: bigint;
|
|
15
|
+
underlyingValue: bigint;
|
|
16
|
+
}>;
|
|
17
|
+
harvest(): Promise<number>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VvsYieldExecutor = void 0;
|
|
4
|
+
// internal/VvsYieldExecutor.ts
|
|
5
|
+
const ethers_1 = require("ethers");
|
|
6
|
+
// Minimal AutoVVS vault ABI (verified)
|
|
7
|
+
const AUTO_VVS_VAULT_ABI = [
|
|
8
|
+
"function userInfo(address) view returns (uint256 shares, uint256 lastDepositedTime, uint256 cakeAtLastUserAction, uint256 lastUserActionTime)",
|
|
9
|
+
"function getPricePerFullShare() view returns (uint256)"
|
|
10
|
+
];
|
|
11
|
+
// Cronos Mainnet AutoVVS Vault
|
|
12
|
+
const AUTO_VVS_VAULT = "0xA6fF77fC8E839679D4F7408E8988B564dE1A2dcD";
|
|
13
|
+
class VvsYieldExecutor {
|
|
14
|
+
provider;
|
|
15
|
+
wallet;
|
|
16
|
+
vault;
|
|
17
|
+
constructor(provider, wallet, vaultAddress = AUTO_VVS_VAULT) {
|
|
18
|
+
this.provider = provider;
|
|
19
|
+
this.wallet = wallet;
|
|
20
|
+
this.vault = new ethers_1.ethers.Contract(vaultAddress, AUTO_VVS_VAULT_ABI, this.provider);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* READ-ONLY SENSOR (Phase 5A)
|
|
24
|
+
* All values are RAW bigint
|
|
25
|
+
*/
|
|
26
|
+
async getVaultPosition() {
|
|
27
|
+
const [userInfo, pricePerShare] = await Promise.all([
|
|
28
|
+
this.vault.userInfo(this.wallet.address),
|
|
29
|
+
this.vault.getPricePerFullShare()
|
|
30
|
+
]);
|
|
31
|
+
const shares = userInfo[0];
|
|
32
|
+
/**
|
|
33
|
+
* Convention:
|
|
34
|
+
* pricePerShare is scaled by 1e18
|
|
35
|
+
*/
|
|
36
|
+
const underlyingValue = (shares * pricePerShare) / 10n ** 18n;
|
|
37
|
+
return {
|
|
38
|
+
shares,
|
|
39
|
+
pricePerShare,
|
|
40
|
+
underlyingValue
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// 🚫 Phase 5A boundary
|
|
44
|
+
async harvest() {
|
|
45
|
+
throw new Error("harvest() not implemented (AutoVVS Phase 5A)");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.VvsYieldExecutor = VvsYieldExecutor;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MockPaymentExecutor = void 0;
|
|
4
|
+
// Phase 3A — Mock executor
|
|
5
|
+
class MockPaymentExecutor {
|
|
6
|
+
async execute(request) {
|
|
7
|
+
console.log(`[EXECUTOR] Paying ${request.amount} ${request.currency} to ${request.payTo}`);
|
|
8
|
+
return `mock_tx_${Date.now()}`;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.MockPaymentExecutor = MockPaymentExecutor;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface PaymentRequest {
|
|
2
|
+
amount: number;
|
|
3
|
+
currency: string;
|
|
4
|
+
payTo: string;
|
|
5
|
+
merchantId: string;
|
|
6
|
+
facilitatorUrl: string;
|
|
7
|
+
chainId: number;
|
|
8
|
+
route: string;
|
|
9
|
+
nonce: string;
|
|
10
|
+
}
|
|
11
|
+
export interface WalletContext {
|
|
12
|
+
chainId: number;
|
|
13
|
+
merchantId?: string;
|
|
14
|
+
analyticsUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface AgentWalletConfig {
|
|
17
|
+
dailyLimit?: number;
|
|
18
|
+
maxPerTransaction?: number;
|
|
19
|
+
trustedFacilitators?: string[];
|
|
20
|
+
allowedMerchants?: string[];
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lastResetDate": "2026-01-12",
|
|
3
|
+
"spentToday": 0.2,
|
|
4
|
+
"paidRequests": [
|
|
5
|
+
[
|
|
6
|
+
"a644d13b-b537-4ad5-bc5a-65705fd3ccec:POST /api/premium:rnvj9m",
|
|
7
|
+
1768211109096
|
|
8
|
+
],
|
|
9
|
+
[
|
|
10
|
+
"a644d13b-b537-4ad5-bc5a-65705fd3ccec:POST /api/premium:mnuc9o",
|
|
11
|
+
1768211999104
|
|
12
|
+
]
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AgentWallet } from "./AgentWallet";
|
|
2
|
+
import { WalletContext } from "./types";
|
|
3
|
+
export declare function x402Request(url: string, wallet: AgentWallet, context: WalletContext, options?: {
|
|
4
|
+
method?: "GET" | "POST";
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
body?: any;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
allowBodyFallback?: boolean;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
data: any;
|
|
11
|
+
payment: null;
|
|
12
|
+
} | {
|
|
13
|
+
data: any;
|
|
14
|
+
payment: {
|
|
15
|
+
txHash: string;
|
|
16
|
+
amount: number;
|
|
17
|
+
currency: string;
|
|
18
|
+
chainId: number;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.x402Request = x402Request;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const errors_1 = require("../errors");
|
|
9
|
+
const HTTP_TIMEOUT_MS = 180_000;
|
|
10
|
+
// Helper for fire-and-forget logging
|
|
11
|
+
async function logDecision(context, agentAddress, req, decision, reason, txHash) {
|
|
12
|
+
if (!context.analyticsUrl)
|
|
13
|
+
return;
|
|
14
|
+
// Non-blocking catch
|
|
15
|
+
axios_1.default.post(`${context.analyticsUrl}/api/analytics/log`, {
|
|
16
|
+
agentAddress,
|
|
17
|
+
url: req.facilitatorUrl,
|
|
18
|
+
merchantId: req.merchantId,
|
|
19
|
+
amount: req.amount,
|
|
20
|
+
currency: req.currency,
|
|
21
|
+
decision,
|
|
22
|
+
reason,
|
|
23
|
+
txHash,
|
|
24
|
+
chainId: context.chainId
|
|
25
|
+
}).catch(err => {
|
|
26
|
+
// Silent fail for analytics to not break flow
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function x402Request(url, wallet, context, options = {}) {
|
|
30
|
+
let paymentAttempted = false; // 🛡️ CRITICAL: Prevents the "Drain Loop"
|
|
31
|
+
// 0. Version Check
|
|
32
|
+
console.log("[SDK] v1.0.4 - ROBUST MODE");
|
|
33
|
+
// Normalize inputs to support both nested headers and flat structure
|
|
34
|
+
const { method, body, headers, ...rest } = options;
|
|
35
|
+
// MERGE both sources: flat properties (rest) and nested headers
|
|
36
|
+
const effectiveHeaders = { ...rest, ...(headers || {}) };
|
|
37
|
+
try {
|
|
38
|
+
// 1. Initial attempt to fetch data
|
|
39
|
+
const res = await (0, axios_1.default)({
|
|
40
|
+
url,
|
|
41
|
+
method: method ?? "GET",
|
|
42
|
+
data: body,
|
|
43
|
+
headers: {
|
|
44
|
+
...effectiveHeaders,
|
|
45
|
+
"x-merchant-id": context.merchantId || "",
|
|
46
|
+
"x-chain-id": context.chainId.toString(),
|
|
47
|
+
},
|
|
48
|
+
timeout: options.timeoutMs ?? HTTP_TIMEOUT_MS,
|
|
49
|
+
validateStatus: (status) => status === 200 || status === 402,
|
|
50
|
+
});
|
|
51
|
+
if (res.status === 200) {
|
|
52
|
+
return { data: res.data, payment: null };
|
|
53
|
+
}
|
|
54
|
+
// 2. 402 Flow
|
|
55
|
+
console.log("[x402] Payment Required Challenge received");
|
|
56
|
+
console.log("[SDK] Response Headers:", JSON.stringify(res.headers, null, 2));
|
|
57
|
+
console.log("[SDK] Response Body:", JSON.stringify(res.data, null, 2));
|
|
58
|
+
if (paymentAttempted) {
|
|
59
|
+
// If the server asks for pay again immediately after we just paid, something is wrong.
|
|
60
|
+
throw new Error("Recursive 402 detected: Server refused proof or loop occurred.");
|
|
61
|
+
}
|
|
62
|
+
let paymentRequest;
|
|
63
|
+
try {
|
|
64
|
+
paymentRequest = wallet.parse402Header(res.headers);
|
|
65
|
+
}
|
|
66
|
+
catch (headerErr) {
|
|
67
|
+
// [SECURITY] Explicit Opt-In for Body Fallback
|
|
68
|
+
if (!options.allowBodyFallback) {
|
|
69
|
+
throw new Error("402 Header Parsing Failed and allowBodyFallback is disabled. Server must provide X-Payment headers.");
|
|
70
|
+
}
|
|
71
|
+
console.warn("[SDK] parsing headers failed, trying body...", headerErr);
|
|
72
|
+
try {
|
|
73
|
+
paymentRequest = wallet.parse402Body(res.data, context.merchantId || "unknown", url);
|
|
74
|
+
}
|
|
75
|
+
catch (bodyErr) {
|
|
76
|
+
throw new Error(`Failed to parse 402 challenge from Headers OR Body. Header error: ${headerErr}. Body error: ${bodyErr}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const decision = wallet.shouldPay(paymentRequest, context, {
|
|
80
|
+
route: wallet.parse402Header(res.headers).route // Validate against what the server claimed in headers
|
|
81
|
+
});
|
|
82
|
+
const agentAddress = wallet.getAddress();
|
|
83
|
+
if (!decision.allow) {
|
|
84
|
+
logDecision(context, agentAddress, paymentRequest, "BLOCKED", decision.reason);
|
|
85
|
+
throw new Error(`Agent Policy Rejection: ${decision.reason}`);
|
|
86
|
+
}
|
|
87
|
+
// Mark as attempted BEFORE the async call
|
|
88
|
+
paymentAttempted = true;
|
|
89
|
+
// 3. Execute Payment (Authorized)
|
|
90
|
+
console.log(`[x402] Payment Approved. Executing on Chain ${context.chainId}...`);
|
|
91
|
+
let txHash;
|
|
92
|
+
try {
|
|
93
|
+
txHash = await wallet.executePayment(paymentRequest);
|
|
94
|
+
}
|
|
95
|
+
catch (execErr) {
|
|
96
|
+
logDecision(context, agentAddress, paymentRequest, "BLOCKED", `Execution Failed: ${execErr.message}`);
|
|
97
|
+
throw execErr;
|
|
98
|
+
}
|
|
99
|
+
logDecision(context, agentAddress, paymentRequest, "APPROVED", "Paid", txHash);
|
|
100
|
+
// 4. Retry Request with Payment Proof
|
|
101
|
+
console.log(`[x402] Payment Successful (${txHash}). Retrying request...`);
|
|
102
|
+
// [SECURITY] Protocol Alignment
|
|
103
|
+
// Must send: x-payment-proof (txHash) AND x-nonce (to verify binding)
|
|
104
|
+
const retryHeaders = {
|
|
105
|
+
...effectiveHeaders,
|
|
106
|
+
"X-Merchant-ID": context.merchantId || "",
|
|
107
|
+
"X-Chain-ID": context.chainId.toString(),
|
|
108
|
+
"X-Payment-Proof": txHash, // Canonical
|
|
109
|
+
"X-Nonce": paymentRequest.nonce // Canonical
|
|
110
|
+
};
|
|
111
|
+
const retryRes = await (0, axios_1.default)({
|
|
112
|
+
url,
|
|
113
|
+
method: method ?? "GET",
|
|
114
|
+
data: {
|
|
115
|
+
...(body || {}), // Preserve original body
|
|
116
|
+
nonce: paymentRequest.nonce // [CRITICAL] Send nonce in body for ReplayKey
|
|
117
|
+
},
|
|
118
|
+
headers: retryHeaders,
|
|
119
|
+
timeout: options.timeoutMs ?? HTTP_TIMEOUT_MS,
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
data: retryRes.data,
|
|
123
|
+
payment: {
|
|
124
|
+
txHash,
|
|
125
|
+
amount: paymentRequest.amount,
|
|
126
|
+
currency: paymentRequest.currency,
|
|
127
|
+
chainId: paymentRequest.chainId // Normalized property name
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// Enhanced error logging
|
|
133
|
+
if (error.response?.status === 402) {
|
|
134
|
+
// This happens if the proof was rejected (REPLAY_DETECTED, etc)
|
|
135
|
+
console.error("[SDK] Payment Proof Rejected by Server:", error.response.data);
|
|
136
|
+
throw new Error(`Use Access Denied: ${error.response.data.error || "UNKNOWN"} - ${error.response.data.message || "Proof rejected"}`);
|
|
137
|
+
}
|
|
138
|
+
// Continue to standard error handling
|
|
139
|
+
if (error.name === "AgentError")
|
|
140
|
+
throw error; // Already wrapped
|
|
141
|
+
const status = error.response?.status;
|
|
142
|
+
const errorData = error.response?.data || error.message;
|
|
143
|
+
// Log minimal info, let the caller handle the details
|
|
144
|
+
console.warn(`[x402] Request Failed: ${status || "Network"} - ${error.message}`);
|
|
145
|
+
throw new errors_1.AgentError(`Request failed with status code ${status || "unknown"}: ${JSON.stringify(errorData)}`, status ? "HTTP_ERROR" : "NETWORK_ERROR", status, errorData);
|
|
146
|
+
}
|
|
147
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cronos-agent-wallet",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"axios": "^1.13.2",
|
|
15
|
+
"dotenv": "^17.2.3",
|
|
16
|
+
"ethers": "^6.0.0",
|
|
17
|
+
"mongoose": "^9.1.1"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"ethers": "^6.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/mocha": "^10.0.10",
|
|
24
|
+
"@types/node": "^25.0.6",
|
|
25
|
+
"@types/uuid": "^10.0.0",
|
|
26
|
+
"ts-node": "^10.9.2",
|
|
27
|
+
"typescript": "^5.5.3"
|
|
28
|
+
}
|
|
29
|
+
}
|