@profullstack/coinpay 0.6.7 → 0.6.10
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/bin/coinpay.js +282 -0
- package/package.json +1 -1
- package/src/auth.d.ts +72 -0
- package/src/auth.js +58 -0
- package/src/client.js +52 -6
- package/src/index.js +49 -0
- package/src/lightning.js +156 -0
- package/src/x402.js +585 -0
package/bin/coinpay.js
CHANGED
|
@@ -119,6 +119,13 @@ function createClient() {
|
|
|
119
119
|
return new CoinPayClient({ apiKey, baseUrl: getBaseUrl() });
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Create unauthenticated client for auth operations
|
|
124
|
+
*/
|
|
125
|
+
function createUnauthenticatedClient() {
|
|
126
|
+
return new CoinPayClient({ baseUrl: getBaseUrl() });
|
|
127
|
+
}
|
|
128
|
+
|
|
122
129
|
/**
|
|
123
130
|
* Parse command line arguments
|
|
124
131
|
*/
|
|
@@ -171,6 +178,11 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
171
178
|
set-url <base-url> Set custom API URL
|
|
172
179
|
show Show current configuration
|
|
173
180
|
|
|
181
|
+
${colors.bright}auth${colors.reset}
|
|
182
|
+
register Register new merchant account
|
|
183
|
+
login Login to merchant account
|
|
184
|
+
me Show current merchant info
|
|
185
|
+
|
|
174
186
|
${colors.bright}payment${colors.reset}
|
|
175
187
|
create Create a new payment
|
|
176
188
|
get <id> Get payment details
|
|
@@ -241,6 +253,8 @@ ${colors.cyan}Commands:${colors.reset}
|
|
|
241
253
|
badge [did] Get embeddable reputation badge URL
|
|
242
254
|
verify <id> Verify a credential
|
|
243
255
|
revocations List revoked credentials
|
|
256
|
+
did claim Claim (auto-generate) a new DID
|
|
257
|
+
did me Show your current DID
|
|
244
258
|
issuer register Register platform issuer (--name, --domain)
|
|
245
259
|
issuer list List your platform issuers
|
|
246
260
|
issuer rotate Rotate issuer API key (--id)
|
|
@@ -1715,6 +1729,136 @@ async function handleEscrow(subcommand, args, flags) {
|
|
|
1715
1729
|
}
|
|
1716
1730
|
}
|
|
1717
1731
|
|
|
1732
|
+
/**
|
|
1733
|
+
* Auth commands
|
|
1734
|
+
*/
|
|
1735
|
+
async function handleAuth(subcommand, args, flags) {
|
|
1736
|
+
const config = loadConfig();
|
|
1737
|
+
|
|
1738
|
+
switch (subcommand) {
|
|
1739
|
+
case 'register': {
|
|
1740
|
+
const email = flags.email;
|
|
1741
|
+
const password = flags.password;
|
|
1742
|
+
const name = flags.name;
|
|
1743
|
+
|
|
1744
|
+
if (!email || !password) {
|
|
1745
|
+
print.error('Required: --email <email> --password <password>');
|
|
1746
|
+
print.info('Example: coinpay auth register --email user@example.com --password mysecret --name "My Company"');
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
try {
|
|
1751
|
+
const client = createUnauthenticatedClient();
|
|
1752
|
+
const { registerMerchant } = await import('../src/auth.js');
|
|
1753
|
+
const result = await registerMerchant(client, { email, password, name });
|
|
1754
|
+
|
|
1755
|
+
if (result.success) {
|
|
1756
|
+
// Store the JWT token in config
|
|
1757
|
+
config.jwtToken = result.token;
|
|
1758
|
+
saveConfig(config);
|
|
1759
|
+
|
|
1760
|
+
print.success('Registration successful!');
|
|
1761
|
+
print.info(`Merchant ID: ${result.merchant.id}`);
|
|
1762
|
+
print.info(`Email: ${result.merchant.email}`);
|
|
1763
|
+
if (result.merchant.name) {
|
|
1764
|
+
print.info(`Name: ${result.merchant.name}`);
|
|
1765
|
+
}
|
|
1766
|
+
print.success('JWT token saved to config');
|
|
1767
|
+
} else {
|
|
1768
|
+
print.error(result.error || 'Registration failed');
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
if (flags.json) {
|
|
1772
|
+
print.json(result);
|
|
1773
|
+
}
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
print.error(error.message);
|
|
1776
|
+
}
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
case 'login': {
|
|
1781
|
+
const email = flags.email;
|
|
1782
|
+
const password = flags.password;
|
|
1783
|
+
|
|
1784
|
+
if (!email || !password) {
|
|
1785
|
+
print.error('Required: --email <email> --password <password>');
|
|
1786
|
+
print.info('Example: coinpay auth login --email user@example.com --password mysecret');
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
try {
|
|
1791
|
+
const client = createUnauthenticatedClient();
|
|
1792
|
+
const { loginMerchant } = await import('../src/auth.js');
|
|
1793
|
+
const result = await loginMerchant(client, { email, password });
|
|
1794
|
+
|
|
1795
|
+
if (result.success) {
|
|
1796
|
+
// Store the JWT token in config
|
|
1797
|
+
config.jwtToken = result.token;
|
|
1798
|
+
saveConfig(config);
|
|
1799
|
+
|
|
1800
|
+
print.success('Login successful!');
|
|
1801
|
+
print.info(`Merchant ID: ${result.merchant.id}`);
|
|
1802
|
+
print.info(`Email: ${result.merchant.email}`);
|
|
1803
|
+
if (result.merchant.name) {
|
|
1804
|
+
print.info(`Name: ${result.merchant.name}`);
|
|
1805
|
+
}
|
|
1806
|
+
print.success('JWT token saved to config');
|
|
1807
|
+
} else {
|
|
1808
|
+
print.error(result.error || 'Login failed');
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (flags.json) {
|
|
1812
|
+
print.json(result);
|
|
1813
|
+
}
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
print.error(error.message);
|
|
1816
|
+
}
|
|
1817
|
+
break;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
case 'me': {
|
|
1821
|
+
try {
|
|
1822
|
+
// Check if we have a JWT token
|
|
1823
|
+
const jwtToken = config.jwtToken;
|
|
1824
|
+
if (!jwtToken) {
|
|
1825
|
+
print.error('Not authenticated. Run: coinpay auth login');
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// Create client with JWT token instead of API key
|
|
1830
|
+
const client = new CoinPayClient({ apiKey: jwtToken, baseUrl: getBaseUrl() });
|
|
1831
|
+
const { getMe } = await import('../src/auth.js');
|
|
1832
|
+
const result = await getMe(client);
|
|
1833
|
+
|
|
1834
|
+
print.success('Current merchant:');
|
|
1835
|
+
print.info(`ID: ${result.id}`);
|
|
1836
|
+
print.info(`Email: ${result.email}`);
|
|
1837
|
+
if (result.name) {
|
|
1838
|
+
print.info(`Name: ${result.name}`);
|
|
1839
|
+
}
|
|
1840
|
+
print.info(`Created: ${result.created_at}`);
|
|
1841
|
+
|
|
1842
|
+
if (flags.json) {
|
|
1843
|
+
print.json(result);
|
|
1844
|
+
}
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
if (error.message.includes('401') || error.message.includes('403')) {
|
|
1847
|
+
print.error('Authentication failed. Please run: coinpay auth login');
|
|
1848
|
+
} else {
|
|
1849
|
+
print.error(error.message);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
break;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
default:
|
|
1856
|
+
print.error(`Unknown auth command: ${subcommand}`);
|
|
1857
|
+
print.info('Available: register, login, me');
|
|
1858
|
+
process.exit(1);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1718
1862
|
/**
|
|
1719
1863
|
* Reputation commands
|
|
1720
1864
|
*/
|
|
@@ -1896,6 +2040,21 @@ async function handleReputation(subcommand, args, flags) {
|
|
|
1896
2040
|
print.error(result.error || 'Failed to link DID');
|
|
1897
2041
|
}
|
|
1898
2042
|
|
|
2043
|
+
if (flags.json) print.json(result);
|
|
2044
|
+
} else if (didSubcommand === 'me') {
|
|
2045
|
+
// Show current DID
|
|
2046
|
+
const { getMyDid } = await import('../src/reputation.js');
|
|
2047
|
+
const result = await getMyDid(client);
|
|
2048
|
+
|
|
2049
|
+
if (result.did) {
|
|
2050
|
+
print.success(`Your DID: ${result.did}`);
|
|
2051
|
+
console.log(` Public Key: ${result.public_key}`);
|
|
2052
|
+
console.log(` Verified: ${result.verified}`);
|
|
2053
|
+
console.log(` Created: ${result.created_at}`);
|
|
2054
|
+
} else {
|
|
2055
|
+
print.info('No DID found. Run: coinpay reputation did claim');
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1899
2058
|
if (flags.json) print.json(result);
|
|
1900
2059
|
} else {
|
|
1901
2060
|
// Default: show current DID
|
|
@@ -2449,9 +2608,17 @@ async function main() {
|
|
|
2449
2608
|
await handleCard(subcommand, args, flags);
|
|
2450
2609
|
break;
|
|
2451
2610
|
|
|
2611
|
+
case 'auth':
|
|
2612
|
+
await handleAuth(subcommand, args, flags);
|
|
2613
|
+
break;
|
|
2614
|
+
|
|
2452
2615
|
case 'reputation':
|
|
2453
2616
|
await handleReputation(subcommand, args, flags);
|
|
2454
2617
|
break;
|
|
2618
|
+
|
|
2619
|
+
case 'x402':
|
|
2620
|
+
await handleX402(subcommand, args, flags);
|
|
2621
|
+
break;
|
|
2455
2622
|
|
|
2456
2623
|
default:
|
|
2457
2624
|
print.error(`Unknown command: ${command}`);
|
|
@@ -2467,4 +2634,119 @@ async function main() {
|
|
|
2467
2634
|
}
|
|
2468
2635
|
}
|
|
2469
2636
|
|
|
2637
|
+
/**
|
|
2638
|
+
* Handle x402 commands
|
|
2639
|
+
*/
|
|
2640
|
+
async function handleX402(subcommand, args, flags) {
|
|
2641
|
+
const config = loadConfig();
|
|
2642
|
+
|
|
2643
|
+
if (!subcommand) {
|
|
2644
|
+
console.log(`
|
|
2645
|
+
${colors.bright}x402 Payment Protocol${colors.reset}
|
|
2646
|
+
|
|
2647
|
+
Usage: coinpay x402 <command> [options]
|
|
2648
|
+
|
|
2649
|
+
Commands:
|
|
2650
|
+
status Check x402 facilitator status
|
|
2651
|
+
test Test x402 flow against an endpoint
|
|
2652
|
+
|
|
2653
|
+
Options:
|
|
2654
|
+
--url <url> Endpoint URL for testing
|
|
2655
|
+
--network <net> Network: base, ethereum, polygon (default: base)
|
|
2656
|
+
--amount <amt> Amount in USDC smallest unit (default: 1000000 = 1 USDC)
|
|
2657
|
+
`);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
switch (subcommand) {
|
|
2662
|
+
case 'status': {
|
|
2663
|
+
const apiKey = flags['api-key'] || config.apiKey;
|
|
2664
|
+
const baseUrl = flags['base-url'] || config.baseUrl || 'https://coinpayportal.com';
|
|
2665
|
+
|
|
2666
|
+
print.info('Checking x402 facilitator status...');
|
|
2667
|
+
|
|
2668
|
+
try {
|
|
2669
|
+
const response = await fetch(`${baseUrl}/api/x402/verify`, {
|
|
2670
|
+
method: 'POST',
|
|
2671
|
+
headers: {
|
|
2672
|
+
'Content-Type': 'application/json',
|
|
2673
|
+
...(apiKey ? { 'X-API-Key': apiKey } : {}),
|
|
2674
|
+
},
|
|
2675
|
+
body: JSON.stringify({ payment: { test: true } }),
|
|
2676
|
+
});
|
|
2677
|
+
|
|
2678
|
+
if (response.status === 401) {
|
|
2679
|
+
print.error('API key required or invalid. Set with: coinpay config set apiKey <key>');
|
|
2680
|
+
} else if (response.status === 400) {
|
|
2681
|
+
print.success('x402 facilitator is reachable and responding');
|
|
2682
|
+
print.info(`Facilitator URL: ${baseUrl}/api/x402`);
|
|
2683
|
+
print.info('Networks: Base, Ethereum, Polygon (Solana coming soon)');
|
|
2684
|
+
} else {
|
|
2685
|
+
print.warn(`Unexpected response: HTTP ${response.status}`);
|
|
2686
|
+
}
|
|
2687
|
+
} catch (err) {
|
|
2688
|
+
print.error(`Cannot reach facilitator at ${baseUrl}: ${err.message}`);
|
|
2689
|
+
}
|
|
2690
|
+
break;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
case 'test': {
|
|
2694
|
+
const url = flags.url || args[0];
|
|
2695
|
+
if (!url) {
|
|
2696
|
+
print.error('URL required: coinpay x402 test --url http://localhost:3000/api/premium');
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
const network = flags.network || 'base';
|
|
2701
|
+
const amount = flags.amount || '1000000';
|
|
2702
|
+
|
|
2703
|
+
print.info(`Testing x402 flow against ${url}`);
|
|
2704
|
+
|
|
2705
|
+
// Step 1: Request without payment
|
|
2706
|
+
try {
|
|
2707
|
+
print.info('Step 1: Requesting resource without payment...');
|
|
2708
|
+
const response = await fetch(url);
|
|
2709
|
+
|
|
2710
|
+
if (response.status === 402) {
|
|
2711
|
+
const body = await response.json();
|
|
2712
|
+
print.success('Received HTTP 402 Payment Required');
|
|
2713
|
+
|
|
2714
|
+
if (body.x402Version) {
|
|
2715
|
+
print.success(`x402 version: ${body.x402Version}`);
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
if (body.accepts && body.accepts.length > 0) {
|
|
2719
|
+
print.success(`${body.accepts.length} payment method(s) offered:`);
|
|
2720
|
+
for (const accept of body.accepts) {
|
|
2721
|
+
const label = accept.extra?.label || `${accept.asset} on ${accept.network}`;
|
|
2722
|
+
const symbol = accept.extra?.assetSymbol || accept.asset || '';
|
|
2723
|
+
print.info(` • ${label}`);
|
|
2724
|
+
print.info(` Amount: ${accept.maxAmountRequired} ${symbol}`);
|
|
2725
|
+
print.info(` Pay to: ${accept.payTo || 'not specified'}`);
|
|
2726
|
+
}
|
|
2727
|
+
const facilitator = body.accepts[0]?.extra?.facilitator;
|
|
2728
|
+
if (facilitator) {
|
|
2729
|
+
print.info(` Facilitator: ${facilitator}`);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
print.success('x402 endpoint is correctly configured!');
|
|
2734
|
+
print.warn('Note: Full payment test requires a funded wallet. Use the dashboard for end-to-end testing.');
|
|
2735
|
+
} else if (response.status === 200) {
|
|
2736
|
+
print.warn('Endpoint returned 200 OK — no payment required (is x402 middleware active?)');
|
|
2737
|
+
} else {
|
|
2738
|
+
print.warn(`Unexpected status: HTTP ${response.status}`);
|
|
2739
|
+
}
|
|
2740
|
+
} catch (err) {
|
|
2741
|
+
print.error(`Failed to reach ${url}: ${err.message}`);
|
|
2742
|
+
}
|
|
2743
|
+
break;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
default:
|
|
2747
|
+
print.error(`Unknown x402 command: ${subcommand}`);
|
|
2748
|
+
print.info('Available: status, test');
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2470
2752
|
main().then(() => process.exit(0));
|
package/package.json
CHANGED
package/src/auth.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth SDK Module Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CoinPayClient } from './client.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Merchant object returned from auth operations
|
|
9
|
+
*/
|
|
10
|
+
export interface Merchant {
|
|
11
|
+
id: string;
|
|
12
|
+
email: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
created_at: string;
|
|
15
|
+
updated_at?: string;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Auth response for register/login
|
|
21
|
+
*/
|
|
22
|
+
export interface AuthResponse {
|
|
23
|
+
success: boolean;
|
|
24
|
+
merchant: Merchant;
|
|
25
|
+
token: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register params
|
|
30
|
+
*/
|
|
31
|
+
export interface RegisterParams {
|
|
32
|
+
email: string;
|
|
33
|
+
password: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Login params
|
|
39
|
+
*/
|
|
40
|
+
export interface LoginParams {
|
|
41
|
+
email: string;
|
|
42
|
+
password: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register a new merchant account
|
|
47
|
+
* @param client - CoinPay client (can be unauthenticated)
|
|
48
|
+
* @param params - Registration parameters
|
|
49
|
+
* @returns Promise with registration response
|
|
50
|
+
*/
|
|
51
|
+
export function registerMerchant(
|
|
52
|
+
client: CoinPayClient,
|
|
53
|
+
params: RegisterParams
|
|
54
|
+
): Promise<AuthResponse>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Login to merchant account
|
|
58
|
+
* @param client - CoinPay client (can be unauthenticated)
|
|
59
|
+
* @param params - Login parameters
|
|
60
|
+
* @returns Promise with login response
|
|
61
|
+
*/
|
|
62
|
+
export function loginMerchant(
|
|
63
|
+
client: CoinPayClient,
|
|
64
|
+
params: LoginParams
|
|
65
|
+
): Promise<AuthResponse>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get current authenticated merchant info
|
|
69
|
+
* @param client - Authenticated CoinPay client
|
|
70
|
+
* @returns Promise with merchant information
|
|
71
|
+
*/
|
|
72
|
+
export function getMe(client: CoinPayClient): Promise<Merchant>;
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth SDK Module
|
|
3
|
+
*
|
|
4
|
+
* Authentication functions for registration, login, and merchant management.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { CoinPayClient } from '@profullstack/coinpay';
|
|
8
|
+
* import { registerMerchant, loginMerchant, getMe } from '@profullstack/coinpay/auth';
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register a new merchant account
|
|
13
|
+
* @param {import('./client.js').CoinPayClient} client - CoinPay client (can be unauthenticated)
|
|
14
|
+
* @param {Object} params
|
|
15
|
+
* @param {string} params.email - Merchant email
|
|
16
|
+
* @param {string} params.password - Account password
|
|
17
|
+
* @param {string} [params.name] - Optional merchant name
|
|
18
|
+
* @returns {Promise<Object>} { success: boolean, merchant: Object, token: string }
|
|
19
|
+
*/
|
|
20
|
+
export async function registerMerchant(client, { email, password, name }) {
|
|
21
|
+
return client.requestUnauthenticated('/auth/register', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
body: JSON.stringify({
|
|
24
|
+
email,
|
|
25
|
+
password,
|
|
26
|
+
name,
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Login to merchant account
|
|
33
|
+
* @param {import('./client.js').CoinPayClient} client - CoinPay client (can be unauthenticated)
|
|
34
|
+
* @param {Object} params
|
|
35
|
+
* @param {string} params.email - Merchant email
|
|
36
|
+
* @param {string} params.password - Account password
|
|
37
|
+
* @returns {Promise<Object>} { success: boolean, merchant: Object, token: string }
|
|
38
|
+
*/
|
|
39
|
+
export async function loginMerchant(client, { email, password }) {
|
|
40
|
+
return client.requestUnauthenticated('/auth/login', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
email,
|
|
44
|
+
password,
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get current authenticated merchant info
|
|
51
|
+
* @param {import('./client.js').CoinPayClient} client - Authenticated CoinPay client
|
|
52
|
+
* @returns {Promise<Object>} Merchant information
|
|
53
|
+
*/
|
|
54
|
+
export async function getMe(client) {
|
|
55
|
+
return client.request('/auth/me', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
});
|
|
58
|
+
}
|
package/src/client.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Main client class for interacting with the CoinPay API
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { LightningClient } from './lightning.js';
|
|
7
|
+
|
|
6
8
|
const DEFAULT_BASE_URL = 'https://coinpayportal.com/api';
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -16,18 +18,17 @@ export class CoinPayClient {
|
|
|
16
18
|
/**
|
|
17
19
|
* Create a new CoinPay client
|
|
18
20
|
* @param {Object} options - Client options
|
|
19
|
-
* @param {string} options.apiKey - Your CoinPay API key
|
|
21
|
+
* @param {string} [options.apiKey] - Your CoinPay API key (optional for auth operations)
|
|
20
22
|
* @param {string} [options.baseUrl] - API base URL (default: https://coinpayportal.com/api)
|
|
21
23
|
* @param {number} [options.timeout] - Request timeout in ms (default: 30000)
|
|
22
24
|
*/
|
|
23
|
-
constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, timeout = 30000 }) {
|
|
24
|
-
if (!apiKey) {
|
|
25
|
-
throw new Error('API key is required');
|
|
26
|
-
}
|
|
27
|
-
|
|
25
|
+
constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, timeout = 30000 } = {}) {
|
|
28
26
|
this.#apiKey = apiKey;
|
|
29
27
|
this.#baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
30
28
|
this.#timeout = timeout;
|
|
29
|
+
|
|
30
|
+
/** @type {LightningClient} Lightning/BOLT12 operations */
|
|
31
|
+
this.lightning = new LightningClient(this);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -37,6 +38,10 @@ export class CoinPayClient {
|
|
|
37
38
|
* @returns {Promise<Object>} API response
|
|
38
39
|
*/
|
|
39
40
|
async request(endpoint, options = {}) {
|
|
41
|
+
if (!this.#apiKey) {
|
|
42
|
+
throw new Error('API key is required for authenticated requests. Use requestUnauthenticated() for registration/login.');
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
const url = `${this.#baseUrl}${endpoint}`;
|
|
41
46
|
const controller = new AbortController();
|
|
42
47
|
const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
|
|
@@ -72,6 +77,47 @@ export class CoinPayClient {
|
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Make an unauthenticated API request (for registration/login)
|
|
82
|
+
* @param {string} endpoint - API endpoint
|
|
83
|
+
* @param {Object} [options] - Fetch options
|
|
84
|
+
* @returns {Promise<Object>} API response
|
|
85
|
+
*/
|
|
86
|
+
async requestUnauthenticated(endpoint, options = {}) {
|
|
87
|
+
const url = `${this.#baseUrl}${endpoint}`;
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timeoutId = setTimeout(() => controller.abort(), this.#timeout);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(url, {
|
|
93
|
+
...options,
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
headers: {
|
|
96
|
+
'Content-Type': 'application/json',
|
|
97
|
+
...options.headers,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const error = new Error(data.error || `HTTP ${response.status}`);
|
|
105
|
+
error.status = response.status;
|
|
106
|
+
error.response = data;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return data;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error.name === 'AbortError') {
|
|
113
|
+
throw new Error(`Request timeout after ${this.#timeout}ms`);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
} finally {
|
|
117
|
+
clearTimeout(timeoutId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
75
121
|
/**
|
|
76
122
|
* Create a new payment
|
|
77
123
|
*
|
package/src/index.js
CHANGED
|
@@ -88,8 +88,18 @@ import {
|
|
|
88
88
|
getBadgeUrl,
|
|
89
89
|
verifyCredential,
|
|
90
90
|
getRevocationList,
|
|
91
|
+
claimDid,
|
|
92
|
+
linkDid,
|
|
93
|
+
getMyDid,
|
|
91
94
|
} from './reputation.js';
|
|
92
95
|
|
|
96
|
+
// Auth exports
|
|
97
|
+
import {
|
|
98
|
+
registerMerchant,
|
|
99
|
+
loginMerchant,
|
|
100
|
+
getMe,
|
|
101
|
+
} from './auth.js';
|
|
102
|
+
|
|
93
103
|
import {
|
|
94
104
|
SwapClient,
|
|
95
105
|
SwapCoins,
|
|
@@ -137,6 +147,21 @@ import {
|
|
|
137
147
|
SubscriptionStatus,
|
|
138
148
|
} from './subscriptions.js';
|
|
139
149
|
|
|
150
|
+
// Lightning
|
|
151
|
+
import { LightningClient } from './lightning.js';
|
|
152
|
+
|
|
153
|
+
// x402 Protocol
|
|
154
|
+
import {
|
|
155
|
+
createX402Middleware,
|
|
156
|
+
verifyX402Payment,
|
|
157
|
+
settleX402Payment,
|
|
158
|
+
buildPaymentRequired,
|
|
159
|
+
convertUsdToAssetAmount,
|
|
160
|
+
PAYMENT_METHODS,
|
|
161
|
+
USDC_CONTRACTS,
|
|
162
|
+
CHAIN_IDS,
|
|
163
|
+
} from './x402.js';
|
|
164
|
+
|
|
140
165
|
export {
|
|
141
166
|
// Client
|
|
142
167
|
CoinPayClient,
|
|
@@ -216,8 +241,32 @@ export {
|
|
|
216
241
|
submitReceipt,
|
|
217
242
|
getReputation,
|
|
218
243
|
getCredential,
|
|
244
|
+
getCredentials,
|
|
245
|
+
getReceipts,
|
|
246
|
+
getBadgeUrl,
|
|
219
247
|
verifyCredential,
|
|
220
248
|
getRevocationList,
|
|
249
|
+
claimDid,
|
|
250
|
+
linkDid,
|
|
251
|
+
getMyDid,
|
|
252
|
+
|
|
253
|
+
// Lightning
|
|
254
|
+
LightningClient,
|
|
255
|
+
|
|
256
|
+
// Auth
|
|
257
|
+
registerMerchant,
|
|
258
|
+
loginMerchant,
|
|
259
|
+
getMe,
|
|
260
|
+
|
|
261
|
+
// x402 Protocol
|
|
262
|
+
createX402Middleware,
|
|
263
|
+
verifyX402Payment,
|
|
264
|
+
settleX402Payment,
|
|
265
|
+
buildPaymentRequired,
|
|
266
|
+
convertUsdToAssetAmount,
|
|
267
|
+
PAYMENT_METHODS,
|
|
268
|
+
USDC_CONTRACTS,
|
|
269
|
+
CHAIN_IDS,
|
|
221
270
|
|
|
222
271
|
// Webhook utilities
|
|
223
272
|
verifyWebhookSignature,
|
package/src/lightning.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightning Module for CoinPay SDK
|
|
3
|
+
*
|
|
4
|
+
* Manages BOLT12 Lightning Network operations: node provisioning,
|
|
5
|
+
* offer creation, and payment tracking.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BASE_URL = 'https://coinpayportal.com/api';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lightning client for BOLT12 operations
|
|
12
|
+
*/
|
|
13
|
+
export class LightningClient {
|
|
14
|
+
#client;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('./client.js').CoinPayClient} client - Parent CoinPay client
|
|
18
|
+
*/
|
|
19
|
+
constructor(client) {
|
|
20
|
+
this.#client = client;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ──────────────────────────────────────────────
|
|
24
|
+
// Nodes
|
|
25
|
+
// ──────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Provision a Greenlight CLN node for a wallet.
|
|
29
|
+
* @param {Object} params
|
|
30
|
+
* @param {string} params.wallet_id - Wallet UUID
|
|
31
|
+
* @param {string} params.mnemonic - BIP39 mnemonic (used server-side to derive LN keys)
|
|
32
|
+
* @param {string} [params.business_id] - Optional business UUID
|
|
33
|
+
* @returns {Promise<Object>} The provisioned node
|
|
34
|
+
*/
|
|
35
|
+
async provisionNode({ wallet_id, mnemonic, business_id }) {
|
|
36
|
+
return this.#client.request('/lightning/nodes', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: JSON.stringify({ wallet_id, mnemonic, business_id }),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get node status by ID.
|
|
44
|
+
* @param {string} nodeId
|
|
45
|
+
* @returns {Promise<Object>}
|
|
46
|
+
*/
|
|
47
|
+
async getNode(nodeId) {
|
|
48
|
+
return this.#client.request(`/lightning/nodes/${nodeId}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get node by wallet ID.
|
|
53
|
+
* @param {string} walletId - Wallet UUID
|
|
54
|
+
* @returns {Promise<Object>}
|
|
55
|
+
*/
|
|
56
|
+
async getNodeByWallet(walletId) {
|
|
57
|
+
return this.#client.request(`/lightning/nodes?wallet_id=${walletId}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ──────────────────────────────────────────────
|
|
61
|
+
// Offers
|
|
62
|
+
// ──────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a BOLT12 offer.
|
|
66
|
+
* @param {Object} params
|
|
67
|
+
* @param {string} params.node_id - Node UUID
|
|
68
|
+
* @param {string} params.description - Human-readable description
|
|
69
|
+
* @param {number} [params.amount_msat] - Fixed amount in millisatoshis (omit for any-amount)
|
|
70
|
+
* @param {string} [params.currency] - Currency code (default: "BTC")
|
|
71
|
+
* @param {string} [params.business_id] - Optional business UUID
|
|
72
|
+
* @returns {Promise<Object>}
|
|
73
|
+
*/
|
|
74
|
+
async createOffer({ node_id, description, amount_msat, currency, business_id }) {
|
|
75
|
+
return this.#client.request('/lightning/offers', {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
body: JSON.stringify({ node_id, description, amount_msat, currency, business_id }),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get offer by ID.
|
|
83
|
+
* @param {string} offerId
|
|
84
|
+
* @returns {Promise<Object>} Includes offer and qr_uri
|
|
85
|
+
*/
|
|
86
|
+
async getOffer(offerId) {
|
|
87
|
+
return this.#client.request(`/lightning/offers/${offerId}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* List offers with optional filters.
|
|
92
|
+
* @param {Object} [params]
|
|
93
|
+
* @param {string} [params.business_id]
|
|
94
|
+
* @param {string} [params.node_id]
|
|
95
|
+
* @param {string} [params.status] - "active" | "disabled" | "archived"
|
|
96
|
+
* @param {number} [params.limit] - Default 20
|
|
97
|
+
* @param {number} [params.offset] - Default 0
|
|
98
|
+
* @returns {Promise<Object>} { offers, total, limit, offset }
|
|
99
|
+
*/
|
|
100
|
+
async listOffers(params = {}) {
|
|
101
|
+
const qs = new URLSearchParams();
|
|
102
|
+
for (const [k, v] of Object.entries(params)) {
|
|
103
|
+
if (v !== undefined && v !== null) qs.set(k, String(v));
|
|
104
|
+
}
|
|
105
|
+
return this.#client.request(`/lightning/offers?${qs}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ──────────────────────────────────────────────
|
|
109
|
+
// Payments
|
|
110
|
+
// ──────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* List Lightning payments.
|
|
114
|
+
* @param {Object} [params]
|
|
115
|
+
* @param {string} [params.business_id]
|
|
116
|
+
* @param {string} [params.node_id]
|
|
117
|
+
* @param {string} [params.offer_id]
|
|
118
|
+
* @param {string} [params.status] - "pending" | "settled" | "failed"
|
|
119
|
+
* @param {number} [params.limit] - Default 50
|
|
120
|
+
* @param {number} [params.offset] - Default 0
|
|
121
|
+
* @returns {Promise<Object>} { payments, total, limit, offset }
|
|
122
|
+
*/
|
|
123
|
+
async listPayments(params = {}) {
|
|
124
|
+
const qs = new URLSearchParams();
|
|
125
|
+
for (const [k, v] of Object.entries(params)) {
|
|
126
|
+
if (v !== undefined && v !== null) qs.set(k, String(v));
|
|
127
|
+
}
|
|
128
|
+
return this.#client.request(`/lightning/payments?${qs}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Send a payment to a BOLT12 offer or invoice.
|
|
133
|
+
* @param {Object} params
|
|
134
|
+
* @param {string} params.node_id - Node UUID
|
|
135
|
+
* @param {string} params.bolt12 - BOLT12 offer or invoice string
|
|
136
|
+
* @param {number} params.amount_sats - Amount in satoshis
|
|
137
|
+
* @returns {Promise<Object>}
|
|
138
|
+
*/
|
|
139
|
+
async sendPayment({ node_id, bolt12, amount_sats }) {
|
|
140
|
+
return this.#client.request('/lightning/payments', {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
body: JSON.stringify({ node_id, bolt12, amount_sats }),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get payment status by payment hash.
|
|
148
|
+
* @param {string} paymentHash
|
|
149
|
+
* @returns {Promise<Object>}
|
|
150
|
+
*/
|
|
151
|
+
async getPayment(paymentHash) {
|
|
152
|
+
return this.#client.request(`/lightning/payments/${paymentHash}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default LightningClient;
|
package/src/x402.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 Payment Protocol Support
|
|
3
|
+
*
|
|
4
|
+
* CoinPayPortal as the first multi-chain, multi-asset x402 facilitator.
|
|
5
|
+
* Supports native crypto (BTC, ETH, SOL, POL, BCH), USDC stablecoins,
|
|
6
|
+
* Lightning (BOLT12), and Stripe fiat — all via HTTP 402.
|
|
7
|
+
*
|
|
8
|
+
* @module x402
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* All supported payment methods with their network/asset metadata.
|
|
13
|
+
*
|
|
14
|
+
* Each entry defines a scheme the facilitator can advertise in the
|
|
15
|
+
* 402 `accepts` array, letting the buyer choose their preferred method.
|
|
16
|
+
*/
|
|
17
|
+
export const PAYMENT_METHODS = {
|
|
18
|
+
// ── Native crypto ──────────────────────────────────────────────
|
|
19
|
+
btc: {
|
|
20
|
+
network: 'bitcoin',
|
|
21
|
+
asset: 'BTC',
|
|
22
|
+
scheme: 'exact',
|
|
23
|
+
decimals: 8,
|
|
24
|
+
label: 'Bitcoin',
|
|
25
|
+
},
|
|
26
|
+
bch: {
|
|
27
|
+
network: 'bitcoin-cash',
|
|
28
|
+
asset: 'BCH',
|
|
29
|
+
scheme: 'exact',
|
|
30
|
+
decimals: 8,
|
|
31
|
+
label: 'Bitcoin Cash',
|
|
32
|
+
},
|
|
33
|
+
eth: {
|
|
34
|
+
network: 'ethereum',
|
|
35
|
+
asset: 'ETH',
|
|
36
|
+
scheme: 'exact',
|
|
37
|
+
decimals: 18,
|
|
38
|
+
chainId: 1,
|
|
39
|
+
label: 'Ethereum',
|
|
40
|
+
},
|
|
41
|
+
pol: {
|
|
42
|
+
network: 'polygon',
|
|
43
|
+
asset: 'POL',
|
|
44
|
+
scheme: 'exact',
|
|
45
|
+
decimals: 18,
|
|
46
|
+
chainId: 137,
|
|
47
|
+
label: 'Polygon',
|
|
48
|
+
},
|
|
49
|
+
sol: {
|
|
50
|
+
network: 'solana',
|
|
51
|
+
asset: 'SOL',
|
|
52
|
+
scheme: 'exact',
|
|
53
|
+
decimals: 9,
|
|
54
|
+
label: 'Solana',
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// ── USDC stablecoins ──────────────────────────────────────────
|
|
58
|
+
usdc_eth: {
|
|
59
|
+
network: 'ethereum',
|
|
60
|
+
asset: 'USDC',
|
|
61
|
+
scheme: 'exact',
|
|
62
|
+
decimals: 6,
|
|
63
|
+
chainId: 1,
|
|
64
|
+
contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
65
|
+
label: 'USDC on Ethereum',
|
|
66
|
+
},
|
|
67
|
+
usdc_polygon: {
|
|
68
|
+
network: 'polygon',
|
|
69
|
+
asset: 'USDC',
|
|
70
|
+
scheme: 'exact',
|
|
71
|
+
decimals: 6,
|
|
72
|
+
chainId: 137,
|
|
73
|
+
contractAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
|
|
74
|
+
label: 'USDC on Polygon',
|
|
75
|
+
},
|
|
76
|
+
usdc_solana: {
|
|
77
|
+
network: 'solana',
|
|
78
|
+
asset: 'USDC',
|
|
79
|
+
scheme: 'exact',
|
|
80
|
+
decimals: 6,
|
|
81
|
+
contractAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
|
|
82
|
+
label: 'USDC on Solana',
|
|
83
|
+
},
|
|
84
|
+
usdc_base: {
|
|
85
|
+
network: 'base',
|
|
86
|
+
asset: 'USDC',
|
|
87
|
+
scheme: 'exact',
|
|
88
|
+
decimals: 6,
|
|
89
|
+
chainId: 8453,
|
|
90
|
+
contractAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
91
|
+
label: 'USDC on Base',
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// ── Lightning ──────────────────────────────────────────────────
|
|
95
|
+
lightning: {
|
|
96
|
+
network: 'lightning',
|
|
97
|
+
asset: 'BTC',
|
|
98
|
+
scheme: 'bolt12',
|
|
99
|
+
decimals: 0, // sats
|
|
100
|
+
label: 'Lightning (BOLT12)',
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// ── Fiat via Stripe ────────────────────────────────────────────
|
|
104
|
+
stripe: {
|
|
105
|
+
network: 'stripe',
|
|
106
|
+
asset: 'USD',
|
|
107
|
+
scheme: 'stripe-checkout',
|
|
108
|
+
decimals: 2,
|
|
109
|
+
label: 'Card (Stripe)',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* USDC contract addresses by network (convenience re-export)
|
|
115
|
+
*/
|
|
116
|
+
export const USDC_CONTRACTS = {
|
|
117
|
+
base: PAYMENT_METHODS.usdc_base.contractAddress,
|
|
118
|
+
ethereum: PAYMENT_METHODS.usdc_eth.contractAddress,
|
|
119
|
+
polygon: PAYMENT_METHODS.usdc_polygon.contractAddress,
|
|
120
|
+
solana: PAYMENT_METHODS.usdc_solana.contractAddress,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Chain IDs for EVM networks
|
|
125
|
+
*/
|
|
126
|
+
export const CHAIN_IDS = {
|
|
127
|
+
base: 8453,
|
|
128
|
+
ethereum: 1,
|
|
129
|
+
polygon: 137,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Default facilitator URL
|
|
134
|
+
*/
|
|
135
|
+
const DEFAULT_FACILITATOR_URL = 'https://coinpayportal.com/api/x402';
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* x402 protocol version
|
|
139
|
+
*/
|
|
140
|
+
const X402_VERSION = 1;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* All payment method keys
|
|
144
|
+
*/
|
|
145
|
+
const ALL_METHOD_KEYS = Object.keys(PAYMENT_METHODS);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Convert a fiat amount (USD cents or dollars) to the smallest unit for a
|
|
149
|
+
* given payment method, using a rates lookup.
|
|
150
|
+
*
|
|
151
|
+
* @param {number} amountUsd - Amount in USD (e.g. 1.00)
|
|
152
|
+
* @param {string} methodKey - Key from PAYMENT_METHODS
|
|
153
|
+
* @param {Object} rates - Map of asset→USD rate (e.g. { BTC: 65000, ETH: 3500 })
|
|
154
|
+
* @returns {string} Amount in the asset's smallest unit
|
|
155
|
+
*/
|
|
156
|
+
export function convertUsdToAssetAmount(amountUsd, methodKey, rates = {}) {
|
|
157
|
+
const method = PAYMENT_METHODS[methodKey];
|
|
158
|
+
if (!method) throw new Error(`Unknown payment method: ${methodKey}`);
|
|
159
|
+
|
|
160
|
+
if (method.asset === 'USD') {
|
|
161
|
+
// Stripe: amount in cents
|
|
162
|
+
return String(Math.round(amountUsd * 100));
|
|
163
|
+
}
|
|
164
|
+
if (method.asset === 'USDC') {
|
|
165
|
+
// USDC: 6 decimals, 1:1 with USD
|
|
166
|
+
return String(Math.round(amountUsd * 1e6));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const rate = rates[method.asset];
|
|
170
|
+
if (!rate) throw new Error(`No exchange rate for ${method.asset}`);
|
|
171
|
+
|
|
172
|
+
const assetAmount = amountUsd / rate;
|
|
173
|
+
const smallest = Math.round(assetAmount * Math.pow(10, method.decimals));
|
|
174
|
+
return String(smallest);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build a single `accepts` entry for a payment method.
|
|
179
|
+
*/
|
|
180
|
+
function buildAcceptEntry(methodKey, { payTo, amount, resource, description, mimeType, maxTimeoutSeconds, facilitatorUrl }) {
|
|
181
|
+
const method = PAYMENT_METHODS[methodKey];
|
|
182
|
+
if (!method) throw new Error(`Unknown payment method: ${methodKey}`);
|
|
183
|
+
|
|
184
|
+
const entry = {
|
|
185
|
+
scheme: method.scheme,
|
|
186
|
+
network: method.network,
|
|
187
|
+
asset: method.contractAddress || method.asset,
|
|
188
|
+
maxAmountRequired: String(amount),
|
|
189
|
+
resource: resource || '',
|
|
190
|
+
description: description || 'Payment required',
|
|
191
|
+
mimeType: mimeType || 'application/json',
|
|
192
|
+
payTo,
|
|
193
|
+
maxTimeoutSeconds: maxTimeoutSeconds || 300,
|
|
194
|
+
extra: {
|
|
195
|
+
facilitator: facilitatorUrl || DEFAULT_FACILITATOR_URL,
|
|
196
|
+
methodKey,
|
|
197
|
+
assetSymbol: method.asset,
|
|
198
|
+
label: method.label,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (method.chainId) {
|
|
203
|
+
entry.extra.chainId = method.chainId;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return entry;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build a 402 response payload advertising multiple payment options.
|
|
211
|
+
*
|
|
212
|
+
* This is CoinPayPortal's key differentiator: the `accepts` array includes
|
|
213
|
+
* every supported chain and asset, letting the buyer choose.
|
|
214
|
+
*
|
|
215
|
+
* @param {Object} options
|
|
216
|
+
* @param {string} options.payTo - Merchant wallet address (or object mapping network→address)
|
|
217
|
+
* @param {number} options.amountUsd - Price in USD
|
|
218
|
+
* @param {Object} [options.rates] - Exchange rates { BTC: 65000, ETH: 3500, ... }
|
|
219
|
+
* @param {string[]} [options.methods] - Payment method keys to include (default: all)
|
|
220
|
+
* @param {string} [options.resource] - Resource URL
|
|
221
|
+
* @param {string} [options.description] - Human-readable description
|
|
222
|
+
* @param {string} [options.mimeType='application/json'] - Response MIME type
|
|
223
|
+
* @param {number} [options.maxTimeoutSeconds=300] - Payment timeout
|
|
224
|
+
* @param {string} [options.facilitatorUrl] - Custom facilitator URL
|
|
225
|
+
* @returns {Object} x402 payment required response body
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* // Advertise all methods
|
|
229
|
+
* const body = buildPaymentRequired({
|
|
230
|
+
* payTo: { ethereum: '0x...', bitcoin: 'bc1...', solana: 'So1...' },
|
|
231
|
+
* amountUsd: 5.00,
|
|
232
|
+
* rates: { BTC: 65000, ETH: 3500, SOL: 150, POL: 0.50, BCH: 350 },
|
|
233
|
+
* });
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // Only accept USDC and Lightning
|
|
237
|
+
* const body = buildPaymentRequired({
|
|
238
|
+
* payTo: '0xMyWallet',
|
|
239
|
+
* amountUsd: 1.00,
|
|
240
|
+
* methods: ['usdc_eth', 'usdc_polygon', 'usdc_base', 'lightning'],
|
|
241
|
+
* });
|
|
242
|
+
*/
|
|
243
|
+
export function buildPaymentRequired(options) {
|
|
244
|
+
const {
|
|
245
|
+
payTo,
|
|
246
|
+
amountUsd,
|
|
247
|
+
amount, // legacy: raw amount for a single method
|
|
248
|
+
network, // legacy: single network
|
|
249
|
+
rates = {},
|
|
250
|
+
methods,
|
|
251
|
+
resource,
|
|
252
|
+
description = 'Payment required',
|
|
253
|
+
mimeType = 'application/json',
|
|
254
|
+
maxTimeoutSeconds = 300,
|
|
255
|
+
facilitatorUrl = DEFAULT_FACILITATOR_URL,
|
|
256
|
+
} = options;
|
|
257
|
+
|
|
258
|
+
// Legacy single-method mode (backwards compatible)
|
|
259
|
+
if (network && amount && !methods) {
|
|
260
|
+
const methodKey = _networkToMethodKey(network);
|
|
261
|
+
const addr = typeof payTo === 'object' ? (payTo[network] || Object.values(payTo)[0]) : payTo;
|
|
262
|
+
return {
|
|
263
|
+
x402Version: X402_VERSION,
|
|
264
|
+
accepts: [buildAcceptEntry(methodKey, {
|
|
265
|
+
payTo: addr, amount, resource, description, mimeType, maxTimeoutSeconds, facilitatorUrl,
|
|
266
|
+
})],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Multi-method mode
|
|
271
|
+
const methodKeys = methods || ALL_METHOD_KEYS;
|
|
272
|
+
const accepts = [];
|
|
273
|
+
|
|
274
|
+
for (const key of methodKeys) {
|
|
275
|
+
const method = PAYMENT_METHODS[key];
|
|
276
|
+
if (!method) continue;
|
|
277
|
+
|
|
278
|
+
// Resolve pay-to address for this network
|
|
279
|
+
let addr;
|
|
280
|
+
if (typeof payTo === 'object') {
|
|
281
|
+
addr = payTo[method.network] || payTo[key];
|
|
282
|
+
} else {
|
|
283
|
+
addr = payTo;
|
|
284
|
+
}
|
|
285
|
+
if (!addr) continue; // skip methods where merchant has no address
|
|
286
|
+
|
|
287
|
+
// Convert USD to asset amount
|
|
288
|
+
let assetAmount;
|
|
289
|
+
try {
|
|
290
|
+
if (amountUsd != null) {
|
|
291
|
+
assetAmount = convertUsdToAssetAmount(amountUsd, key, rates);
|
|
292
|
+
} else if (amount) {
|
|
293
|
+
assetAmount = String(amount);
|
|
294
|
+
} else {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// No rate available for this asset — skip it
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
accepts.push(buildAcceptEntry(key, {
|
|
303
|
+
payTo: addr, amount: assetAmount, resource, description, mimeType, maxTimeoutSeconds, facilitatorUrl,
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (accepts.length === 0) {
|
|
308
|
+
throw new Error('No payment methods could be built. Check payTo addresses and rates.');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
x402Version: X402_VERSION,
|
|
313
|
+
accepts,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Map a simple network name to the best-guess method key (legacy compat).
|
|
319
|
+
*/
|
|
320
|
+
function _networkToMethodKey(network) {
|
|
321
|
+
const map = {
|
|
322
|
+
base: 'usdc_base',
|
|
323
|
+
ethereum: 'eth',
|
|
324
|
+
polygon: 'pol',
|
|
325
|
+
solana: 'sol',
|
|
326
|
+
bitcoin: 'btc',
|
|
327
|
+
'bitcoin-cash': 'bch',
|
|
328
|
+
lightning: 'lightning',
|
|
329
|
+
stripe: 'stripe',
|
|
330
|
+
};
|
|
331
|
+
return map[network] || network;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Create Express/Next.js middleware that gates routes behind x402 payments.
|
|
336
|
+
*
|
|
337
|
+
* Returns a function that accepts per-route options and returns middleware.
|
|
338
|
+
* The 402 response advertises ALL supported payment methods by default.
|
|
339
|
+
*
|
|
340
|
+
* @param {Object} globalOptions
|
|
341
|
+
* @param {string} globalOptions.apiKey - CoinPayPortal API key
|
|
342
|
+
* @param {string|Object} globalOptions.payTo - Wallet address(es): string or { network: address }
|
|
343
|
+
* @param {string[]} [globalOptions.methods] - Payment methods to accept (default: all with addresses)
|
|
344
|
+
* @param {Object} [globalOptions.rates] - Exchange rates { BTC: 65000, ... }
|
|
345
|
+
* @param {string} [globalOptions.ratesEndpoint] - URL to fetch live rates (polled periodically)
|
|
346
|
+
* @param {string} [globalOptions.description='Payment required'] - Default description
|
|
347
|
+
* @param {string} [globalOptions.facilitatorUrl] - Custom facilitator URL
|
|
348
|
+
* @param {string} [globalOptions.apiBaseUrl='https://coinpayportal.com'] - CoinPayPortal API base
|
|
349
|
+
* @returns {Function} Middleware factory: (routeOptions) => middleware
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* const x402 = createX402Middleware({
|
|
353
|
+
* apiKey: 'cp_live_xxxxx',
|
|
354
|
+
* payTo: {
|
|
355
|
+
* ethereum: '0xAbC...',
|
|
356
|
+
* bitcoin: 'bc1q...',
|
|
357
|
+
* solana: 'So1...',
|
|
358
|
+
* lightning: 'lno1...',
|
|
359
|
+
* stripe: 'acct_xxx',
|
|
360
|
+
* },
|
|
361
|
+
* rates: { BTC: 65000, ETH: 3500, SOL: 150, POL: 0.50, BCH: 350 },
|
|
362
|
+
* });
|
|
363
|
+
*
|
|
364
|
+
* // Charge $5 for premium access — buyer picks their chain
|
|
365
|
+
* app.get('/premium', x402({ amountUsd: 5.00 }), handler);
|
|
366
|
+
*/
|
|
367
|
+
export function createX402Middleware(globalOptions) {
|
|
368
|
+
const {
|
|
369
|
+
apiKey,
|
|
370
|
+
payTo,
|
|
371
|
+
methods,
|
|
372
|
+
rates = {},
|
|
373
|
+
description = 'Payment required',
|
|
374
|
+
facilitatorUrl = DEFAULT_FACILITATOR_URL,
|
|
375
|
+
apiBaseUrl = 'https://coinpayportal.com',
|
|
376
|
+
} = globalOptions;
|
|
377
|
+
|
|
378
|
+
if (!apiKey) throw new Error('x402 middleware requires an apiKey');
|
|
379
|
+
if (!payTo) throw new Error('x402 middleware requires a payTo address');
|
|
380
|
+
|
|
381
|
+
// Mutable rates cache — can be updated externally or via ratesEndpoint
|
|
382
|
+
let currentRates = { ...rates };
|
|
383
|
+
|
|
384
|
+
// If ratesEndpoint provided, poll for fresh rates
|
|
385
|
+
if (globalOptions.ratesEndpoint) {
|
|
386
|
+
const fetchRates = async () => {
|
|
387
|
+
try {
|
|
388
|
+
const res = await fetch(globalOptions.ratesEndpoint);
|
|
389
|
+
if (res.ok) currentRates = await res.json();
|
|
390
|
+
} catch { /* ignore */ }
|
|
391
|
+
};
|
|
392
|
+
fetchRates();
|
|
393
|
+
setInterval(fetchRates, 60_000); // refresh every 60s
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Route-level middleware factory.
|
|
398
|
+
*
|
|
399
|
+
* @param {Object} routeOptions
|
|
400
|
+
* @param {number} [routeOptions.amountUsd] - Price in USD (preferred)
|
|
401
|
+
* @param {string} [routeOptions.amount] - Raw amount (legacy, single-method)
|
|
402
|
+
* @param {string} [routeOptions.network] - Single network (legacy)
|
|
403
|
+
* @param {string[]} [routeOptions.methods] - Override accepted methods for this route
|
|
404
|
+
* @param {string} [routeOptions.description] - Route-specific description
|
|
405
|
+
*/
|
|
406
|
+
return function x402Route(routeOptions = {}) {
|
|
407
|
+
const routeAmountUsd = routeOptions.amountUsd;
|
|
408
|
+
const routeAmount = routeOptions.amount;
|
|
409
|
+
const routeDescription = routeOptions.description || description;
|
|
410
|
+
const routeMethods = routeOptions.methods || methods;
|
|
411
|
+
|
|
412
|
+
if (!routeAmountUsd && !routeAmount) {
|
|
413
|
+
throw new Error('x402 route requires amountUsd or amount');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return async function x402Middleware(req, res, next) {
|
|
417
|
+
const paymentHeader = req.headers['x-payment'] || req.headers['X-Payment'];
|
|
418
|
+
|
|
419
|
+
if (!paymentHeader) {
|
|
420
|
+
const resource = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
|
|
421
|
+
try {
|
|
422
|
+
const body = buildPaymentRequired({
|
|
423
|
+
payTo,
|
|
424
|
+
amountUsd: routeAmountUsd,
|
|
425
|
+
amount: routeAmount,
|
|
426
|
+
network: routeOptions.network,
|
|
427
|
+
rates: currentRates,
|
|
428
|
+
methods: routeMethods,
|
|
429
|
+
resource,
|
|
430
|
+
description: routeDescription,
|
|
431
|
+
facilitatorUrl,
|
|
432
|
+
});
|
|
433
|
+
return res.status(402).json(body);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
return res.status(500).json({ error: 'Failed to build payment options', details: err.message });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Verify the payment
|
|
440
|
+
try {
|
|
441
|
+
const result = await verifyX402Payment(paymentHeader, {
|
|
442
|
+
apiKey,
|
|
443
|
+
apiBaseUrl,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (!result.valid) {
|
|
447
|
+
return res.status(402).json({
|
|
448
|
+
error: 'Invalid payment proof',
|
|
449
|
+
details: result.reason,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Attach payment info to request for downstream use
|
|
454
|
+
req.x402Payment = result.payment;
|
|
455
|
+
next();
|
|
456
|
+
} catch (err) {
|
|
457
|
+
return res.status(500).json({ error: 'Payment verification failed', details: err.message });
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Verify an x402 payment proof.
|
|
465
|
+
*
|
|
466
|
+
* Calls CoinPayPortal's facilitator API to validate the cryptographic
|
|
467
|
+
* signature and payment details in the X-PAYMENT header.
|
|
468
|
+
* Supports all payment schemes: EVM signatures, BTC/BCH tx proofs,
|
|
469
|
+
* Solana tx proofs, Lightning preimages, and Stripe payment intents.
|
|
470
|
+
*
|
|
471
|
+
* @param {string} paymentHeader - The X-PAYMENT header value (base64-encoded JSON)
|
|
472
|
+
* @param {Object} options
|
|
473
|
+
* @param {string} options.apiKey - CoinPayPortal API key
|
|
474
|
+
* @param {string} [options.apiBaseUrl='https://coinpayportal.com'] - API base URL
|
|
475
|
+
* @returns {Promise<{valid: boolean, payment?: Object, reason?: string}>}
|
|
476
|
+
*/
|
|
477
|
+
export async function verifyX402Payment(paymentHeader, options = {}) {
|
|
478
|
+
const { apiKey, apiBaseUrl = 'https://coinpayportal.com' } = options;
|
|
479
|
+
|
|
480
|
+
if (!paymentHeader) {
|
|
481
|
+
return { valid: false, reason: 'Missing payment header' };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Decode the payment header
|
|
485
|
+
let payment;
|
|
486
|
+
try {
|
|
487
|
+
const decoded = Buffer.from(paymentHeader, 'base64').toString('utf-8');
|
|
488
|
+
payment = JSON.parse(decoded);
|
|
489
|
+
} catch {
|
|
490
|
+
return { valid: false, reason: 'Invalid payment header encoding' };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Validate required fields
|
|
494
|
+
if (!payment.scheme && !payment.signature && !payment.payload) {
|
|
495
|
+
return { valid: false, reason: 'Missing scheme, signature, or payload in payment proof' };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Check expiry if present
|
|
499
|
+
if (payment.payload?.expiresAt) {
|
|
500
|
+
const expiresAt = typeof payment.payload.expiresAt === 'number'
|
|
501
|
+
? new Date(payment.payload.expiresAt * 1000)
|
|
502
|
+
: new Date(payment.payload.expiresAt);
|
|
503
|
+
if (expiresAt < new Date()) {
|
|
504
|
+
return { valid: false, reason: 'Payment proof has expired' };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Call CoinPayPortal facilitator to verify
|
|
509
|
+
try {
|
|
510
|
+
const response = await fetch(`${apiBaseUrl}/api/x402/verify`, {
|
|
511
|
+
method: 'POST',
|
|
512
|
+
headers: {
|
|
513
|
+
'Content-Type': 'application/json',
|
|
514
|
+
...(apiKey ? { 'X-API-Key': apiKey } : {}),
|
|
515
|
+
},
|
|
516
|
+
body: JSON.stringify({ payment }),
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const data = await response.json();
|
|
520
|
+
|
|
521
|
+
if (!response.ok) {
|
|
522
|
+
return { valid: false, reason: data.error || 'Verification failed' };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return { valid: true, payment: data.payment };
|
|
526
|
+
} catch (err) {
|
|
527
|
+
return { valid: false, reason: `Facilitator error: ${err.message}` };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Settle an x402 payment on-chain (or via the appropriate payment rail).
|
|
533
|
+
*
|
|
534
|
+
* For crypto: claims the payment on-chain.
|
|
535
|
+
* For Lightning: confirms the preimage.
|
|
536
|
+
* For Stripe: captures the payment intent.
|
|
537
|
+
*
|
|
538
|
+
* @param {string} paymentHeader - The X-PAYMENT header value (base64-encoded JSON)
|
|
539
|
+
* @param {Object} options
|
|
540
|
+
* @param {string} options.apiKey - CoinPayPortal API key
|
|
541
|
+
* @param {string} [options.apiBaseUrl='https://coinpayportal.com'] - API base URL
|
|
542
|
+
* @returns {Promise<{settled: boolean, txHash?: string, network?: string, error?: string}>}
|
|
543
|
+
*/
|
|
544
|
+
export async function settleX402Payment(paymentHeader, options = {}) {
|
|
545
|
+
const { apiKey, apiBaseUrl = 'https://coinpayportal.com' } = options;
|
|
546
|
+
|
|
547
|
+
if (!paymentHeader) {
|
|
548
|
+
return { settled: false, error: 'Missing payment header' };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let payment;
|
|
552
|
+
try {
|
|
553
|
+
const decoded = Buffer.from(paymentHeader, 'base64').toString('utf-8');
|
|
554
|
+
payment = JSON.parse(decoded);
|
|
555
|
+
} catch {
|
|
556
|
+
return { settled: false, error: 'Invalid payment header encoding' };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
const response = await fetch(`${apiBaseUrl}/api/x402/settle`, {
|
|
561
|
+
method: 'POST',
|
|
562
|
+
headers: {
|
|
563
|
+
'Content-Type': 'application/json',
|
|
564
|
+
...(apiKey ? { 'X-API-Key': apiKey } : {}),
|
|
565
|
+
},
|
|
566
|
+
body: JSON.stringify({ payment }),
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const data = await response.json();
|
|
570
|
+
|
|
571
|
+
if (!response.ok) {
|
|
572
|
+
return { settled: false, error: data.error || 'Settlement failed' };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
settled: true,
|
|
577
|
+
txHash: data.txHash,
|
|
578
|
+
network: data.network,
|
|
579
|
+
asset: data.asset,
|
|
580
|
+
method: data.method,
|
|
581
|
+
};
|
|
582
|
+
} catch (err) {
|
|
583
|
+
return { settled: false, error: `Settlement error: ${err.message}` };
|
|
584
|
+
}
|
|
585
|
+
}
|