@paychainly/cli 1.0.4 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/bin/paychainly.js +36 -4
- package/package.json +1 -1
- package/src/listen.js +75 -29
- package/src/replay.js +139 -0
- package/src/status.js +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @paychainly/cli
|
|
2
|
+
|
|
3
|
+
> Relay live Paychainly webhooks to your local server during development — no public URL needed.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@paychainly/cli)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @paychainly/cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
paychainly listen --api-key pk_live_... --forward-to http://localhost:3000/webhook
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Webhooks fired on [api.paychainly.com](https://api.paychainly.com) are forwarded in real time to your local server.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
### `listen` — relay live webhooks
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
paychainly listen [options]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Option | Description | Default |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `--api-key` | Your Paychainly API key (`pk_live_...` or `pk_test_...`) | `PAYCHAINLY_API_KEY` env |
|
|
39
|
+
| `--forward-to` | Full local URL to POST webhooks to | `http://localhost:3000/webhook` |
|
|
40
|
+
| `--port` | Shorthand for `--forward-to http://localhost:<port>/webhook` | `3000` |
|
|
41
|
+
| `--host` | Paychainly server URL | `https://api.paychainly.com` |
|
|
42
|
+
| `--secret` | Webhook secret — enables HMAC-SHA256 signature verification | — |
|
|
43
|
+
| `--verbose` | Pretty-print the full webhook payload (JSON) | off |
|
|
44
|
+
| `--filter` | Only relay events matching this name (e.g. `deposit_detected`) | all events |
|
|
45
|
+
| `--log` | Append every received event to a JSONL file for later replay | — |
|
|
46
|
+
|
|
47
|
+
**Examples**
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Basic
|
|
51
|
+
paychainly listen --api-key pk_live_... --port 3000
|
|
52
|
+
|
|
53
|
+
# Show full payload for every event
|
|
54
|
+
paychainly listen --api-key pk_live_... --verbose
|
|
55
|
+
|
|
56
|
+
# Only relay deposit events and save them to disk
|
|
57
|
+
paychainly listen --api-key pk_live_... \
|
|
58
|
+
--filter deposit_detected \
|
|
59
|
+
--log deposits.jsonl
|
|
60
|
+
|
|
61
|
+
# Verify webhook signatures
|
|
62
|
+
paychainly listen --api-key pk_live_... --secret whsec_...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### `status` — check API key & connectivity
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
paychainly status --api-key pk_live_...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Connects to the server, validates the key, and prints the user ID and account mode. Useful for confirming your key is correct before starting a session.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### `replay` — resend saved events to your local server
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
paychainly replay --log events.jsonl [options]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Option | Description | Default |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `--log` | JSONL file produced by `listen --log` **(required)** | — |
|
|
86
|
+
| `--forward-to` | Local URL to POST events to | `http://localhost:3000/webhook` |
|
|
87
|
+
| `--port` | Shorthand for `--forward-to http://localhost:<port>/webhook` | `3000` |
|
|
88
|
+
| `--filter` | Only replay events matching this name | all events |
|
|
89
|
+
| `--delay` | Milliseconds between replayed events | `500` |
|
|
90
|
+
| `--verbose` | Pretty-print each payload before sending | off |
|
|
91
|
+
|
|
92
|
+
**Examples**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Replay everything
|
|
96
|
+
paychainly replay --log events.jsonl
|
|
97
|
+
|
|
98
|
+
# Replay only deposit events with a 1-second gap, show payloads
|
|
99
|
+
paychainly replay --log events.jsonl \
|
|
100
|
+
--filter deposit_detected \
|
|
101
|
+
--delay 1000 \
|
|
102
|
+
--verbose
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Typical Developer Workflow
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# 1. Start listening and capture events to a file
|
|
111
|
+
paychainly listen --api-key pk_live_... --log events.jsonl --verbose
|
|
112
|
+
|
|
113
|
+
# 2. Trigger a test payment on your dashboard
|
|
114
|
+
# 3. Inspect your webhook handler logs
|
|
115
|
+
|
|
116
|
+
# 4. Fix a bug in your handler, restart your server
|
|
117
|
+
|
|
118
|
+
# 5. Replay the captured events — no real payment needed
|
|
119
|
+
paychainly replay --log events.jsonl --filter deposit_detected
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Environment Variables
|
|
125
|
+
|
|
126
|
+
You can set these instead of passing flags every time:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
export PAYCHAINLY_API_KEY=pk_live_...
|
|
130
|
+
export PAYCHAINLY_HOST=https://api.paychainly.com # optional override
|
|
131
|
+
export PAYCHAINLY_WEBHOOK_SECRET=whsec_... # optional
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Webhook Events
|
|
137
|
+
|
|
138
|
+
| Event | Description |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `deposit_detected` | A USDT transfer to a monitored deposit address was confirmed |
|
|
141
|
+
| `sweep_completed` | Funds swept from deposit address to master wallet |
|
|
142
|
+
| `address_expired` | A deposit address session expired without a payment |
|
|
143
|
+
| `withdrawal_completed` | A user-initiated withdrawal was processed |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT © [Paychainly](https://paychainly.com)
|
package/bin/paychainly.js
CHANGED
|
@@ -1,28 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
const [,, cmd, ...argv] = process.argv;
|
|
4
|
+
|
|
5
|
+
if (cmd === '--version' || cmd === '-V') {
|
|
6
|
+
console.log(require('../package.json').version);
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
11
|
+
const v = require('../package.json').version;
|
|
5
12
|
console.log(`
|
|
6
|
-
Paychainly CLI
|
|
13
|
+
Paychainly CLI v${v}
|
|
7
14
|
|
|
8
15
|
Commands:
|
|
9
|
-
listen Relay webhooks to your local server
|
|
16
|
+
listen Relay live webhooks to your local server
|
|
17
|
+
status Check API key validity and server connectivity
|
|
18
|
+
replay Replay saved webhook events to your local server
|
|
10
19
|
|
|
11
20
|
Usage:
|
|
12
|
-
paychainly listen --api-key <key> --port <port>
|
|
13
21
|
paychainly listen --api-key <key> --forward-to http://localhost:3000/webhook
|
|
22
|
+
paychainly listen --api-key <key> --port 3000 --verbose
|
|
23
|
+
paychainly listen --api-key <key> --filter deposit_detected --log events.jsonl
|
|
24
|
+
paychainly status --api-key <key>
|
|
25
|
+
paychainly replay --log events.jsonl --forward-to http://localhost:3000/webhook
|
|
14
26
|
|
|
15
|
-
Options:
|
|
27
|
+
Options (listen):
|
|
16
28
|
--api-key Your Paychainly API key (pk_live_... or pk_test_...)
|
|
17
29
|
--port Local port to forward webhooks to (default: 3000)
|
|
18
30
|
--forward-to Full local URL to forward webhooks to
|
|
19
31
|
--host Paychainly server URL (default: https://api.paychainly.com)
|
|
20
32
|
--secret Webhook secret to verify incoming signatures
|
|
33
|
+
--verbose Print full webhook payload as pretty JSON
|
|
34
|
+
--filter Only relay events matching this name (e.g. deposit_detected)
|
|
35
|
+
--log Append all received events to a JSONL file for later replay
|
|
36
|
+
|
|
37
|
+
Options (status):
|
|
38
|
+
--api-key Your Paychainly API key
|
|
39
|
+
--host Paychainly server URL (default: https://api.paychainly.com)
|
|
40
|
+
|
|
41
|
+
Options (replay):
|
|
42
|
+
--log JSONL file produced by --log (required)
|
|
43
|
+
--forward-to Local URL to forward to (default: http://localhost:3000/webhook)
|
|
44
|
+
--port Local port shorthand
|
|
45
|
+
--filter Only replay events matching this name
|
|
46
|
+
--delay Milliseconds between replayed events (default: 500)
|
|
47
|
+
--verbose Print full payload for each replayed event
|
|
21
48
|
`);
|
|
22
49
|
process.exit(0);
|
|
23
50
|
}
|
|
51
|
+
|
|
24
52
|
if (cmd === 'listen') {
|
|
25
53
|
require('../src/listen.js')(argv);
|
|
54
|
+
} else if (cmd === 'status') {
|
|
55
|
+
require('../src/status.js')(argv);
|
|
56
|
+
} else if (cmd === 'replay') {
|
|
57
|
+
require('../src/replay.js')(argv);
|
|
26
58
|
} else {
|
|
27
59
|
console.error(`Unknown command: ${cmd}. Run "paychainly help" for usage.`);
|
|
28
60
|
process.exit(1);
|
package/package.json
CHANGED
package/src/listen.js
CHANGED
|
@@ -4,20 +4,20 @@ const { io } = require('socket.io-client');
|
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
|
+
const fs = require('fs');
|
|
7
8
|
|
|
8
|
-
// ── ANSI colours ──────────────────────────────────────────────────────────────
|
|
9
9
|
const c = {
|
|
10
|
-
reset:
|
|
11
|
-
bold:
|
|
12
|
-
dim:
|
|
13
|
-
green:
|
|
14
|
-
cyan:
|
|
15
|
-
yellow:
|
|
16
|
-
red:
|
|
17
|
-
magenta:'\x1b[35m',
|
|
18
|
-
blue:
|
|
19
|
-
white:
|
|
20
|
-
gray:
|
|
10
|
+
reset: '\x1b[0m',
|
|
11
|
+
bold: '\x1b[1m',
|
|
12
|
+
dim: '\x1b[2m',
|
|
13
|
+
green: '\x1b[32m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
yellow: '\x1b[33m',
|
|
16
|
+
red: '\x1b[31m',
|
|
17
|
+
magenta: '\x1b[35m',
|
|
18
|
+
blue: '\x1b[34m',
|
|
19
|
+
white: '\x1b[37m',
|
|
20
|
+
gray: '\x1b[90m',
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
function parseArgs(argv) {
|
|
@@ -65,7 +65,7 @@ function postToLocal(url, payload) {
|
|
|
65
65
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
66
66
|
}, (res) => {
|
|
67
67
|
let data = '';
|
|
68
|
-
res.on('data',
|
|
68
|
+
res.on('data', ch => data += ch);
|
|
69
69
|
res.on('end', () => resolve({ statusCode: res.statusCode, body: data, ms: Date.now() - start }));
|
|
70
70
|
});
|
|
71
71
|
req.on('error', (err) => resolve({ statusCode: 0, body: err.message, ms: Date.now() - start }));
|
|
@@ -80,26 +80,41 @@ function now() {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function eventColor(event) {
|
|
83
|
-
if (event?.includes('deposit'))
|
|
84
|
-
if (event?.includes('sweep'))
|
|
83
|
+
if (event?.includes('deposit')) return c.green;
|
|
84
|
+
if (event?.includes('sweep')) return c.cyan;
|
|
85
85
|
if (event?.includes('withdrawal')) return c.blue;
|
|
86
|
-
if (event?.includes('address'))
|
|
86
|
+
if (event?.includes('address')) return c.magenta;
|
|
87
87
|
return c.white;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function prettyJson(obj) {
|
|
91
|
+
return JSON.stringify(obj, null, 2)
|
|
92
|
+
.split('\n')
|
|
93
|
+
.map(l => ' ' + l)
|
|
94
|
+
.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
module.exports = function listen(argv) {
|
|
91
|
-
const args
|
|
92
|
-
const apiKey
|
|
93
|
-
const host
|
|
94
|
-
const secret
|
|
98
|
+
const args = parseArgs(argv);
|
|
99
|
+
const apiKey = args['api-key'] || process.env.PAYCHAINLY_API_KEY;
|
|
100
|
+
const host = args['host'] || process.env.PAYCHAINLY_HOST || 'https://api.paychainly.com';
|
|
101
|
+
const secret = args['secret'] || process.env.PAYCHAINLY_WEBHOOK_SECRET || '';
|
|
95
102
|
const forwardTo = args['forward-to'] || (args['port'] ? `http://localhost:${args['port']}/webhook` : 'http://localhost:3000/webhook');
|
|
103
|
+
const verbose = !!args['verbose'];
|
|
104
|
+
const filter = args['filter'] || null;
|
|
105
|
+
const logPath = args['log'] || null;
|
|
96
106
|
|
|
97
107
|
if (!apiKey) {
|
|
98
108
|
console.error(`${c.red}✗ Missing --api-key. Run: paychainly listen --api-key <pk_live_...>${c.reset}`);
|
|
99
109
|
process.exit(1);
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
|
|
112
|
+
let logStream = null;
|
|
113
|
+
if (logPath) {
|
|
114
|
+
logStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
115
|
+
logStream.on('error', (err) => console.error(`${c.red}✗ Log file error: ${err.message}${c.reset}`));
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
console.log(`
|
|
104
119
|
${c.bold}${c.cyan} ╔═══════════════════════════════════════╗
|
|
105
120
|
║ Paychainly CLI Relay ║
|
|
@@ -107,11 +122,15 @@ ${c.bold}${c.cyan} ╔═══════════════════
|
|
|
107
122
|
`);
|
|
108
123
|
console.log(` ${c.gray}Server :${c.reset} ${host}`);
|
|
109
124
|
console.log(` ${c.gray}Forward :${c.reset} ${c.cyan}${forwardTo}${c.reset}`);
|
|
110
|
-
if (
|
|
125
|
+
if (filter) console.log(` ${c.gray}Filter :${c.reset} ${c.yellow}${filter}${c.reset}`);
|
|
126
|
+
if (logPath) console.log(` ${c.gray}Log :${c.reset} ${logPath}`);
|
|
127
|
+
if (verbose) console.log(` ${c.gray}Mode :${c.reset} ${c.cyan}verbose${c.reset}`);
|
|
128
|
+
if (secret) console.log(` ${c.gray}Secret :${c.reset} ${c.green}✓ signature verification enabled${c.reset}`);
|
|
111
129
|
console.log(` ${c.gray}API key :${c.reset} ${apiKey.slice(0, 12)}${'•'.repeat(8)}\n`);
|
|
112
130
|
|
|
113
131
|
let connected = false;
|
|
114
132
|
let eventCount = 0;
|
|
133
|
+
const eventStats = {};
|
|
115
134
|
|
|
116
135
|
function connect() {
|
|
117
136
|
const socket = io(`${host}/relay`, {
|
|
@@ -137,7 +156,7 @@ ${c.bold}${c.cyan} ╔═══════════════════
|
|
|
137
156
|
console.log(`\n ${c.yellow}⚠ Replaced by a newer connection.${c.reset}`);
|
|
138
157
|
});
|
|
139
158
|
|
|
140
|
-
socket.on('connect_error', (
|
|
159
|
+
socket.on('connect_error', () => {
|
|
141
160
|
if (!connected) {
|
|
142
161
|
process.stdout.write(` ${c.yellow}⟳ Connecting to ${host}...${c.reset}\r`);
|
|
143
162
|
}
|
|
@@ -156,16 +175,34 @@ ${c.bold}${c.cyan} ╔═══════════════════
|
|
|
156
175
|
});
|
|
157
176
|
|
|
158
177
|
socket.on('webhook', async ({ relayId, payload, targetUrl }) => {
|
|
178
|
+
// Skip events that don't match the filter (ack silently)
|
|
179
|
+
if (filter && payload?.event !== filter) {
|
|
180
|
+
socket.emit('webhook_response', { relayId, statusCode: 200, body: '' });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
eventCount++;
|
|
160
|
-
const
|
|
185
|
+
const evtName = payload?.event || 'unknown';
|
|
186
|
+
eventStats[evtName] = (eventStats[evtName] || 0) + 1;
|
|
187
|
+
|
|
188
|
+
const target = forwardTo || targetUrl;
|
|
161
189
|
const sigValid = secret ? verifySignature(payload, secret) : null;
|
|
162
|
-
const ec
|
|
163
|
-
const divider
|
|
190
|
+
const ec = eventColor(payload?.event);
|
|
191
|
+
const divider = c.gray + ' ' + '─'.repeat(56) + c.reset;
|
|
164
192
|
|
|
165
193
|
console.log(divider);
|
|
166
|
-
console.log(` ${c.bold}${ec}${
|
|
194
|
+
console.log(` ${c.bold}${ec}${evtName}${c.reset} ${c.gray}${now()}${c.reset} ${c.gray}#${eventCount}${c.reset}`);
|
|
195
|
+
|
|
167
196
|
if (sigValid === true) console.log(` ${c.green}🔐 Signature valid${c.reset}`);
|
|
168
197
|
if (sigValid === false) console.log(` ${c.red}🔐 Signature INVALID${c.reset}`);
|
|
198
|
+
|
|
199
|
+
if (verbose) {
|
|
200
|
+
console.log(`\n${c.gray}${prettyJson(payload)}${c.reset}\n`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Write to log file before forwarding
|
|
204
|
+
if (logStream) logStream.write(JSON.stringify(payload) + '\n');
|
|
205
|
+
|
|
169
206
|
console.log(` ${c.gray}→${c.reset} ${target}`);
|
|
170
207
|
|
|
171
208
|
const result = await postToLocal(target, payload);
|
|
@@ -176,7 +213,8 @@ ${c.bold}${c.cyan} ╔═══════════════════
|
|
|
176
213
|
} else if (result.statusCode === 0) {
|
|
177
214
|
console.log(` ${c.red}✗ Connection refused — is your local server running?${c.reset}`);
|
|
178
215
|
} else {
|
|
179
|
-
console.log(` ${c.red}✗ ${result.statusCode} ${result.ms}ms${c.reset}
|
|
216
|
+
console.log(` ${c.red}✗ ${result.statusCode} ${result.ms}ms${c.reset}`);
|
|
217
|
+
if (result.body) console.log(` ${c.gray} ${result.body.slice(0, 200)}${c.reset}`);
|
|
180
218
|
}
|
|
181
219
|
|
|
182
220
|
socket.emit('webhook_response', {
|
|
@@ -194,7 +232,15 @@ ${c.bold}${c.cyan} ╔═══════════════════
|
|
|
194
232
|
process.on('SIGINT', () => {
|
|
195
233
|
console.log(`\n\n ${c.gray}Closing relay...${c.reset}`);
|
|
196
234
|
socket.disconnect();
|
|
197
|
-
|
|
235
|
+
if (logStream) logStream.end();
|
|
236
|
+
console.log(` ${c.green}✓ ${eventCount} event(s) relayed${c.reset}`);
|
|
237
|
+
if (eventCount > 0) {
|
|
238
|
+
Object.entries(eventStats).forEach(([evt, count]) => {
|
|
239
|
+
console.log(` ${c.gray} ${evt}: ${count}${c.reset}`);
|
|
240
|
+
});
|
|
241
|
+
if (logPath) console.log(` ${c.gray} saved → ${logPath}${c.reset}`);
|
|
242
|
+
}
|
|
243
|
+
console.log();
|
|
198
244
|
process.exit(0);
|
|
199
245
|
});
|
|
200
246
|
};
|
package/src/replay.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
|
|
7
|
+
const c = {
|
|
8
|
+
reset: '\x1b[0m', bold: '\x1b[1m', green: '\x1b[32m', cyan: '\x1b[36m',
|
|
9
|
+
red: '\x1b[31m', gray: '\x1b[90m', yellow: '\x1b[33m', white: '\x1b[37m',
|
|
10
|
+
magenta: '\x1b[35m', blue: '\x1b[34m',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const args = {};
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
if (argv[i].startsWith('--')) {
|
|
17
|
+
const key = argv[i].slice(2);
|
|
18
|
+
args[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function postToLocal(url, payload) {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const body = JSON.stringify(payload);
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
29
|
+
const start = Date.now();
|
|
30
|
+
const req = lib.request({
|
|
31
|
+
hostname: parsed.hostname,
|
|
32
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
33
|
+
path: parsed.pathname + parsed.search,
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
36
|
+
}, (res) => {
|
|
37
|
+
let data = '';
|
|
38
|
+
res.on('data', ch => data += ch);
|
|
39
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body: data, ms: Date.now() - start }));
|
|
40
|
+
});
|
|
41
|
+
req.on('error', (err) => resolve({ statusCode: 0, body: err.message, ms: Date.now() - start }));
|
|
42
|
+
req.setTimeout(30_000, () => { req.destroy(); resolve({ statusCode: 0, body: 'timeout', ms: Date.now() - start }); });
|
|
43
|
+
req.write(body);
|
|
44
|
+
req.end();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
49
|
+
|
|
50
|
+
function eventColor(event) {
|
|
51
|
+
if (event?.includes('deposit')) return c.green;
|
|
52
|
+
if (event?.includes('sweep')) return c.cyan;
|
|
53
|
+
if (event?.includes('withdrawal')) return c.blue;
|
|
54
|
+
if (event?.includes('address')) return c.magenta;
|
|
55
|
+
return c.white;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function prettyJson(obj) {
|
|
59
|
+
return JSON.stringify(obj, null, 2).split('\n').map(l => ' ' + l).join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = async function replay(argv) {
|
|
63
|
+
const args = parseArgs(argv);
|
|
64
|
+
const logFile = args['log'];
|
|
65
|
+
const forwardTo = args['forward-to'] || (args['port'] ? `http://localhost:${args['port']}/webhook` : 'http://localhost:3000/webhook');
|
|
66
|
+
const filter = args['filter'] || null;
|
|
67
|
+
const delay = parseInt(args['delay'] || '500', 10);
|
|
68
|
+
const verbose = !!args['verbose'];
|
|
69
|
+
|
|
70
|
+
if (!logFile) {
|
|
71
|
+
console.error(`${c.red}✗ Missing --log <file>. Run: paychainly replay --log events.jsonl${c.reset}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let lines;
|
|
76
|
+
try {
|
|
77
|
+
lines = fs.readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
|
|
78
|
+
} catch {
|
|
79
|
+
console.error(`${c.red}✗ Cannot read file: ${logFile}${c.reset}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const events = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
84
|
+
const toReplay = filter ? events.filter(e => e.event === filter) : events;
|
|
85
|
+
|
|
86
|
+
console.log(`
|
|
87
|
+
${c.bold}${c.cyan} ╔═══════════════════════════════════════╗
|
|
88
|
+
║ Paychainly CLI Replay ║
|
|
89
|
+
╚═══════════════════════════════════════╝${c.reset}
|
|
90
|
+
`);
|
|
91
|
+
console.log(` ${c.gray}File :${c.reset} ${logFile}`);
|
|
92
|
+
console.log(` ${c.gray}Forward :${c.reset} ${c.cyan}${forwardTo}${c.reset}`);
|
|
93
|
+
console.log(` ${c.gray}Events :${c.reset} ${toReplay.length}${filter ? ` ${c.yellow}(filter: ${filter})${c.reset}` : ''}`);
|
|
94
|
+
console.log(` ${c.gray}Delay :${c.reset} ${delay}ms between events\n`);
|
|
95
|
+
|
|
96
|
+
if (toReplay.length === 0) {
|
|
97
|
+
console.log(` ${c.yellow}No events to replay.${c.reset}\n`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let ok = 0, fail = 0;
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < toReplay.length; i++) {
|
|
104
|
+
const payload = toReplay[i];
|
|
105
|
+
const evtName = payload?.event || 'unknown';
|
|
106
|
+
const ec = eventColor(evtName);
|
|
107
|
+
const divider = c.gray + ' ' + '─'.repeat(56) + c.reset;
|
|
108
|
+
|
|
109
|
+
console.log(divider);
|
|
110
|
+
console.log(` ${c.bold}${ec}${evtName}${c.reset} ${c.gray}#${i + 1}/${toReplay.length}${c.reset}`);
|
|
111
|
+
|
|
112
|
+
if (verbose) {
|
|
113
|
+
console.log(`\n${c.gray}${prettyJson(payload)}${c.reset}\n`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(` ${c.gray}→${c.reset} ${forwardTo}`);
|
|
117
|
+
|
|
118
|
+
const result = await postToLocal(forwardTo, payload);
|
|
119
|
+
const isOk = result.statusCode >= 200 && result.statusCode < 300;
|
|
120
|
+
|
|
121
|
+
if (isOk) {
|
|
122
|
+
ok++;
|
|
123
|
+
console.log(` ${c.green}✓ ${result.statusCode} ${result.ms}ms${c.reset}`);
|
|
124
|
+
} else if (result.statusCode === 0) {
|
|
125
|
+
fail++;
|
|
126
|
+
console.log(` ${c.red}✗ Connection refused — is your local server running?${c.reset}`);
|
|
127
|
+
} else {
|
|
128
|
+
fail++;
|
|
129
|
+
console.log(` ${c.red}✗ ${result.statusCode} ${result.ms}ms${c.reset}`);
|
|
130
|
+
if (result.body) console.log(` ${c.gray} ${result.body.slice(0, 200)}${c.reset}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (i < toReplay.length - 1) await sleep(delay);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const divider = c.gray + ' ' + '─'.repeat(56) + c.reset;
|
|
137
|
+
console.log(divider);
|
|
138
|
+
console.log(`\n ${c.green}✓ ${ok} delivered${c.reset}${fail ? ` ${c.red}✗ ${fail} failed${c.reset}` : ''}\n`);
|
|
139
|
+
};
|
package/src/status.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { io } = require('socket.io-client');
|
|
4
|
+
|
|
5
|
+
const c = {
|
|
6
|
+
reset: '\x1b[0m', bold: '\x1b[1m', green: '\x1b[32m',
|
|
7
|
+
cyan: '\x1b[36m', red: '\x1b[31m', gray: '\x1b[90m', yellow: '\x1b[33m',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
const args = {};
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
if (argv[i].startsWith('--')) {
|
|
14
|
+
const key = argv[i].slice(2);
|
|
15
|
+
args[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return args;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = function status(argv) {
|
|
22
|
+
const args = parseArgs(argv);
|
|
23
|
+
const apiKey = args['api-key'] || process.env.PAYCHAINLY_API_KEY;
|
|
24
|
+
const host = args['host'] || process.env.PAYCHAINLY_HOST || 'https://api.paychainly.com';
|
|
25
|
+
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
console.error(`${c.red}✗ Missing --api-key${c.reset}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`\n ${c.bold}${c.cyan}Paychainly Status Check${c.reset}\n`);
|
|
32
|
+
console.log(` ${c.gray}Server :${c.reset} ${host}`);
|
|
33
|
+
console.log(` ${c.gray}API key :${c.reset} ${apiKey.slice(0, 12)}${'•'.repeat(8)}\n`);
|
|
34
|
+
|
|
35
|
+
process.stdout.write(` ${c.yellow}⟳ Connecting...${c.reset}\r`);
|
|
36
|
+
|
|
37
|
+
const socket = io(`${host}/relay`, {
|
|
38
|
+
auth: { apiKey },
|
|
39
|
+
transports: ['websocket'],
|
|
40
|
+
reconnection: false,
|
|
41
|
+
timeout: 10000,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
socket.on('relay_connected', ({ userId, mode }) => {
|
|
45
|
+
process.stdout.write(' '.repeat(40) + '\r');
|
|
46
|
+
console.log(` ${c.green}✓ API key valid${c.reset}`);
|
|
47
|
+
console.log(` ${c.gray}User ID :${c.reset} ${userId}`);
|
|
48
|
+
console.log(` ${c.gray}Mode :${c.reset} ${mode}\n`);
|
|
49
|
+
socket.disconnect();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
socket.on('relay_error', ({ message }) => {
|
|
54
|
+
process.stdout.write(' '.repeat(40) + '\r');
|
|
55
|
+
console.error(` ${c.red}✗ ${message}${c.reset}\n`);
|
|
56
|
+
socket.disconnect();
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
socket.on('connect_error', (err) => {
|
|
61
|
+
process.stdout.write(' '.repeat(40) + '\r');
|
|
62
|
+
console.error(` ${c.red}✗ Cannot reach server: ${err.message}${c.reset}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
console.error(`\n ${c.red}✗ Timed out after 10s — server unreachable${c.reset}\n`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}, 10000);
|
|
70
|
+
};
|