@darksol/terminal 0.6.1 → 0.6.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/package.json +1 -1
- package/skill/SKILL.md +273 -94
- package/src/services/casino.js +76 -3
- package/src/services/oracle.js +75 -120
- package/src/utils/x402.js +232 -0
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,177 +1,356 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: darksol-terminal
|
|
3
|
-
description: "DARKSOL Terminal — unified CLI for trading, wallets,
|
|
3
|
+
description: "DARKSOL Terminal — unified CLI + x402 platform for trading, wallets, AI-powered market analysis, on-chain casino (4 games), random oracle (x402 auto-pay), prepaid crypto cards, secure agent signing, execution scripts, and all DARKSOL services. Use when: (1) swapping/sniping tokens on Base/Ethereum/L2s, (2) managing encrypted wallets, (3) running automated trading scripts, (4) DCA strategies, (5) market intel lookups, (6) playing on-chain casino games (coinflip, dice, hilo, slots), (7) using the random oracle (x402-gated, auto-pays via agent signer), (8) ordering prepaid Visa/Mastercard cards with crypto, (9) natural language trading via LLM, (10) signing transactions securely for x402/contracts without exposing private keys."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# DARKSOL Terminal
|
|
6
|
+
# DARKSOL Terminal — OpenClaw Agent Skill
|
|
7
7
|
|
|
8
8
|
**All DARKSOL services. One terminal. Zero trust required. 🌑**
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
`@darksol/terminal` v0.6.x | npm: `npm install -g @darksol/terminal`
|
|
11
11
|
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## For OpenClaw Agents — How To Use This
|
|
15
|
+
|
|
16
|
+
You're an AI agent. This skill gives you a full crypto terminal. Here's what matters:
|
|
17
|
+
|
|
18
|
+
### Setup (one-time)
|
|
12
19
|
```bash
|
|
13
20
|
npm install -g @darksol/terminal
|
|
21
|
+
darksol setup # Interactive first-run wizard
|
|
22
|
+
darksol config set output json # JSON output for programmatic parsing
|
|
14
23
|
```
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
### Agent Signer (REQUIRED for trading + x402)
|
|
26
|
+
The agent signer is your secure wallet interface. It runs as a local HTTP server and signs transactions without ever exposing the private key.
|
|
17
27
|
|
|
18
|
-
### Wallet Management
|
|
19
28
|
```bash
|
|
20
|
-
|
|
29
|
+
# Start the signer with a wallet
|
|
30
|
+
darksol signer start <wallet-name>
|
|
31
|
+
darksol signer start <wallet-name> --max-value 0.5 --daily-limit 2.0
|
|
32
|
+
|
|
33
|
+
# Or set env vars for non-interactive use
|
|
34
|
+
export DARKSOL_WALLET_PASSWORD=<password>
|
|
35
|
+
export DARKSOL_SIGNER_TOKEN=<token> # Set after first start, reuse for API calls
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Signer API (127.0.0.1:18790):**
|
|
39
|
+
| Endpoint | Method | What |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `/health` | GET | Check signer status |
|
|
42
|
+
| `/address` | GET | Get wallet address |
|
|
43
|
+
| `/balance` | GET | ETH balance |
|
|
44
|
+
| `/send` | POST | Sign + broadcast transaction |
|
|
45
|
+
| `/sign-message` | POST | Sign EIP-191 message |
|
|
46
|
+
| `/sign-typed-data` | POST | Sign EIP-712 typed data (x402) |
|
|
47
|
+
| `/policy` | GET | Spending limits + daily remaining |
|
|
48
|
+
| `/audit` | GET | Last 50 operations log |
|
|
49
|
+
|
|
50
|
+
**Security:** PK never leaves the signer process. Bearer token auth. Blocked selectors (transferOwnership, selfdestruct). Spending limits enforced. Full audit log.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Complete Command Reference
|
|
55
|
+
|
|
56
|
+
### 💰 Wallet Management
|
|
57
|
+
```bash
|
|
58
|
+
darksol wallet create <name> # Create new wallet (AES-256-GCM + scrypt)
|
|
21
59
|
darksol wallet import <name> # Import from private key
|
|
22
60
|
darksol wallet list # List all wallets
|
|
23
|
-
darksol wallet balance [name] #
|
|
61
|
+
darksol wallet balance [name] # ETH + USDC balance
|
|
24
62
|
darksol wallet use <name> # Set active wallet
|
|
25
|
-
darksol wallet export [name] # Export
|
|
63
|
+
darksol wallet export [name] # Export (password required for PK)
|
|
26
64
|
```
|
|
27
65
|
|
|
28
|
-
### Trading
|
|
66
|
+
### 📊 Trading (5 chains)
|
|
29
67
|
```bash
|
|
30
|
-
darksol trade swap -i ETH -o USDC -a 0.1 #
|
|
68
|
+
darksol trade swap -i ETH -o USDC -a 0.1 # Uniswap V3 swap with slippage protection
|
|
69
|
+
darksol trade swap -i USDC -o ETH -a 100 -c polygon # Swap on Polygon
|
|
31
70
|
darksol trade snipe <token> -a 0.05 # Fast buy with gas boost
|
|
32
71
|
darksol trade snipe <token> -a 0.05 -g 2.0 # Snipe with 2x gas priority
|
|
33
|
-
darksol trade watch # Monitor new pairs
|
|
72
|
+
darksol trade watch # Monitor new pairs (experimental)
|
|
73
|
+
darksol send # Interactive ETH/ERC-20 transfer
|
|
74
|
+
darksol receive # Show your address for receiving
|
|
34
75
|
```
|
|
35
76
|
|
|
36
|
-
|
|
77
|
+
**Supported chains:** Base (default), Ethereum, Polygon, Arbitrum, Optimism
|
|
78
|
+
**Swap routers:** Base uses SwapRouter02 (V2), others use V1 SwapRouter. Handled automatically.
|
|
79
|
+
|
|
80
|
+
### 📈 DCA (Dollar-Cost Averaging)
|
|
37
81
|
```bash
|
|
38
|
-
darksol dca create # Interactive DCA
|
|
39
|
-
darksol dca list #
|
|
82
|
+
darksol dca create # Interactive DCA setup
|
|
83
|
+
darksol dca list # Active orders
|
|
40
84
|
darksol dca run # Execute pending orders
|
|
41
|
-
darksol dca cancel <id> # Cancel
|
|
85
|
+
darksol dca cancel <id> # Cancel
|
|
42
86
|
```
|
|
43
87
|
|
|
44
|
-
### AI Trading Assistant
|
|
88
|
+
### 🤖 AI Trading Assistant
|
|
45
89
|
```bash
|
|
46
|
-
darksol ai chat # Interactive AI
|
|
47
|
-
darksol ai ask "buy 0.5 ETH of AERO" # Parse natural language trade intent
|
|
90
|
+
darksol ai chat # Interactive AI chat (supports swap/send/price/casino/cards)
|
|
91
|
+
darksol ai ask "buy 0.5 ETH of AERO" # Parse natural language → trade intent
|
|
92
|
+
darksol ai ask "flip a coin" -x # Auto-execute if confidence ≥ 60%
|
|
48
93
|
darksol ai strategy VIRTUAL -b 500 # DCA strategy recommendation
|
|
49
|
-
darksol ai analyze AERO #
|
|
94
|
+
darksol ai analyze AERO # Token analysis
|
|
50
95
|
```
|
|
51
96
|
|
|
52
|
-
|
|
97
|
+
**AI Intent Actions:** swap, send, snipe, dca, price, balance, info, analyze, gas, cards, casino, unknown
|
|
98
|
+
|
|
99
|
+
The AI understands natural language and maps it to executable commands:
|
|
100
|
+
- "swap 100 USDC to ETH" → `darksol trade swap -i USDC -o ETH -a 100`
|
|
101
|
+
- "bet on heads" → `darksol casino bet coinflip -c heads`
|
|
102
|
+
- "order a $50 prepaid card" → `darksol cards order -a 50`
|
|
103
|
+
- "what's the price of AERO" → `darksol market token AERO`
|
|
104
|
+
|
|
105
|
+
### 📝 Execution Scripts
|
|
53
106
|
```bash
|
|
54
|
-
darksol script templates #
|
|
55
|
-
darksol script create #
|
|
56
|
-
darksol script list #
|
|
57
|
-
darksol script run <name> # Execute (
|
|
58
|
-
darksol script run <name> -p "pw" -y # Non-interactive (for automation
|
|
107
|
+
darksol script templates # 7 templates: buy, sell, limit-buy, stop-loss, multi-buy, transfer, empty
|
|
108
|
+
darksol script create # Interactive template builder
|
|
109
|
+
darksol script list # Saved scripts
|
|
110
|
+
darksol script run <name> # Execute (password required)
|
|
111
|
+
darksol script run <name> -p "pw" -y # Non-interactive (for cron/automation)
|
|
59
112
|
darksol script show <name> # View code + params
|
|
60
|
-
darksol script edit <name> # Edit
|
|
61
|
-
darksol script clone <name> <new> # Clone
|
|
62
|
-
darksol script delete <name> # Delete
|
|
113
|
+
darksol script edit <name> # Edit
|
|
114
|
+
darksol script clone <name> <new> # Clone
|
|
115
|
+
darksol script delete <name> # Delete
|
|
63
116
|
```
|
|
64
117
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
### Market Intel
|
|
118
|
+
### 📈 Market Intel
|
|
68
119
|
```bash
|
|
69
|
-
darksol market top # Top movers on
|
|
70
|
-
darksol market top -c ethereum # Top movers on
|
|
71
|
-
darksol market token VIRTUAL # Full token detail
|
|
120
|
+
darksol market top # Top movers on active chain
|
|
121
|
+
darksol market top -c ethereum # Top movers on specific chain
|
|
122
|
+
darksol market token VIRTUAL # Full token detail (price, volume, liquidity, chain, DEX)
|
|
72
123
|
darksol market compare ETH AERO VIRTUAL # Side-by-side comparison
|
|
124
|
+
darksol price ETH AERO USDC # Quick multi-token price check
|
|
125
|
+
darksol watch ETH # Live streaming price updates
|
|
73
126
|
```
|
|
74
127
|
|
|
75
|
-
###
|
|
128
|
+
### 🎰 Casino (The Clawsino)
|
|
129
|
+
All bets are $1 USDC. House edge: 5%. Results verified on-chain.
|
|
130
|
+
|
|
76
131
|
```bash
|
|
77
|
-
darksol
|
|
78
|
-
darksol
|
|
79
|
-
darksol
|
|
132
|
+
darksol casino status # House stats, balance, game list
|
|
133
|
+
darksol casino bet # Interactive (picks game → params → wallet → confirm)
|
|
134
|
+
darksol casino bet coinflip -c heads # Coin flip — 1.90x payout
|
|
135
|
+
darksol casino bet dice -d over -t 3 # Dice over 3 — variable payout
|
|
136
|
+
darksol casino bet hilo -c higher # Hi-Lo — ~2.06x payout
|
|
137
|
+
darksol casino bet slots # Slots — 1.50-5.00x payout
|
|
138
|
+
darksol casino tables # Recent bets
|
|
139
|
+
darksol casino receipt <id> # Bet receipt
|
|
140
|
+
darksol casino verify <id> # On-chain verification (Basescan links)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**API:** `POST https://casino.darksol.net/api/bet`
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"gameType": "coinflip",
|
|
147
|
+
"betParams": { "choice": "heads" },
|
|
148
|
+
"agentWallet": "0x..."
|
|
149
|
+
}
|
|
80
150
|
```
|
|
81
151
|
|
|
82
|
-
|
|
152
|
+
**Games:**
|
|
153
|
+
| Game | Params | Payout |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `coinflip` | `{ "choice": "heads"\|"tails" }` | 1.90x |
|
|
156
|
+
| `dice` | `{ "direction": "over"\|"under", "threshold": 2-5 }` | variable |
|
|
157
|
+
| `hilo` | `{ "choice": "higher"\|"lower" }` | ~2.06x |
|
|
158
|
+
| `slots` | `{}` | 1.50-5.00x |
|
|
83
159
|
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
- `GET /balance` — ETH balance
|
|
87
|
-
- `POST /send` — sign + broadcast transaction
|
|
88
|
-
- `POST /sign-message` — sign EIP-191 message (x402)
|
|
89
|
-
- `POST /sign-typed-data` — sign EIP-712 typed data (x402)
|
|
90
|
-
- `GET /policy` — spending limits
|
|
91
|
-
- `GET /audit` — operation log
|
|
160
|
+
### 🎲 Random Oracle (x402-gated)
|
|
161
|
+
On-chain verifiable randomness. Each call costs $0.05 USDC on Base via x402 protocol.
|
|
92
162
|
|
|
93
|
-
### Oracle
|
|
94
163
|
```bash
|
|
164
|
+
darksol oracle health # Status (free)
|
|
95
165
|
darksol oracle flip # Coin flip
|
|
96
166
|
darksol oracle dice 20 # Roll d20
|
|
97
167
|
darksol oracle number 1 100 # Random 1-100
|
|
98
168
|
darksol oracle shuffle a b c d # Shuffle list
|
|
99
169
|
```
|
|
100
170
|
|
|
101
|
-
|
|
171
|
+
**x402 Auto-Pay:** If the agent signer is running, oracle requests auto-pay via EIP-3009 (transferWithAuthorization). No manual payment needed.
|
|
172
|
+
|
|
173
|
+
**API:** `https://acp.darksol.net/api/oracle/`
|
|
174
|
+
- `GET /health` — free, returns status + contract address
|
|
175
|
+
- `GET /coin` — 402 → x402 payment → result
|
|
176
|
+
- `GET /dice?sides=N` — 402 → x402 payment → result
|
|
177
|
+
- `GET /number?min=N&max=M` — 402 → x402 payment → result
|
|
178
|
+
- `POST /shuffle` — 402 → x402 payment → result
|
|
179
|
+
|
|
180
|
+
### 💳 Prepaid Cards (Crypto → Visa/Mastercard)
|
|
102
181
|
```bash
|
|
103
|
-
darksol
|
|
104
|
-
darksol
|
|
105
|
-
darksol
|
|
106
|
-
darksol
|
|
182
|
+
darksol cards catalog # Available providers + amounts
|
|
183
|
+
darksol cards order # Interactive (prompts for everything)
|
|
184
|
+
darksol cards order -p swype -a 50 -e me@email.com -t usdc # Full flags
|
|
185
|
+
darksol cards status <tradeId> # Check order status
|
|
107
186
|
```
|
|
108
187
|
|
|
109
|
-
|
|
188
|
+
**Providers:** swype (Mastercard, Global), mpc (Mastercard, US), reward (Visa, US)
|
|
189
|
+
**Amounts:** $10, $25, $50, $100, $250, $500, $1000
|
|
190
|
+
**Crypto payments:** usdc/base, usdc/ERC20, usdt/trc20, btc/Mainnet, eth/ERC20, sol/Mainnet, xmr/Mainnet
|
|
191
|
+
|
|
192
|
+
Invalid inputs re-prompt instead of failing — fully guided flow.
|
|
193
|
+
|
|
194
|
+
### 🔗 x402 Facilitator
|
|
195
|
+
Free on-chain payment settlement. Zero fees — DARKSOL covers gas.
|
|
196
|
+
|
|
110
197
|
```bash
|
|
111
|
-
darksol
|
|
112
|
-
darksol
|
|
113
|
-
darksol
|
|
198
|
+
darksol facilitator health # Status, chains, contracts, settlement stats
|
|
199
|
+
darksol facilitator verify <payment> # Verify payment off-chain
|
|
200
|
+
darksol facilitator settle <payment> # Settle on-chain (free)
|
|
114
201
|
```
|
|
115
202
|
|
|
116
|
-
|
|
203
|
+
**Chains:** Base + Polygon
|
|
204
|
+
**API:** `https://facilitator.darksol.net/`
|
|
205
|
+
- `GET /` — service info + chain status
|
|
206
|
+
- `POST /verify` — verify payment
|
|
207
|
+
- `POST /settle` — settle on-chain
|
|
208
|
+
|
|
209
|
+
### 🏗️ Builder Index
|
|
117
210
|
```bash
|
|
118
211
|
darksol builders leaderboard # ERC-8021 builder rankings
|
|
119
212
|
darksol builders lookup <code> # Builder profile
|
|
120
213
|
darksol builders feed # Recent transactions
|
|
121
214
|
```
|
|
122
215
|
|
|
123
|
-
###
|
|
216
|
+
### 📧 AgentMail
|
|
124
217
|
```bash
|
|
125
|
-
darksol
|
|
126
|
-
darksol
|
|
127
|
-
darksol
|
|
218
|
+
darksol mail setup # Set up email inbox
|
|
219
|
+
darksol mail inbox # View messages
|
|
220
|
+
darksol mail send <to> -s "Subject" # Send email
|
|
221
|
+
darksol mail read <id> # Read message
|
|
222
|
+
darksol mail reply <id> # Reply
|
|
128
223
|
```
|
|
129
224
|
|
|
130
|
-
###
|
|
225
|
+
### ⛽ Gas & Network
|
|
131
226
|
```bash
|
|
132
|
-
darksol
|
|
133
|
-
darksol
|
|
134
|
-
darksol
|
|
135
|
-
darksol keys add alchemy # Add Alchemy RPC key
|
|
136
|
-
darksol keys remove <service> # Remove a key
|
|
227
|
+
darksol gas # Gas prices on active chain
|
|
228
|
+
darksol gas --all # Gas across all 5 chains
|
|
229
|
+
darksol networks # Chain reference table
|
|
137
230
|
```
|
|
138
231
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
### Configuration
|
|
232
|
+
### 🔧 Configuration
|
|
142
233
|
```bash
|
|
143
234
|
darksol config show # View all settings
|
|
144
235
|
darksol config set chain base # Set active chain
|
|
145
|
-
darksol config set slippage 1.0 #
|
|
146
|
-
darksol config rpc base https://... # Custom RPC
|
|
236
|
+
darksol config set slippage 1.0 # Slippage %
|
|
237
|
+
darksol config rpc base https://... # Custom RPC
|
|
147
238
|
```
|
|
148
239
|
|
|
149
|
-
###
|
|
240
|
+
### 🔑 API Keys (Encrypted Vault)
|
|
241
|
+
```bash
|
|
242
|
+
darksol keys list # All services + status
|
|
243
|
+
darksol keys add openai # Add key (encrypted AES-256-GCM)
|
|
244
|
+
darksol keys add anthropic # Supported: openai, anthropic, openrouter, ollama,
|
|
245
|
+
darksol keys add email # coingecko, dexscreener, alchemy, infura, email
|
|
246
|
+
darksol keys remove <service> # Remove key
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 🌐 Web Shell (GUI)
|
|
250
|
+
```bash
|
|
251
|
+
darksol serve # Launch web terminal at localhost:18791
|
|
252
|
+
darksol serve -p 3000 # Custom port
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Web shell includes: full CLI, AI chat, interactive menus, casino, cards ordering, wallet management, agent signer controls.
|
|
256
|
+
|
|
257
|
+
### 📚 Reference
|
|
150
258
|
```bash
|
|
151
259
|
darksol tips # Trading + scripting tips
|
|
152
|
-
darksol tips --trading # Trading tips only
|
|
153
|
-
darksol networks # Chain reference table
|
|
154
260
|
darksol quickstart # Getting started guide
|
|
155
|
-
darksol lookup 0x... #
|
|
261
|
+
darksol lookup 0x... # On-chain address lookup
|
|
262
|
+
darksol setup # Re-run setup wizard
|
|
156
263
|
```
|
|
157
264
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
-
|
|
163
|
-
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## x402 Payment Flow (for agents)
|
|
268
|
+
|
|
269
|
+
The terminal includes a built-in x402 client (`src/utils/x402.js`). When a service returns HTTP 402:
|
|
270
|
+
|
|
271
|
+
1. Parse `payment-required` header (base64 JSON)
|
|
272
|
+
2. Sign EIP-3009 `transferWithAuthorization` via agent signer
|
|
273
|
+
3. Retry request with `X-PAYMENT` header containing the signed authorization
|
|
274
|
+
4. Facilitator settles on-chain (free, DARKSOL covers gas)
|
|
275
|
+
|
|
276
|
+
**For agents:** Just start the signer and make requests. x402 auto-pay handles the rest.
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
import { fetchWithX402 } from '@darksol/terminal/src/utils/x402.js';
|
|
280
|
+
|
|
281
|
+
const result = await fetchWithX402(
|
|
282
|
+
'https://acp.darksol.net/api/oracle/coin',
|
|
283
|
+
{},
|
|
284
|
+
{ signerToken: process.env.DARKSOL_SIGNER_TOKEN }
|
|
285
|
+
);
|
|
286
|
+
// result.data = { result: "heads", proof: "0x..." }
|
|
287
|
+
// result.paid = true
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Agent Integration Patterns
|
|
293
|
+
|
|
294
|
+
### Non-interactive mode (for cron / automation)
|
|
295
|
+
```bash
|
|
296
|
+
# All trading commands accept flags for non-interactive use
|
|
297
|
+
darksol trade swap -i ETH -o USDC -a 0.1 -y
|
|
298
|
+
darksol script run my-dca -p "password" -y
|
|
299
|
+
darksol casino bet coinflip -c heads -w 0x1234...
|
|
300
|
+
|
|
301
|
+
# Set JSON output for parsing
|
|
302
|
+
darksol config set output json
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Environment variables
|
|
306
|
+
```bash
|
|
307
|
+
DARKSOL_WALLET_PASSWORD # Skip password prompts
|
|
308
|
+
DARKSOL_SIGNER_TOKEN # Reuse signer auth token
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Programmatic imports
|
|
312
|
+
```javascript
|
|
313
|
+
// Direct service access from Node.js
|
|
314
|
+
import { casinoBet, casinoHealth, GAMES } from '@darksol/terminal/src/services/casino.js';
|
|
315
|
+
import { oracleFlip, oracleDice } from '@darksol/terminal/src/services/oracle.js';
|
|
316
|
+
import { cardsCatalog, cardsOrder } from '@darksol/terminal/src/services/cards.js';
|
|
317
|
+
import { facilitatorHealth } from '@darksol/terminal/src/services/facilitator.js';
|
|
318
|
+
import { fetchWithX402 } from '@darksol/terminal/src/utils/x402.js';
|
|
319
|
+
import { parseIntent, executeIntent } from '@darksol/terminal/src/llm/intent.js';
|
|
320
|
+
import { topMovers, tokenDetail } from '@darksol/terminal/src/services/market.js';
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### OpenClaw cron example
|
|
324
|
+
```bash
|
|
325
|
+
# Run a DCA script every 4 hours
|
|
326
|
+
darksol script run eth-dca -p "$DARKSOL_WALLET_PASSWORD" -y
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Service Endpoints
|
|
332
|
+
|
|
333
|
+
| Service | URL | Auth |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| Casino | `https://casino.darksol.net/api/` | None (wallet for payouts) |
|
|
336
|
+
| Oracle | `https://acp.darksol.net/api/oracle/` | x402 ($0.05 USDC) |
|
|
337
|
+
| Cards | `https://acp.darksol.net/api/cards/` | None |
|
|
338
|
+
| Facilitator | `https://facilitator.darksol.net/` | None |
|
|
339
|
+
| Builders | `https://builders.darksol.net/` | None |
|
|
340
|
+
| Casino Docs | `https://casino.darksol.net/docs` | — |
|
|
341
|
+
| Oracle Docs | `https://acp.darksol.net/oracle` | — |
|
|
342
|
+
| Facilitator Docs | `https://acp.darksol.net/facilitator` | — |
|
|
343
|
+
|
|
344
|
+
---
|
|
164
345
|
|
|
165
|
-
##
|
|
346
|
+
## Security Model
|
|
166
347
|
|
|
167
|
-
-
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
348
|
+
- **Private keys:** AES-256-GCM + scrypt (N=2^18), never stored in plaintext
|
|
349
|
+
- **Agent signer:** PK-isolated HTTP proxy, bearer auth, loopback only (127.0.0.1)
|
|
350
|
+
- **Spending limits:** Per-tx max value + daily spend limit
|
|
351
|
+
- **Blocked selectors:** transferOwnership, selfdestruct, approve(max), setApprovalForAll
|
|
352
|
+
- **Audit log:** Every sign/send operation logged with timestamp + details
|
|
353
|
+
- **API key vault:** AES-256-GCM encrypted, machine-derived password
|
|
354
|
+
- **No PK endpoint:** Literally no code path returns the private key
|
|
172
355
|
|
|
173
|
-
|
|
174
|
-
- Private keys encrypted with AES-256-GCM + scrypt KDF
|
|
175
|
-
- Agent signer: PK never exposed, loopback-only, bearer auth, spending limits
|
|
176
|
-
- Dangerous contract calls (transferOwnership, selfdestruct) blocked by default
|
|
177
|
-
- Full audit logging on all signing operations
|
|
356
|
+
Built with teeth. 🌑
|
package/src/services/casino.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { fetchJSON } from '../utils/fetch.js';
|
|
2
2
|
import fetch from 'node-fetch';
|
|
3
|
-
import {
|
|
4
|
-
import { getConfig } from '../config/store.js';
|
|
3
|
+
import { ethers } from 'ethers';
|
|
4
|
+
import { getServiceURL, getConfig, getRPC } from '../config/store.js';
|
|
5
5
|
import { theme } from '../ui/theme.js';
|
|
6
6
|
import { spinner, kvDisplay, success, error, warn, info, table } from '../ui/components.js';
|
|
7
7
|
import { showSection } from '../ui/banner.js';
|
|
8
|
+
import { isSignerRunning } from '../utils/x402.js';
|
|
8
9
|
|
|
9
10
|
const getURL = () => getServiceURL('casino') || 'https://casino.darksol.net';
|
|
10
11
|
|
|
@@ -180,12 +181,81 @@ export async function casinoBet(gameType, betParams = {}, opts = {}) {
|
|
|
180
181
|
return;
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
// ── Payment: Send 1 USDC to the house ──
|
|
185
|
+
const HOUSE_WALLET = '0x7B0a6330121B26100D47BCcd5640cc6617F8adA7';
|
|
186
|
+
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
187
|
+
const USDC_AMOUNT = '1000000'; // 1 USDC (6 decimals)
|
|
188
|
+
|
|
189
|
+
const paymentSpin = spinner('Sending $1 USDC to the house...').start();
|
|
190
|
+
let paymentTxHash;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Try agent signer first
|
|
194
|
+
const signerToken = process.env.DARKSOL_SIGNER_TOKEN || getConfig('signerToken') || null;
|
|
195
|
+
const signerUp = await isSignerRunning(signerToken);
|
|
196
|
+
|
|
197
|
+
if (signerUp) {
|
|
198
|
+
// Use agent signer to send USDC
|
|
199
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
200
|
+
if (signerToken) headers.Authorization = `Bearer ${signerToken}`;
|
|
201
|
+
|
|
202
|
+
// ERC-20 transfer calldata: transfer(address,uint256)
|
|
203
|
+
const iface = new ethers.Interface(['function transfer(address to, uint256 amount) returns (bool)']);
|
|
204
|
+
const txData = iface.encodeFunctionData('transfer', [HOUSE_WALLET, USDC_AMOUNT]);
|
|
205
|
+
|
|
206
|
+
const resp = await fetch('http://127.0.0.1:18790/send', {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers,
|
|
209
|
+
body: JSON.stringify({ to: USDC_BASE, data: txData, value: '0' }),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (!resp.ok) {
|
|
213
|
+
const errText = await resp.text();
|
|
214
|
+
throw new Error(`Signer refused: ${errText}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const result = await resp.json();
|
|
218
|
+
paymentTxHash = result.txHash || result.hash;
|
|
219
|
+
} else {
|
|
220
|
+
// Try wallet directly (needs password)
|
|
221
|
+
const activeWallet = getConfig('activeWallet');
|
|
222
|
+
if (!activeWallet) throw new Error('No wallet configured. Set one: darksol wallet use <name>');
|
|
223
|
+
|
|
224
|
+
const { decryptKey } = await import('../wallet/keystore.js');
|
|
225
|
+
const password = process.env.DARKSOL_WALLET_PASSWORD;
|
|
226
|
+
if (!password) {
|
|
227
|
+
paymentSpin.fail('Payment requires agent signer or DARKSOL_WALLET_PASSWORD');
|
|
228
|
+
info('Start agent signer: darksol signer start');
|
|
229
|
+
info('Or set: export DARKSOL_WALLET_PASSWORD=<password>');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const pk = decryptKey(activeWallet, password);
|
|
234
|
+
const provider = new ethers.JsonRpcProvider(getRPC('base'));
|
|
235
|
+
const wallet = new ethers.Wallet(pk, provider);
|
|
236
|
+
const usdc = new ethers.Contract(USDC_BASE, ['function transfer(address,uint256) returns (bool)'], wallet);
|
|
237
|
+
const tx = await usdc.transfer(HOUSE_WALLET, USDC_AMOUNT);
|
|
238
|
+
const receipt = await tx.wait();
|
|
239
|
+
paymentTxHash = receipt.hash;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
paymentSpin.succeed(`Payment sent: ${paymentTxHash.slice(0, 16)}...`);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
paymentSpin.fail('Payment failed');
|
|
245
|
+
error(err.message);
|
|
246
|
+
if (err.message.includes('insufficient')) {
|
|
247
|
+
info('You need at least 1 USDC on Base to play');
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Place the bet with payment proof ──
|
|
183
253
|
const spin = spinner(`Playing ${gameInfo.name}...`).start();
|
|
184
254
|
try {
|
|
185
255
|
const data = await fetchJSON(`${getURL()}/api/bet`, {
|
|
186
256
|
method: 'POST',
|
|
187
257
|
headers: { 'Content-Type': 'application/json' },
|
|
188
|
-
body: JSON.stringify({ gameType, betParams, agentWallet }),
|
|
258
|
+
body: JSON.stringify({ gameType, betParams, agentWallet, paymentTxHash }),
|
|
189
259
|
});
|
|
190
260
|
|
|
191
261
|
if (data.won) {
|
|
@@ -201,6 +271,7 @@ export async function casinoBet(gameType, betParams = {}, opts = {}) {
|
|
|
201
271
|
['Result', data.result || '-'],
|
|
202
272
|
['Won', data.won ? theme.success('YES! 🎉') : theme.error('No')],
|
|
203
273
|
['Payout', data.won ? `$${data.payoutAmount} USDC` : '$0'],
|
|
274
|
+
['Payment TX', paymentTxHash.slice(0, 20) + '...'],
|
|
204
275
|
['Oracle TX', data.oracleTxHash ? data.oracleTxHash.slice(0, 20) + '...' : '-'],
|
|
205
276
|
['Payout TX', data.payoutTxHash ? data.payoutTxHash.slice(0, 20) + '...' : '-'],
|
|
206
277
|
]);
|
|
@@ -215,6 +286,8 @@ export async function casinoBet(gameType, betParams = {}, opts = {}) {
|
|
|
215
286
|
error(err.message);
|
|
216
287
|
if (err.message.includes('not accepting') || err.message.includes('closed')) {
|
|
217
288
|
info('The casino may be temporarily closed. Check: darksol casino status');
|
|
289
|
+
} else if (err.message.includes('payment') || err.message.includes('Payment')) {
|
|
290
|
+
info(`Your USDC was sent (${paymentTxHash.slice(0, 16)}...) — contact support if bet wasn't processed`);
|
|
218
291
|
}
|
|
219
292
|
}
|
|
220
293
|
}
|
package/src/services/oracle.js
CHANGED
|
@@ -1,71 +1,43 @@
|
|
|
1
1
|
import { fetchJSON } from '../utils/fetch.js';
|
|
2
|
-
import
|
|
2
|
+
import { fetchWithX402, isSignerRunning } from '../utils/x402.js';
|
|
3
3
|
import { getServiceURL, getConfig } from '../config/store.js';
|
|
4
4
|
import { theme } from '../ui/theme.js';
|
|
5
5
|
import { spinner, kvDisplay, success, error, warn, info } from '../ui/components.js';
|
|
6
6
|
import { showSection } from '../ui/banner.js';
|
|
7
7
|
|
|
8
|
-
// Oracle lives
|
|
9
|
-
//
|
|
10
|
-
const getURL = () =>
|
|
11
|
-
const custom = getServiceURL('oracle');
|
|
12
|
-
if (custom) return custom;
|
|
13
|
-
return 'https://acp.darksol.net/api/oracle';
|
|
14
|
-
};
|
|
8
|
+
// Oracle lives at acp.darksol.net/api/oracle/
|
|
9
|
+
// Health is free; game endpoints are x402-gated ($0.05 USDC on Base)
|
|
10
|
+
const getURL = () => getServiceURL('oracle') || 'https://acp.darksol.net/api/oracle';
|
|
15
11
|
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
warn('Oracle requires x402 payment ($0.05 USDC on Base)');
|
|
19
|
-
info('Use with agent signer: darksol signer start → requests auto-pay');
|
|
20
|
-
info('Or pay manually via facilitator: darksol facilitator');
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function oracleRequest(path, opts = {}) {
|
|
27
|
-
const url = `${getURL()}${path}`;
|
|
28
|
-
const resp = await fetch(url, opts);
|
|
29
|
-
|
|
30
|
-
if (resp.status === 402) {
|
|
31
|
-
// Return x402 info so caller can handle
|
|
32
|
-
const paymentHeader = resp.headers.get('payment-required');
|
|
33
|
-
let paymentInfo = null;
|
|
34
|
-
if (paymentHeader) {
|
|
35
|
-
try {
|
|
36
|
-
paymentInfo = JSON.parse(Buffer.from(paymentHeader, 'base64').toString());
|
|
37
|
-
} catch {}
|
|
38
|
-
}
|
|
39
|
-
return { x402: true, paymentInfo, status: 402 };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const ct = resp.headers.get('content-type') || '';
|
|
43
|
-
if (!ct.includes('json')) {
|
|
44
|
-
throw new Error(`Oracle returned non-JSON response (${resp.status})`);
|
|
45
|
-
}
|
|
46
|
-
return await resp.json();
|
|
12
|
+
function getSignerToken() {
|
|
13
|
+
return process.env.DARKSOL_SIGNER_TOKEN || getConfig('signerToken') || null;
|
|
47
14
|
}
|
|
48
15
|
|
|
49
16
|
export async function oracleHealth() {
|
|
50
17
|
const spin = spinner('Checking oracle...').start();
|
|
51
18
|
try {
|
|
52
|
-
const data = await
|
|
53
|
-
if (data.x402) {
|
|
54
|
-
spin.succeed('Oracle online (health should be free)');
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
19
|
+
const data = await fetchJSON(`${getURL()}/health`);
|
|
57
20
|
spin.succeed('Oracle online');
|
|
58
21
|
|
|
59
|
-
showSection('ORACLE
|
|
22
|
+
showSection('RANDOM ORACLE 🎲');
|
|
60
23
|
kvDisplay([
|
|
61
24
|
['Status', data.status === 'ok' ? theme.success('● Online') : theme.error('○ ' + data.status)],
|
|
62
25
|
['Contract', data.contract || '-'],
|
|
63
26
|
['Chain', data.chain || 'base'],
|
|
64
27
|
['Block', String(data.blockNumber || '-')],
|
|
65
28
|
]);
|
|
29
|
+
|
|
30
|
+
// Check signer status
|
|
31
|
+
const signerUp = await isSignerRunning(getSignerToken());
|
|
66
32
|
console.log('');
|
|
67
|
-
|
|
68
|
-
|
|
33
|
+
if (signerUp) {
|
|
34
|
+
console.log(` ${theme.success('●')} Agent signer running — x402 auto-pay enabled`);
|
|
35
|
+
} else {
|
|
36
|
+
console.log(` ${theme.dim('○')} Agent signer not running — start for auto-pay: ${theme.gold('darksol signer start')}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('');
|
|
40
|
+
info('Games: coin flip, dice, random number, shuffle ($0.05 USDC each)');
|
|
69
41
|
info('Docs: https://acp.darksol.net/oracle');
|
|
70
42
|
} catch (err) {
|
|
71
43
|
spin.fail('Oracle unreachable');
|
|
@@ -73,99 +45,82 @@ export async function oracleHealth() {
|
|
|
73
45
|
}
|
|
74
46
|
}
|
|
75
47
|
|
|
76
|
-
|
|
77
|
-
const spin = spinner(
|
|
48
|
+
async function oraclePlay(endpoint, label, displayFn) {
|
|
49
|
+
const spin = spinner(`${label}...`).start();
|
|
50
|
+
const token = getSignerToken();
|
|
51
|
+
|
|
78
52
|
try {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
['Amount', `$${(parseInt(accepts.amount) / 1e6).toFixed(2)} USDC`],
|
|
88
|
-
['Network', 'Base'],
|
|
89
|
-
['Pay To', accepts.payTo || '-'],
|
|
90
|
-
]);
|
|
91
|
-
}
|
|
53
|
+
const result = await fetchWithX402(`${getURL()}${endpoint}`, {}, { signerToken: token });
|
|
54
|
+
|
|
55
|
+
if (result.x402 && !result.paid) {
|
|
56
|
+
// Payment required but couldn't auto-pay
|
|
57
|
+
spin.info('x402 payment required');
|
|
58
|
+
const accepts = result.paymentInfo?.accepts?.[0];
|
|
59
|
+
if (accepts) {
|
|
60
|
+
warn(`Cost: $${(parseInt(accepts.amount) / 1e6).toFixed(2)} USDC on Base`);
|
|
92
61
|
}
|
|
93
|
-
|
|
62
|
+
if (result.error) {
|
|
63
|
+
info(result.error);
|
|
64
|
+
} else {
|
|
65
|
+
info('Start agent signer for auto-pay: darksol signer start');
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
94
68
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
69
|
+
|
|
70
|
+
if (result.paid) {
|
|
71
|
+
spin.succeed(`${label} ✓ (paid $0.05 USDC)`);
|
|
72
|
+
} else {
|
|
73
|
+
spin.succeed(label);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
displayFn(result.data);
|
|
77
|
+
return result.data;
|
|
101
78
|
} catch (err) {
|
|
102
|
-
spin.fail(
|
|
79
|
+
spin.fail(`${label} failed`);
|
|
103
80
|
error(err.message);
|
|
81
|
+
return null;
|
|
104
82
|
}
|
|
105
83
|
}
|
|
106
84
|
|
|
85
|
+
export async function oracleFlip() {
|
|
86
|
+
return oraclePlay('/coin', 'Coin flip', (data) => {
|
|
87
|
+
showSection('ORACLE — COIN FLIP 🪙');
|
|
88
|
+
kvDisplay([
|
|
89
|
+
['Result', theme.gold.bold(data.result || data.value || '-')],
|
|
90
|
+
['Proof', data.proof || data.txHash || '-'],
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
107
95
|
export async function oracleDice(sides = 6) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const data = await oracleRequest(`/dice?sides=${sides}`);
|
|
111
|
-
if (data.x402) {
|
|
112
|
-
spin.info('Payment required');
|
|
113
|
-
handleX402(data);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
spin.succeed('Dice rolled');
|
|
117
|
-
showSection(`ORACLE — D${sides}`);
|
|
96
|
+
return oraclePlay(`/dice?sides=${sides}`, `Rolling d${sides}`, (data) => {
|
|
97
|
+
showSection(`ORACLE — D${sides} 🎲`);
|
|
118
98
|
kvDisplay([
|
|
119
|
-
['Result', theme.gold.bold(data.result || data.value)],
|
|
99
|
+
['Result', theme.gold.bold(data.result || data.value || '-')],
|
|
120
100
|
['Sides', sides.toString()],
|
|
121
|
-
['Proof', data.proof || data.txHash || '
|
|
101
|
+
['Proof', data.proof || data.txHash || '-'],
|
|
122
102
|
]);
|
|
123
|
-
}
|
|
124
|
-
spin.fail('Oracle failed');
|
|
125
|
-
error(err.message);
|
|
126
|
-
}
|
|
103
|
+
});
|
|
127
104
|
}
|
|
128
105
|
|
|
129
106
|
export async function oracleNumber(min = 1, max = 100) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const data = await oracleRequest(`/number?min=${min}&max=${max}`);
|
|
133
|
-
if (data.x402) {
|
|
134
|
-
spin.info('Payment required');
|
|
135
|
-
handleX402(data);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
spin.succeed('Number generated');
|
|
139
|
-
showSection('ORACLE — RANDOM NUMBER');
|
|
107
|
+
return oraclePlay(`/number?min=${min}&max=${max}`, `Number ${min}-${max}`, (data) => {
|
|
108
|
+
showSection('ORACLE — RANDOM NUMBER 🔢');
|
|
140
109
|
kvDisplay([
|
|
141
|
-
['Result', theme.gold.bold(data.result || data.value)],
|
|
110
|
+
['Result', theme.gold.bold(data.result || data.value || '-')],
|
|
142
111
|
['Range', `${min} — ${max}`],
|
|
143
|
-
['Proof', data.proof || data.txHash || '
|
|
112
|
+
['Proof', data.proof || data.txHash || '-'],
|
|
144
113
|
]);
|
|
145
|
-
}
|
|
146
|
-
spin.fail('Oracle failed');
|
|
147
|
-
error(err.message);
|
|
148
|
-
}
|
|
114
|
+
});
|
|
149
115
|
}
|
|
150
116
|
|
|
151
117
|
export async function oracleShuffle(items) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
if (data.x402) {
|
|
160
|
-
spin.info('Payment required');
|
|
161
|
-
handleX402(data);
|
|
162
|
-
return;
|
|
118
|
+
return oraclePlay('/shuffle', 'Shuffling', (data) => {
|
|
119
|
+
showSection('ORACLE — SHUFFLE 🔀');
|
|
120
|
+
const result = data.result || data.value || [];
|
|
121
|
+
console.log(theme.gold(' Result: ') + (Array.isArray(result) ? result.join(', ') : result));
|
|
122
|
+
if (data.proof || data.txHash) {
|
|
123
|
+
console.log(theme.dim(` Proof: ${data.proof || data.txHash}`));
|
|
163
124
|
}
|
|
164
|
-
|
|
165
|
-
showSection('ORACLE — SHUFFLE');
|
|
166
|
-
console.log(theme.gold(' Result: ') + (data.result || data.value || []).join(', '));
|
|
167
|
-
} catch (err) {
|
|
168
|
-
spin.fail('Oracle failed');
|
|
169
|
-
error(err.message);
|
|
170
|
-
}
|
|
125
|
+
});
|
|
171
126
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* x402 Payment Flow
|
|
5
|
+
*
|
|
6
|
+
* When a server returns 402 with a `payment-required` header (base64-encoded JSON),
|
|
7
|
+
* this utility:
|
|
8
|
+
* 1. Decodes the payment requirement
|
|
9
|
+
* 2. Signs an EIP-3009 transferWithAuthorization via the local agent signer
|
|
10
|
+
* 3. Retries the original request with the signed payment in the `X-PAYMENT` header
|
|
11
|
+
*
|
|
12
|
+
* Requires: agent signer running at 127.0.0.1:18790
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const SIGNER_URL = 'http://127.0.0.1:18790';
|
|
16
|
+
|
|
17
|
+
// USDC contract details for EIP-3009
|
|
18
|
+
const USDC_CONTRACTS = {
|
|
19
|
+
8453: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // Base
|
|
20
|
+
137: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // Polygon
|
|
21
|
+
1: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // Ethereum
|
|
22
|
+
42161: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // Arbitrum
|
|
23
|
+
10: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // Optimism
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse the payment-required header (base64 JSON)
|
|
28
|
+
*/
|
|
29
|
+
function parsePaymentRequired(header) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(Buffer.from(header, 'base64').toString());
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if agent signer is running
|
|
39
|
+
*/
|
|
40
|
+
async function isSignerRunning(token) {
|
|
41
|
+
try {
|
|
42
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
43
|
+
const resp = await fetch(`${SIGNER_URL}/health`, { headers, timeout: 2000 });
|
|
44
|
+
return resp.ok;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get signer address
|
|
52
|
+
*/
|
|
53
|
+
async function getSignerAddress(token) {
|
|
54
|
+
try {
|
|
55
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
56
|
+
const resp = await fetch(`${SIGNER_URL}/address`, { headers, timeout: 2000 });
|
|
57
|
+
if (!resp.ok) return null;
|
|
58
|
+
const data = await resp.json();
|
|
59
|
+
return data.address;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sign EIP-712 typed data for x402 payment (EIP-3009 transferWithAuthorization)
|
|
67
|
+
*/
|
|
68
|
+
async function signX402Payment(paymentReq, signerToken) {
|
|
69
|
+
const accepts = paymentReq.accepts?.[0];
|
|
70
|
+
if (!accepts) throw new Error('No payment scheme in x402 requirement');
|
|
71
|
+
|
|
72
|
+
// Parse network — format is "eip155:<chainId>"
|
|
73
|
+
const chainId = parseInt(accepts.network?.split(':')[1] || '8453');
|
|
74
|
+
const usdcAddress = accepts.asset || USDC_CONTRACTS[chainId];
|
|
75
|
+
const amount = accepts.amount; // in smallest unit (e.g., 50000 = $0.05 USDC)
|
|
76
|
+
const payTo = accepts.payTo;
|
|
77
|
+
const deadline = Math.floor(Date.now() / 1000) + (accepts.maxTimeoutSeconds || 300);
|
|
78
|
+
|
|
79
|
+
// Get our address
|
|
80
|
+
const fromAddress = await getSignerAddress(signerToken);
|
|
81
|
+
if (!fromAddress) throw new Error('Cannot get signer address');
|
|
82
|
+
|
|
83
|
+
// Generate random nonce (32 bytes)
|
|
84
|
+
const nonce = '0x' + [...Array(32)].map(() => Math.floor(Math.random() * 256).toString(16).padStart(2, '0')).join('');
|
|
85
|
+
|
|
86
|
+
// EIP-712 domain for USDC
|
|
87
|
+
const domain = {
|
|
88
|
+
name: accepts.extra?.name || 'USD Coin',
|
|
89
|
+
version: accepts.extra?.version || '2',
|
|
90
|
+
chainId: chainId,
|
|
91
|
+
verifyingContract: usdcAddress,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// EIP-3009 TransferWithAuthorization types
|
|
95
|
+
const types = {
|
|
96
|
+
TransferWithAuthorization: [
|
|
97
|
+
{ name: 'from', type: 'address' },
|
|
98
|
+
{ name: 'to', type: 'address' },
|
|
99
|
+
{ name: 'value', type: 'uint256' },
|
|
100
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
101
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
102
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const value = {
|
|
107
|
+
from: fromAddress,
|
|
108
|
+
to: payTo,
|
|
109
|
+
value: amount,
|
|
110
|
+
validAfter: '0',
|
|
111
|
+
validBefore: String(deadline),
|
|
112
|
+
nonce: nonce,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Sign via agent signer
|
|
116
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
117
|
+
if (signerToken) headers.Authorization = `Bearer ${signerToken}`;
|
|
118
|
+
|
|
119
|
+
const resp = await fetch(`${SIGNER_URL}/sign-typed-data`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers,
|
|
122
|
+
body: JSON.stringify({ domain, types, value }),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!resp.ok) {
|
|
126
|
+
const err = await resp.text();
|
|
127
|
+
throw new Error(`Signer refused: ${err}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = await resp.json();
|
|
131
|
+
|
|
132
|
+
// Build the x402 payment payload
|
|
133
|
+
return {
|
|
134
|
+
x402Version: paymentReq.x402Version || 2,
|
|
135
|
+
scheme: 'exact',
|
|
136
|
+
network: accepts.network || `eip155:${chainId}`,
|
|
137
|
+
payload: {
|
|
138
|
+
signature: result.signature,
|
|
139
|
+
authorization: {
|
|
140
|
+
from: fromAddress,
|
|
141
|
+
to: payTo,
|
|
142
|
+
value: amount,
|
|
143
|
+
validAfter: '0',
|
|
144
|
+
validBefore: String(deadline),
|
|
145
|
+
nonce: nonce,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Make an x402-aware fetch request.
|
|
153
|
+
*
|
|
154
|
+
* If the server returns 402, this will:
|
|
155
|
+
* 1. Parse the payment requirement
|
|
156
|
+
* 2. Sign the payment via agent signer
|
|
157
|
+
* 3. Retry with the X-PAYMENT header
|
|
158
|
+
*
|
|
159
|
+
* @param {string} url - Request URL
|
|
160
|
+
* @param {object} opts - fetch options
|
|
161
|
+
* @param {object} x402Opts - { signerToken, autoSign }
|
|
162
|
+
* @returns {object} { data, paid, paymentInfo }
|
|
163
|
+
*/
|
|
164
|
+
export async function fetchWithX402(url, opts = {}, x402Opts = {}) {
|
|
165
|
+
const { signerToken, autoSign = true } = x402Opts;
|
|
166
|
+
|
|
167
|
+
// First attempt
|
|
168
|
+
const resp = await fetch(url, opts);
|
|
169
|
+
|
|
170
|
+
if (resp.status !== 402) {
|
|
171
|
+
// Normal response
|
|
172
|
+
const ct = resp.headers.get('content-type') || '';
|
|
173
|
+
if (!ct.includes('json')) {
|
|
174
|
+
const text = await resp.text();
|
|
175
|
+
throw new Error(`Non-JSON response (${resp.status}): ${text.substring(0, 100)}`);
|
|
176
|
+
}
|
|
177
|
+
return { data: await resp.json(), paid: false };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 402 — payment required
|
|
181
|
+
const paymentHeader = resp.headers.get('payment-required');
|
|
182
|
+
if (!paymentHeader) {
|
|
183
|
+
throw new Error('402 but no payment-required header');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const paymentReq = parsePaymentRequired(paymentHeader);
|
|
187
|
+
if (!paymentReq) {
|
|
188
|
+
throw new Error('Failed to parse payment-required header');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!autoSign) {
|
|
192
|
+
return { data: null, paid: false, x402: true, paymentInfo: paymentReq };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check if signer is running
|
|
196
|
+
const signerUp = await isSignerRunning(signerToken);
|
|
197
|
+
if (!signerUp) {
|
|
198
|
+
return {
|
|
199
|
+
data: null,
|
|
200
|
+
paid: false,
|
|
201
|
+
x402: true,
|
|
202
|
+
paymentInfo: paymentReq,
|
|
203
|
+
error: 'Agent signer not running. Start it: darksol signer start',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Sign the payment
|
|
208
|
+
const payment = await signX402Payment(paymentReq, signerToken);
|
|
209
|
+
|
|
210
|
+
// Retry with payment
|
|
211
|
+
const retryOpts = { ...opts };
|
|
212
|
+
retryOpts.headers = {
|
|
213
|
+
...(opts.headers || {}),
|
|
214
|
+
'X-PAYMENT': Buffer.from(JSON.stringify(payment)).toString('base64'),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const retryResp = await fetch(url, retryOpts);
|
|
218
|
+
const ct = retryResp.headers.get('content-type') || '';
|
|
219
|
+
if (!ct.includes('json')) {
|
|
220
|
+
const text = await retryResp.text();
|
|
221
|
+
throw new Error(`Paid but got non-JSON (${retryResp.status}): ${text.substring(0, 100)}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!retryResp.ok) {
|
|
225
|
+
const errData = await retryResp.json().catch(() => ({}));
|
|
226
|
+
throw new Error(`Payment sent but request failed (${retryResp.status}): ${JSON.stringify(errData)}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { data: await retryResp.json(), paid: true, paymentInfo: paymentReq };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export { parsePaymentRequired, isSignerRunning, getSignerAddress, signX402Payment };
|