@profullstack/coinpay 0.8.0 → 0.10.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/CHANGELOG.md +77 -0
- package/README.md +136 -3
- package/bin/coinpay.js +1057 -41
- package/package.json +37 -9
- package/src/card-payments.js +23 -14
- package/src/client.d.ts +10 -0
- package/src/client.js +131 -2
- package/src/finances-commands.js +423 -0
- package/src/finances-report.js +458 -0
- package/src/finances-reports.d.ts +243 -0
- package/src/finances-reports.js +257 -0
- package/src/finances-tui.js +1019 -0
- package/src/finances.d.ts +365 -0
- package/src/finances.js +511 -0
- package/src/index.d.ts +4 -0
- package/src/index.js +38 -0
- package/src/invoices.d.ts +1 -0
- package/src/invoices.js +14 -1
- package/src/lightning.js +8 -3
- package/src/pdf.js +151 -0
- package/src/swap.js +36 -5
- package/src/taskmarket.js +329 -0
- package/src/wallet.js +54 -24
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/coinpay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CoinPay SDK & CLI
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "CoinPay SDK & CLI \u2014 Accept cryptocurrency and card payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet, swap and a live finances dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
@@ -48,6 +48,29 @@
|
|
|
48
48
|
},
|
|
49
49
|
"./x402-pay-server": {
|
|
50
50
|
"import": "./src/x402-pay-server.js"
|
|
51
|
+
},
|
|
52
|
+
"./taskmarket": {
|
|
53
|
+
"import": "./src/taskmarket.js"
|
|
54
|
+
},
|
|
55
|
+
"./finances": {
|
|
56
|
+
"import": "./src/finances.js",
|
|
57
|
+
"types": "./src/finances.d.ts"
|
|
58
|
+
},
|
|
59
|
+
"./finances-tui": {
|
|
60
|
+
"import": "./src/finances-tui.js"
|
|
61
|
+
},
|
|
62
|
+
"./finances-reports": {
|
|
63
|
+
"types": "./src/finances-reports.d.ts",
|
|
64
|
+
"import": "./src/finances-reports.js",
|
|
65
|
+
"default": "./src/finances-reports.js"
|
|
66
|
+
},
|
|
67
|
+
"./finances-commands": {
|
|
68
|
+
"import": "./src/finances-commands.js",
|
|
69
|
+
"default": "./src/finances-commands.js"
|
|
70
|
+
},
|
|
71
|
+
"./finances-report": {
|
|
72
|
+
"import": "./src/finances-report.js",
|
|
73
|
+
"default": "./src/finances-report.js"
|
|
51
74
|
}
|
|
52
75
|
},
|
|
53
76
|
"files": [
|
|
@@ -86,7 +109,12 @@
|
|
|
86
109
|
"payment-gateway",
|
|
87
110
|
"merchant",
|
|
88
111
|
"bip39",
|
|
89
|
-
"mnemonic"
|
|
112
|
+
"mnemonic",
|
|
113
|
+
"finances",
|
|
114
|
+
"tui",
|
|
115
|
+
"dashboard",
|
|
116
|
+
"plaid",
|
|
117
|
+
"simplefin"
|
|
90
118
|
],
|
|
91
119
|
"author": "Profullstack, Inc.",
|
|
92
120
|
"license": "MIT",
|
|
@@ -103,16 +131,16 @@
|
|
|
103
131
|
"node": ">=20.0.0"
|
|
104
132
|
},
|
|
105
133
|
"devDependencies": {
|
|
106
|
-
"vitest": "^4.1.0",
|
|
107
134
|
"eslint": "^8.57.0",
|
|
108
|
-
"prettier": "^3.4.1"
|
|
135
|
+
"prettier": "^3.4.1",
|
|
136
|
+
"vitest": "^4.1.11"
|
|
109
137
|
},
|
|
110
|
-
"peerDependencies": {},
|
|
111
138
|
"dependencies": {
|
|
112
|
-
"@
|
|
113
|
-
"@scure/bip32": "^1.5.0",
|
|
139
|
+
"@inquirer/prompts": "^8.3.0",
|
|
114
140
|
"@noble/curves": "^1.6.0",
|
|
115
141
|
"@noble/hashes": "^1.5.0",
|
|
116
|
-
"@
|
|
142
|
+
"@profullstack/hqtui": "^0.1.11",
|
|
143
|
+
"@scure/bip32": "^1.5.0",
|
|
144
|
+
"@scure/bip39": "^1.4.0"
|
|
117
145
|
}
|
|
118
146
|
}
|
package/src/card-payments.js
CHANGED
|
@@ -309,13 +309,21 @@ export async function createCardEscrow(client, businessId, amountUSD, descriptio
|
|
|
309
309
|
// Convert USD to cents
|
|
310
310
|
const amountCents = Math.round(amountUSD * 100);
|
|
311
311
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
312
|
+
// `CoinPayClient.request(endpoint, options)` takes TWO arguments. Every call
|
|
313
|
+
// in this file passed three, in the shape `request(method, path, body)` — so
|
|
314
|
+
// `endpoint` received 'POST', the URL became `baseUrl + 'POST'`, and `options`
|
|
315
|
+
// received the path as a string. These calls failed 100% of the time, which
|
|
316
|
+
// made releasing or refunding a card escrow unreachable through the SDK.
|
|
317
|
+
return client.request('/api/stripe/payments/create', {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
body: JSON.stringify({
|
|
320
|
+
businessId,
|
|
321
|
+
amount: amountCents,
|
|
322
|
+
currency: 'usd',
|
|
323
|
+
description,
|
|
324
|
+
metadata,
|
|
325
|
+
escrowMode: true,
|
|
326
|
+
}),
|
|
319
327
|
});
|
|
320
328
|
}
|
|
321
329
|
|
|
@@ -346,7 +354,7 @@ export async function listCardEscrows(client, options = {}) {
|
|
|
346
354
|
const queryString = queryParams.toString();
|
|
347
355
|
const url = `/api/stripe/escrows${queryString ? `?${queryString}` : ''}`;
|
|
348
356
|
|
|
349
|
-
return client.request('GET'
|
|
357
|
+
return client.request(url, { method: 'GET' });
|
|
350
358
|
}
|
|
351
359
|
|
|
352
360
|
/**
|
|
@@ -360,8 +368,9 @@ export async function listCardEscrows(client, options = {}) {
|
|
|
360
368
|
* const result = await releaseCardEscrow(client, 'escrow-123');
|
|
361
369
|
*/
|
|
362
370
|
export async function releaseCardEscrow(client, escrowId) {
|
|
363
|
-
return client.request('
|
|
364
|
-
|
|
371
|
+
return client.request('/api/stripe/escrow/release', {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
body: JSON.stringify({ escrowId }),
|
|
365
374
|
});
|
|
366
375
|
}
|
|
367
376
|
|
|
@@ -386,9 +395,9 @@ export async function releaseCardEscrow(client, escrowId) {
|
|
|
386
395
|
* });
|
|
387
396
|
*/
|
|
388
397
|
export async function refundCardEscrow(client, escrowId, options = {}) {
|
|
389
|
-
return client.request('
|
|
390
|
-
|
|
391
|
-
...options,
|
|
398
|
+
return client.request('/api/stripe/escrow/refund', {
|
|
399
|
+
method: 'POST',
|
|
400
|
+
body: JSON.stringify({ escrowId, ...options }),
|
|
392
401
|
});
|
|
393
402
|
}
|
|
394
403
|
|
|
@@ -404,7 +413,7 @@ export async function refundCardEscrow(client, escrowId, options = {}) {
|
|
|
404
413
|
* console.log(status.escrow_status); // 'pending', 'released', 'refunded'
|
|
405
414
|
*/
|
|
406
415
|
export async function getCardEscrowStatus(client, escrowId) {
|
|
407
|
-
return client.request(
|
|
416
|
+
return client.request(`/api/stripe/transactions/${escrowId}`, { method: 'GET' });
|
|
408
417
|
}
|
|
409
418
|
|
|
410
419
|
/**
|
package/src/client.d.ts
CHANGED
|
@@ -313,6 +313,16 @@ export class CoinPayClient {
|
|
|
313
313
|
* @param eventType - Event type to simulate (default: `'payment.completed'`)
|
|
314
314
|
*/
|
|
315
315
|
testWebhook(businessId: string, eventType?: string): Promise<Record<string, unknown>>;
|
|
316
|
+
/** Fetch a binary body (PDF, CSV) with its describing headers. */
|
|
317
|
+
requestBinary(endpoint: string, options?: RequestInit): Promise<{
|
|
318
|
+
bytes: Uint8Array;
|
|
319
|
+
contentType: string;
|
|
320
|
+
filename: string | null;
|
|
321
|
+
sha256: string | null;
|
|
322
|
+
headers: Record<string, string>;
|
|
323
|
+
}>;
|
|
324
|
+
/** POST a multipart form (file upload). */
|
|
325
|
+
requestForm(endpoint: string, form: FormData): Promise<any>;
|
|
316
326
|
}
|
|
317
327
|
|
|
318
328
|
export default CoinPayClient;
|
package/src/client.js
CHANGED
|
@@ -77,6 +77,97 @@ export class CoinPayClient {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Fetch a binary body (a PDF, a CSV) rather than JSON.
|
|
82
|
+
*
|
|
83
|
+
* `request()` always parses JSON, which a file download is not. This
|
|
84
|
+
* returns the bytes plus the headers that describe them; an error body
|
|
85
|
+
* is still parsed as JSON so its `code` survives.
|
|
86
|
+
* @param {string} endpoint - API endpoint
|
|
87
|
+
* @param {Object} [options] - Fetch options
|
|
88
|
+
* @returns {Promise<{bytes: Uint8Array, contentType: string, filename: string|null, sha256: string|null, headers: Record<string,string>}>}
|
|
89
|
+
*/
|
|
90
|
+
async requestBinary(endpoint, options = {}) {
|
|
91
|
+
if (!this.#apiKey) {
|
|
92
|
+
throw new Error('API key is required for authenticated requests.');
|
|
93
|
+
}
|
|
94
|
+
const url = `${this.#baseUrl}${endpoint}`;
|
|
95
|
+
const controller = new AbortController();
|
|
96
|
+
const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(url, {
|
|
99
|
+
...options,
|
|
100
|
+
signal: controller.signal,
|
|
101
|
+
headers: { 'Authorization': `Bearer ${this.#apiKey}`, ...options.headers },
|
|
102
|
+
});
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
let data = {};
|
|
105
|
+
try { data = await response.json(); } catch { /* not JSON */ }
|
|
106
|
+
const structured = data && data.error && typeof data.error === 'object' ? data.error : null;
|
|
107
|
+
const error = new Error((structured && structured.message) || data.error || `HTTP ${response.status}`);
|
|
108
|
+
error.status = response.status;
|
|
109
|
+
error.response = data;
|
|
110
|
+
if (structured && structured.code) error.code = structured.code;
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
const headers = {};
|
|
114
|
+
response.headers.forEach((value, key) => { headers[key] = value; });
|
|
115
|
+
const disposition = headers['content-disposition'] || '';
|
|
116
|
+
const match = /filename="?([^";]+)"?/i.exec(disposition);
|
|
117
|
+
return {
|
|
118
|
+
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
119
|
+
contentType: headers['content-type'] || 'application/octet-stream',
|
|
120
|
+
filename: match ? match[1] : null,
|
|
121
|
+
sha256: headers['x-content-sha256'] || null,
|
|
122
|
+
headers,
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error.name === 'AbortError') throw new Error(`Request timeout after ${this.#timeout}ms`);
|
|
126
|
+
throw error;
|
|
127
|
+
} finally {
|
|
128
|
+
clearTimeout(timeoutId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* POST a multipart form (a file upload). The browser/undici sets the
|
|
134
|
+
* boundary, so no Content-Type is forced here.
|
|
135
|
+
* @param {string} endpoint - API endpoint
|
|
136
|
+
* @param {FormData} form - The form to send
|
|
137
|
+
* @returns {Promise<Object>} API response
|
|
138
|
+
*/
|
|
139
|
+
async requestForm(endpoint, form) {
|
|
140
|
+
if (!this.#apiKey) {
|
|
141
|
+
throw new Error('API key is required for authenticated requests.');
|
|
142
|
+
}
|
|
143
|
+
const url = `${this.#baseUrl}${endpoint}`;
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(url, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
body: form,
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
headers: { 'Authorization': `Bearer ${this.#apiKey}` },
|
|
152
|
+
});
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
const structured = data && data.error && typeof data.error === 'object' ? data.error : null;
|
|
156
|
+
const error = new Error((structured && structured.message) || data.error || `HTTP ${response.status}`);
|
|
157
|
+
error.status = response.status;
|
|
158
|
+
error.response = data;
|
|
159
|
+
if (structured && structured.code) error.code = structured.code;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
return data;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (error.name === 'AbortError') throw new Error(`Request timeout after ${this.#timeout}ms`);
|
|
165
|
+
throw error;
|
|
166
|
+
} finally {
|
|
167
|
+
clearTimeout(timeoutId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
80
171
|
/**
|
|
81
172
|
* Make an unauthenticated API request (for registration/login)
|
|
82
173
|
* @param {string} endpoint - API endpoint
|
|
@@ -454,23 +545,61 @@ export class CoinPayClient {
|
|
|
454
545
|
|
|
455
546
|
/**
|
|
456
547
|
* Create a new business
|
|
548
|
+
*
|
|
549
|
+
* Field names are snake_case because that is what the API reads. This used to
|
|
550
|
+
* send `webhookUrl`, which POST /api/businesses does not look at -- it reads
|
|
551
|
+
* `input.webhook_url` -- so every business created through the SDK came back
|
|
552
|
+
* with a null webhook_url and no webhook secret, silently. The value was
|
|
553
|
+
* accepted, echoed nowhere, and dropped.
|
|
554
|
+
*
|
|
555
|
+
* `category` is required by the API and was not sent at all, so
|
|
556
|
+
* `coinpay business create` failed outright with "Select a valid business
|
|
557
|
+
* category" and there was no flag to supply one. Valid slugs live in
|
|
558
|
+
* src/lib/business/taxonomy.ts.
|
|
559
|
+
*
|
|
457
560
|
* @param {Object} params - Business parameters
|
|
458
561
|
* @param {string} params.name - Business name
|
|
562
|
+
* @param {string} params.category - Taxonomy slug; required by the API
|
|
563
|
+
* @param {string} [params.description]
|
|
459
564
|
* @param {string} [params.webhookUrl] - Webhook URL
|
|
565
|
+
* @param {string} [params.webhookSecret] - Supply one, or the API generates it
|
|
460
566
|
* @param {Object} [params.walletAddresses] - Wallet addresses by chain
|
|
461
567
|
* @returns {Promise<Object>} Created business
|
|
462
568
|
*/
|
|
463
|
-
async createBusiness({
|
|
569
|
+
async createBusiness({
|
|
570
|
+
name,
|
|
571
|
+
category,
|
|
572
|
+
description,
|
|
573
|
+
webhookUrl,
|
|
574
|
+
webhookSecret,
|
|
575
|
+
walletAddresses,
|
|
576
|
+
}) {
|
|
464
577
|
return this.request('/businesses', {
|
|
465
578
|
method: 'POST',
|
|
466
579
|
body: JSON.stringify({
|
|
467
580
|
name,
|
|
468
|
-
|
|
581
|
+
category,
|
|
582
|
+
description,
|
|
583
|
+
webhook_url: webhookUrl,
|
|
584
|
+
webhook_secret: webhookSecret,
|
|
469
585
|
walletAddresses,
|
|
470
586
|
}),
|
|
471
587
|
});
|
|
472
588
|
}
|
|
473
589
|
|
|
590
|
+
/**
|
|
591
|
+
* The taxonomy a business may be created with.
|
|
592
|
+
*
|
|
593
|
+
* Fetched rather than bundled: a copy inside the SDK goes stale the moment a
|
|
594
|
+
* category is added, and a stale copy is how "Select a valid business category"
|
|
595
|
+
* happens a second time.
|
|
596
|
+
*
|
|
597
|
+
* @returns {Promise<Object>} { success, categories: [{ slug, label, group, baseRisk }] }
|
|
598
|
+
*/
|
|
599
|
+
async listBusinessCategories() {
|
|
600
|
+
return this.request('/businesses/categories', { method: 'GET' });
|
|
601
|
+
}
|
|
602
|
+
|
|
474
603
|
/**
|
|
475
604
|
* Update business
|
|
476
605
|
* @param {string} businessId - Business ID
|