@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
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, execution scripts, AI-powered market analysis, secure agent signing, 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) on-chain oracle/casino/cards, (7) natural language trading via LLM, (8) signing transactions securely for x402/contracts without exposing private keys."
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
- ## Install
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
- ## Quick Reference
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
- darksol wallet create <name> # Create new wallet (AES-256-GCM encrypted)
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] # Check ETH + USDC balance
61
+ darksol wallet balance [name] # ETH + USDC balance
24
62
  darksol wallet use <name> # Set active wallet
25
- darksol wallet export [name] # Export details (password required for PK)
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 # Swap via Uniswap V3
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
- ### DCA (Dollar-Cost Averaging)
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 order creation
39
- darksol dca list # List active DCA orders
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 an order
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 trading chat
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 # AI-powered token analysis
94
+ darksol ai analyze AERO # Token analysis
50
95
  ```
51
96
 
52
- ### Execution Scripts
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 # List available templates
55
- darksol script create # Create from template (buy, sell, limit-buy, stop-loss, etc.)
56
- darksol script list # List saved scripts
57
- darksol script run <name> # Execute (requires wallet password)
58
- darksol script run <name> -p "pw" -y # Non-interactive (for automation/cron)
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 params/wallet/chain
61
- darksol script clone <name> <new> # Clone a script
62
- darksol script delete <name> # Delete a script
113
+ darksol script edit <name> # Edit
114
+ darksol script clone <name> <new> # Clone
115
+ darksol script delete <name> # Delete
63
116
  ```
64
117
 
65
- Script templates: `buy-token`, `sell-token`, `limit-buy`, `stop-loss`, `multi-buy`, `transfer`, `empty` (custom)
66
-
67
- ### Market Intel
118
+ ### 📈 Market Intel
68
119
  ```bash
69
- darksol market top # Top movers on Base
70
- darksol market top -c ethereum # Top movers on Ethereum
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
- ### Secure Agent Signer (for OpenClaw / AI agents)
128
+ ### 🎰 Casino (The Clawsino)
129
+ All bets are $1 USDC. House edge: 5%. Results verified on-chain.
130
+
76
131
  ```bash
77
- darksol agent start <wallet> # Start signing proxy
78
- darksol agent start <wallet> --max-value 0.5 --daily-limit 2.0
79
- darksol agent docs # Full security documentation
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
- The agent signer creates a local HTTP server at `127.0.0.1:18790` that signs transactions without exposing the private key. AI agents authenticate with a one-time bearer token.
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
- **Endpoints:**
85
- - `GET /address` wallet address
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
- ### Casino
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 casino bet coin-flip heads # Place a bet
104
- darksol casino tables # View games
105
- darksol casino stats # House stats
106
- darksol casino receipt <id> # Verify on-chain
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
- ### Prepaid Cards
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 cards catalog # Available providers
112
- darksol cards order -p swype -a 50 # Order a card
113
- darksol cards status <id> # Check order
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
- ### Builder Index
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
- ### Facilitator
216
+ ### 📧 AgentMail
124
217
  ```bash
125
- darksol facilitator health # Status
126
- darksol facilitator verify <payment> # Verify off-chain
127
- darksol facilitator settle <payment> # Settle on-chain (free)
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
- ### API Keys
225
+ ### Gas & Network
131
226
  ```bash
132
- darksol keys list # Show all services + status
133
- darksol keys add openai # Add OpenAI key
134
- darksol keys add coingecko # Add CoinGecko Pro key
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
- Supported: `openai`, `anthropic`, `openrouter`, `ollama`, `coingecko`, `dexscreener`, `alchemy`, `infura`, `quicknode`, `oneinch`, `paraswap`
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 # Set slippage %
146
- darksol config rpc base https://... # Custom RPC endpoint
236
+ darksol config set slippage 1.0 # Slippage %
237
+ darksol config rpc base https://... # Custom RPC
147
238
  ```
148
239
 
149
- ### Reference
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... # Look up address on-chain
261
+ darksol lookup 0x... # On-chain address lookup
262
+ darksol setup # Re-run setup wizard
156
263
  ```
157
264
 
158
- ## Supported Chains
159
- - **Base** (default) — chain ID 8453
160
- - **Ethereum** chain ID 1
161
- - **Polygon** — chain ID 137
162
- - **Arbitrum** chain ID 42161
163
- - **Optimism** — chain ID 10
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
- ## Agent Integration Notes
346
+ ## Security Model
166
347
 
167
- - All commands work non-interactively with flags (`-p`, `-y`, `--key`, etc.)
168
- - Set `darksol config set output json` for programmatic JSON responses
169
- - Scripts can be executed via cron: `darksol script run my-dca -p "pass" -y`
170
- - The agent signer is the recommended way to give AI agents wallet access
171
- - Helper functions available at `@darksol/terminal/src/utils/helpers.js`
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
- ## Security
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. 🌑
@@ -1,10 +1,11 @@
1
1
  import { fetchJSON } from '../utils/fetch.js';
2
2
  import fetch from 'node-fetch';
3
- import { getServiceURL } from '../config/store.js';
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
  }
@@ -1,71 +1,43 @@
1
1
  import { fetchJSON } from '../utils/fetch.js';
2
- import fetch from 'node-fetch';
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 under acp.darksol.net/api/oracle/
9
- // Endpoints: health (free), coin/dice/number/shuffle (x402 $0.05 USDC on Base)
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 handleX402(response) {
17
- if (response === 402 || (response && response.status === 402)) {
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 oracleRequest('/health');
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 STATUS');
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
- info('Endpoints require x402 payment ($0.05 USDC on Base)');
68
- info('Games: coin flip, dice, random number, shuffle');
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
- export async function oracleFlip() {
77
- const spin = spinner('Flipping coin...').start();
48
+ async function oraclePlay(endpoint, label, displayFn) {
49
+ const spin = spinner(`${label}...`).start();
50
+ const token = getSignerToken();
51
+
78
52
  try {
79
- const data = await oracleRequest('/coin');
80
- if (data.x402) {
81
- spin.info('Payment required');
82
- handleX402(data);
83
- if (data.paymentInfo) {
84
- const accepts = data.paymentInfo.accepts?.[0];
85
- if (accepts) {
86
- kvDisplay([
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
- return;
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
- spin.succeed('Coin flipped');
96
- showSection('ORACLE — COIN FLIP');
97
- kvDisplay([
98
- ['Result', theme.gold.bold(data.result || data.value)],
99
- ['Proof', data.proof || data.txHash || 'N/A'],
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('Oracle failed');
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
- const spin = spinner(`Rolling d${sides}...`).start();
109
- try {
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 || 'N/A'],
101
+ ['Proof', data.proof || data.txHash || '-'],
122
102
  ]);
123
- } catch (err) {
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
- const spin = spinner(`Generating number ${min}-${max}...`).start();
131
- try {
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 || 'N/A'],
112
+ ['Proof', data.proof || data.txHash || '-'],
144
113
  ]);
145
- } catch (err) {
146
- spin.fail('Oracle failed');
147
- error(err.message);
148
- }
114
+ });
149
115
  }
150
116
 
151
117
  export async function oracleShuffle(items) {
152
- const spin = spinner('Shuffling...').start();
153
- try {
154
- const data = await oracleRequest('/shuffle', {
155
- method: 'POST',
156
- headers: { 'Content-Type': 'application/json' },
157
- body: JSON.stringify({ items }),
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
- spin.succeed('Shuffled');
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 };