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 +219 -0
- package/dist/chunk-IXURARCD.js +123 -0
- package/dist/daemon-2URHH37P.js +19 -0
- package/dist/index.js +2101 -0
- package/dist/monitor-VOAT2EK6.js +70 -0
- package/dist/server-QC7YOM64.js +218 -0
- package/package.json +34 -0
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
|
+
};
|