clw-cash 0.1.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/SKILL.md ADDED
@@ -0,0 +1,219 @@
1
+ # cash - Bitcoin & Stablecoin Agent Wallet
2
+
3
+ A command-line tool for sending and receiving Bitcoin and stablecoins. Keys are held in a secure enclave — the CLI never touches private keys.
4
+
5
+ All commands output JSON to stdout. Exit code 0 = success, 1 = error.
6
+
7
+ ## Setup
8
+
9
+ ```bash
10
+ # First time — authenticates, creates identity, saves config, starts daemon
11
+ cash init
12
+ # Re-authenticate when session expires
13
+ cash login
14
+ ```
15
+
16
+ `init` handles authentication automatically (Telegram 2FA in production, auto-resolves in test mode). It creates an identity, saves config to `~/.clw-cash/config.json`, and **auto-starts a background daemon** for monitoring swaps (Lightning HTLC claiming and LendaSwap polling).
17
+
18
+ If the session token expires, run `cash login` to re-authenticate. If the daemon stops, restart it with `cash start`.
19
+
20
+ You can also pass a token explicitly: `cash init --api-url <url> --token <jwt> --ark-server <url>`.
21
+
22
+ Or set environment variables:
23
+
24
+ ```bash
25
+ CLW_API_URL=https://api.clw.cash
26
+ CLW_SESSION_TOKEN=<jwt>
27
+ CLW_IDENTITY_ID=<uuid>
28
+ CLW_PUBLIC_KEY=<hex>
29
+ CLW_ARK_SERVER_URL=https://ark.clw.cash
30
+ ```
31
+
32
+ ## Commands
33
+
34
+ ### Send Bitcoin
35
+
36
+ ```bash
37
+ # Send sats via Ark (instant, off-chain)
38
+ cash send --amount 100000 --currency btc --where arkade --to <ark-address>
39
+
40
+ # Send sats on-chain
41
+ cash send --amount 100000 --currency btc --where onchain --to <bitcoin-address>
42
+
43
+ # Pay a Lightning invoice
44
+ cash send --amount 50000 --currency btc --where lightning --to <bolt11-invoice>
45
+
46
+ # Auto-detect invoice format (bolt11 or BIP21, positional arg)
47
+ cash send lnbc500n1pj...
48
+ cash send bitcoin:bc1q...?amount=0.001&lightning=lnbc...
49
+ ```
50
+
51
+ ### Send Stablecoins (BTC to Stablecoin swap)
52
+
53
+ ```bash
54
+ # Swap BTC to USDT on Polygon
55
+ cash send --amount 10 --currency usdt --where polygon --to <0x-address>
56
+
57
+ # Swap BTC to USDC on Arbitrum
58
+ cash send --amount 50 --currency usdc --where arbitrum --to <0x-address>
59
+ ```
60
+
61
+ ### Receive Bitcoin
62
+
63
+ ```bash
64
+ # Get an Ark address
65
+ cash receive --amount 100000 --currency btc --where arkade
66
+ # -> {"ok": true, "data": {"address": "ark1q...", "type": "ark", "amount": 100000}}
67
+
68
+ # Create a Lightning invoice
69
+ cash receive --amount 50000 --currency btc --where lightning
70
+ # -> {"ok": true, "data": {"bolt11": "lnbc...", "paymentHash": "...", "amount": 50000}}
71
+
72
+ # Get a boarding (on-chain) address
73
+ cash receive --amount 100000 --currency btc --where onchain
74
+ # -> {"ok": true, "data": {"address": "bc1q...", "type": "onchain", "amount": 100000}}
75
+ ```
76
+
77
+ ### Receive Stablecoins (Stablecoin to BTC swap)
78
+
79
+ ```bash
80
+ # Receive USDT from Polygon (swap to BTC)
81
+ cash receive --amount 10 --currency usdt --where polygon --address <0x-sender-address>
82
+ ```
83
+
84
+ ### Check Balance
85
+
86
+ ```bash
87
+ cash balance
88
+ # -> {"ok": true, "data": {"total": 250000, "offchain": {"settled": 50000, "preconfirmed": 20000, "available": 70000}, "onchain": {"confirmed": 30000, "total": 30000}}}
89
+ ```
90
+
91
+ ### Swap Management
92
+
93
+ ```bash
94
+ # Check a single swap by ID
95
+ cash swap <swap-id>
96
+ # -> {"ok": true, "data": {"id": "...", "status": "funded", "direction": "btc_to_stablecoin", "local": {...}, "remote": {...}}}
97
+
98
+ # List swaps (grouped by status, last 5 per category)
99
+ cash swaps
100
+ # -> {"ok": true, "data": {"lendaswap": {"pending": [...], "claimed": [...], "refunded": [...], "expired": [...], "failed": [...]}}}
101
+
102
+ # Filter by status
103
+ cash swaps --pending
104
+ cash swaps --claimed --limit 10
105
+
106
+ # Manually claim a completed swap
107
+ cash claim <swap-id>
108
+ # -> {"ok": true, "data": {"success": true, "txHash": "0x...", "chain": "polygon"}}
109
+
110
+ # Refund an expired swap
111
+ cash refund <swap-id>
112
+ # -> {"ok": true, "data": {"success": true, "txId": "...", "refundAmount": 95000}}
113
+ ```
114
+
115
+ ### Daemon (Swap Monitoring)
116
+
117
+ The daemon runs in the background to automatically claim Lightning HTLCs and monitor LendaSwap swaps. It is **auto-started by `cash init`**. Use these commands to manage it manually:
118
+
119
+ ```bash
120
+ # Start the daemon
121
+ cash start
122
+ # -> {"ok": true, "data": {"started": true, "pid": 12345, "port": 3457}}
123
+
124
+ # Check daemon and session status
125
+ cash status
126
+ # -> {"ok": true, "data": {"session": "active", "sessionExpiresAt": 1739..., "daemon": {"running": true, "pid": 12345, "port": 3457}}}
127
+
128
+ # List pending swaps
129
+ cash swaps --pending
130
+
131
+ # Stop the daemon
132
+ cash stop
133
+ # -> {"ok": true, "data": {"stopped": true, "pid": 12345}}
134
+ ```
135
+
136
+ ## Output Format
137
+
138
+ Success:
139
+
140
+ ```json
141
+ {"ok": true, "data": { ... }}
142
+ ```
143
+
144
+ Error:
145
+
146
+ ```json
147
+ {"ok": false, "error": "description of what went wrong"}
148
+ ```
149
+
150
+ ## Currency & Network Matrix
151
+
152
+ | Currency | Networks |
153
+ | -------- | --------------------------- |
154
+ | btc | onchain, lightning, arkade |
155
+ | usdt | polygon, ethereum, arbitrum |
156
+ | usdc | polygon, ethereum, arbitrum |
157
+
158
+ ## Swap Status Lifecycle
159
+
160
+ | Status | Meaning |
161
+ | ------------------ | ------------------------------ |
162
+ | pending | Swap created, awaiting funding |
163
+ | awaiting_funding | Initial state |
164
+ | funded | User has sent funds |
165
+ | processing | Swap in progress |
166
+ | completed | Swap done, claimed |
167
+ | expired | Timelock expired |
168
+ | refunded | Funds returned |
169
+ | failed | Swap failed |
170
+
171
+ Directions: `btc_to_stablecoin` or `stablecoin_to_btc`.
172
+
173
+ Token identifiers: `btc_arkade`, `usdc_pol`, `usdc_eth`, `usdc_arb`, `usdt0_pol`, `usdt_eth`, `usdt_arb`.
174
+
175
+ ## Agent Tips
176
+
177
+ All output is JSON — pipe through `jq` to extract specific fields:
178
+
179
+ ```bash
180
+ # Get just the swap status
181
+ cash swap <swap-id> | jq .data.status
182
+
183
+ # Get total balance in sats
184
+ cash balance | jq .data.total
185
+
186
+ # Get offchain available balance
187
+ cash balance | jq .data.offchain.available
188
+
189
+ # Check if daemon is running
190
+ cash status | jq .data.daemon.running
191
+
192
+ # Check session state (active or expired)
193
+ cash status | jq .data.session
194
+
195
+ # List only pending swap IDs
196
+ cash swaps --pending | jq '[.data.lendaswap.pending[].id]'
197
+
198
+ # Get the payment bolt11 invoice
199
+ cash receive --amount 50000 --currency btc --where lightning | jq -r .data.bolt11
200
+
201
+ # Get the ark address for receiving
202
+ cash receive --amount 100000 --currency btc --where arkade | jq -r .data.address
203
+
204
+ # Check if a command succeeded
205
+ cash send ... && echo "sent" || echo "failed"
206
+ ```
207
+
208
+ Common workflow for monitoring a stablecoin swap:
209
+
210
+ ```bash
211
+ # 1. Initiate the swap
212
+ cash send --amount 10 --currency usdc --where polygon --to 0x...
213
+
214
+ # 2. Poll status until completed (daemon does this automatically)
215
+ cash swap <swap-id> | jq .data.status
216
+
217
+ # 3. If expired, refund
218
+ cash refund <swap-id>
219
+ ```
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/daemon.ts
4
+ import { spawn } from "child_process";
5
+ import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ import { resolve } from "path";
9
+ var CONFIG_DIR = join(homedir(), ".clw-cash");
10
+ var PID_FILE = join(CONFIG_DIR, "daemon.pid");
11
+ var LOG_FILE = join(CONFIG_DIR, "daemon.log");
12
+ function getPort() {
13
+ const envPort = process.env.CLW_DAEMON_PORT;
14
+ if (envPort) {
15
+ const parsed = parseInt(envPort, 10);
16
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
17
+ }
18
+ return 3457;
19
+ }
20
+ function getDaemonStatus() {
21
+ let data;
22
+ try {
23
+ const raw = readFileSync(PID_FILE, "utf-8");
24
+ data = JSON.parse(raw);
25
+ } catch {
26
+ return { running: false };
27
+ }
28
+ try {
29
+ process.kill(data.pid, 0);
30
+ return { running: true, pid: data.pid, port: data.port };
31
+ } catch {
32
+ try {
33
+ unlinkSync(PID_FILE);
34
+ } catch {
35
+ }
36
+ return { running: false };
37
+ }
38
+ }
39
+ function saveDaemonPid(pid, port) {
40
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
41
+ writeFileSync(PID_FILE, JSON.stringify({ pid, port }), { mode: 384 });
42
+ }
43
+ function removeDaemonPid() {
44
+ try {
45
+ unlinkSync(PID_FILE);
46
+ } catch {
47
+ }
48
+ }
49
+ async function startDaemonInBackground(port) {
50
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
51
+ const logFd = openSync(LOG_FILE, "a");
52
+ const entrypoint = resolve(process.cwd(), "cli/src/index.ts");
53
+ const tsxBin = resolve(process.cwd(), "node_modules/.bin/tsx");
54
+ const child = spawn(tsxBin, [entrypoint, "--daemon-internal", "--port", String(port)], {
55
+ cwd: process.cwd(),
56
+ env: { ...process.env },
57
+ detached: true,
58
+ stdio: ["ignore", logFd, logFd]
59
+ });
60
+ const pid = child.pid;
61
+ if (!pid) {
62
+ throw new Error("Failed to spawn daemon process");
63
+ }
64
+ child.unref();
65
+ const deadline = Date.now() + 3e4;
66
+ while (Date.now() < deadline) {
67
+ try {
68
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
69
+ if (res.ok) {
70
+ return { pid };
71
+ }
72
+ } catch {
73
+ }
74
+ await new Promise((r) => setTimeout(r, 250));
75
+ }
76
+ try {
77
+ process.kill(pid, "SIGTERM");
78
+ } catch {
79
+ }
80
+ throw new Error(`Daemon did not become healthy within 30s (pid: ${pid})`);
81
+ }
82
+ async function ensureDaemonRunning() {
83
+ const status = getDaemonStatus();
84
+ if (status.running && status.pid && status.port) {
85
+ return { pid: status.pid, port: status.port };
86
+ }
87
+ const port = getPort();
88
+ const { pid } = await startDaemonInBackground(port);
89
+ return { pid, port };
90
+ }
91
+ async function stopDaemon() {
92
+ const status = getDaemonStatus();
93
+ if (!status.running || !status.pid) {
94
+ return false;
95
+ }
96
+ process.kill(status.pid, "SIGTERM");
97
+ const deadline = Date.now() + 5e3;
98
+ while (Date.now() < deadline) {
99
+ try {
100
+ process.kill(status.pid, 0);
101
+ } catch {
102
+ removeDaemonPid();
103
+ return true;
104
+ }
105
+ await new Promise((r) => setTimeout(r, 200));
106
+ }
107
+ try {
108
+ process.kill(status.pid, "SIGKILL");
109
+ } catch {
110
+ }
111
+ removeDaemonPid();
112
+ return true;
113
+ }
114
+
115
+ export {
116
+ getPort,
117
+ getDaemonStatus,
118
+ saveDaemonPid,
119
+ removeDaemonPid,
120
+ startDaemonInBackground,
121
+ ensureDaemonRunning,
122
+ stopDaemon
123
+ };
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ensureDaemonRunning,
4
+ getDaemonStatus,
5
+ getPort,
6
+ removeDaemonPid,
7
+ saveDaemonPid,
8
+ startDaemonInBackground,
9
+ stopDaemon
10
+ } from "./chunk-IXURARCD.js";
11
+ export {
12
+ ensureDaemonRunning,
13
+ getDaemonStatus,
14
+ getPort,
15
+ removeDaemonPid,
16
+ saveDaemonPid,
17
+ startDaemonInBackground,
18
+ stopDaemon
19
+ };