x402-bazaar 1.2.2 → 3.0.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/bin/cli.js +49 -7
- package/package.json +4 -3
- package/src/commands/call.js +305 -0
- package/src/commands/list.js +125 -0
- package/src/commands/search.js +120 -0
- package/src/commands/wallet.js +216 -0
- package/src/lib/payment.js +106 -0
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,10 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { initCommand } from '../src/commands/init.js';
|
|
5
5
|
import { configCommand } from '../src/commands/config.js';
|
|
6
6
|
import { statusCommand } from '../src/commands/status.js';
|
|
7
|
+
import { listCommand } from '../src/commands/list.js';
|
|
8
|
+
import { searchCommand } from '../src/commands/search.js';
|
|
9
|
+
import { callCommand } from '../src/commands/call.js';
|
|
10
|
+
import { walletCommand } from '../src/commands/wallet.js';
|
|
7
11
|
import chalk from 'chalk';
|
|
8
12
|
|
|
9
13
|
// Global error handler
|
|
@@ -21,7 +25,7 @@ const program = new Command();
|
|
|
21
25
|
program
|
|
22
26
|
.name('x402-bazaar')
|
|
23
27
|
.description(chalk.hex('#FF9900')('x402 Bazaar') + ' — Connect your AI agent to the marketplace in one command')
|
|
24
|
-
.version('
|
|
28
|
+
.version('3.0.0');
|
|
25
29
|
|
|
26
30
|
program
|
|
27
31
|
.command('init')
|
|
@@ -49,17 +53,55 @@ program
|
|
|
49
53
|
.option('--server-url <url>', 'Server URL to check', 'https://x402-api.onrender.com')
|
|
50
54
|
.action(statusCommand);
|
|
51
55
|
|
|
56
|
+
program
|
|
57
|
+
.command('list')
|
|
58
|
+
.description('List all services on x402 Bazaar')
|
|
59
|
+
.option('--chain <chain>', 'Filter by chain (base or skale)')
|
|
60
|
+
.option('--category <category>', 'Filter by category (ai, data, weather, etc.)')
|
|
61
|
+
.option('--free', 'Show only free services')
|
|
62
|
+
.option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
|
|
63
|
+
.action(listCommand);
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command('search <query>')
|
|
67
|
+
.description('Search for services by keyword')
|
|
68
|
+
.option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
|
|
69
|
+
.action(searchCommand);
|
|
70
|
+
|
|
71
|
+
program
|
|
72
|
+
.command('call <endpoint>')
|
|
73
|
+
.description('Call a marketplace endpoint with automatic x402 payment')
|
|
74
|
+
.option('--param <key=value>', 'Add parameter (can be used multiple times)', (value, previous) => {
|
|
75
|
+
return previous ? [...previous, value] : [value];
|
|
76
|
+
}, [])
|
|
77
|
+
.option('--key <privateKey>', 'Private key for auto-payment (or set X402_PRIVATE_KEY env)')
|
|
78
|
+
.option('--server-url <url>', 'Server URL', 'https://x402-api.onrender.com')
|
|
79
|
+
.action(callCommand);
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command('wallet')
|
|
83
|
+
.description('Check USDC wallet balance or generate a new wallet')
|
|
84
|
+
.option('--address <address>', 'Ethereum address to check')
|
|
85
|
+
.option('--setup', 'Generate a new wallet for auto-payment')
|
|
86
|
+
.action(walletCommand);
|
|
87
|
+
|
|
52
88
|
// Default: show help if no command given
|
|
53
89
|
if (process.argv.length <= 2) {
|
|
54
90
|
console.log('');
|
|
55
|
-
console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI'));
|
|
91
|
+
console.log(chalk.hex('#FF9900').bold(' x402 Bazaar') + chalk.dim(' — AI Agent Marketplace CLI v3'));
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(' Setup commands:');
|
|
94
|
+
console.log(chalk.cyan(' npx x402-bazaar init') + chalk.dim(' Full interactive setup'));
|
|
95
|
+
console.log(chalk.cyan(' npx x402-bazaar config') + chalk.dim(' Generate MCP config'));
|
|
96
|
+
console.log(chalk.cyan(' npx x402-bazaar status') + chalk.dim(' Check server connection'));
|
|
56
97
|
console.log('');
|
|
57
|
-
console.log('
|
|
58
|
-
console.log(chalk.cyan(' npx x402-bazaar
|
|
59
|
-
console.log(chalk.cyan(' npx x402-bazaar
|
|
60
|
-
console.log(chalk.cyan(' npx x402-bazaar
|
|
98
|
+
console.log(' Marketplace commands:');
|
|
99
|
+
console.log(chalk.cyan(' npx x402-bazaar list') + chalk.dim(' Browse all services'));
|
|
100
|
+
console.log(chalk.cyan(' npx x402-bazaar search <query>') + chalk.dim(' Find services by keyword'));
|
|
101
|
+
console.log(chalk.cyan(' npx x402-bazaar call <endpoint>') + chalk.dim(' Call API with auto-payment'));
|
|
102
|
+
console.log(chalk.cyan(' npx x402-bazaar wallet') + chalk.dim(' Check balance / setup wallet'));
|
|
61
103
|
console.log('');
|
|
62
|
-
console.log(chalk.dim(' Run with --help for
|
|
104
|
+
console.log(chalk.dim(' Run any command with --help for detailed options'));
|
|
63
105
|
console.log('');
|
|
64
106
|
process.exit(0);
|
|
65
107
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x402-bazaar",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "CLI for x402 Bazaar — AI Agent Marketplace. Browse, call, and auto-pay APIs with USDC on Base. One command to connect your agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/cli.js",
|
|
7
7
|
"bin": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"commander": "^12.1.0",
|
|
46
46
|
"inquirer": "^9.3.0",
|
|
47
|
-
"ora": "^8.0.1"
|
|
47
|
+
"ora": "^8.0.1",
|
|
48
|
+
"viem": "^2.45.3"
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { log } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
export async function callCommand(endpoint, options) {
|
|
8
|
+
if (!endpoint || endpoint.trim().length === 0) {
|
|
9
|
+
log.error('Endpoint is required');
|
|
10
|
+
log.dim(' Usage: x402-bazaar call <endpoint> [--param key=value...]');
|
|
11
|
+
log.dim(' Example: x402-bazaar call /api/weather --param city=Paris');
|
|
12
|
+
log.dim(' Example: x402-bazaar call /api/hash --param text=hello --key 0x...');
|
|
13
|
+
console.log('');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
18
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
19
|
+
const fullUrl = `${serverUrl}${normalizedEndpoint}`;
|
|
20
|
+
|
|
21
|
+
log.banner();
|
|
22
|
+
log.info(`Calling endpoint: ${chalk.bold(normalizedEndpoint)}`);
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
// Parse params
|
|
26
|
+
const params = {};
|
|
27
|
+
if (options.param) {
|
|
28
|
+
const paramArray = Array.isArray(options.param) ? options.param : [options.param];
|
|
29
|
+
for (const p of paramArray) {
|
|
30
|
+
const [key, ...valueParts] = p.split('=');
|
|
31
|
+
if (!key || valueParts.length === 0) {
|
|
32
|
+
log.warn(`Invalid param format: ${p} (expected key=value)`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
let value = valueParts.join('=');
|
|
36
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
37
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
38
|
+
value = value.slice(1, -1);
|
|
39
|
+
}
|
|
40
|
+
params[key.trim()] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (Object.keys(params).length > 0) {
|
|
45
|
+
log.info('Parameters:');
|
|
46
|
+
for (const [k, v] of Object.entries(params)) {
|
|
47
|
+
log.dim(` ${k}: ${v}`);
|
|
48
|
+
}
|
|
49
|
+
console.log('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Resolve private key for auto-payment
|
|
53
|
+
const privateKey = resolvePrivateKey(options);
|
|
54
|
+
const autoPay = !!privateKey;
|
|
55
|
+
|
|
56
|
+
if (autoPay) {
|
|
57
|
+
log.info(`Auto-payment: ${chalk.hex('#34D399').bold('enabled')}`);
|
|
58
|
+
try {
|
|
59
|
+
const { getAddressFromKey } = await import('../lib/payment.js');
|
|
60
|
+
const address = getAddressFromKey(privateKey);
|
|
61
|
+
log.dim(` Wallet: ${address.slice(0, 6)}...${address.slice(-4)}`);
|
|
62
|
+
} catch { /* ignore display errors */ }
|
|
63
|
+
console.log('');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build URL with query params
|
|
67
|
+
let finalUrl = fullUrl;
|
|
68
|
+
if (Object.keys(params).length > 0) {
|
|
69
|
+
const queryString = new URLSearchParams(params).toString();
|
|
70
|
+
finalUrl = `${fullUrl}?${queryString}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const spinner = ora(`GET ${finalUrl}...`).start();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const fetchOptions = {
|
|
77
|
+
method: 'GET',
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
signal: AbortSignal.timeout(30000),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const res = await fetch(finalUrl, fetchOptions);
|
|
83
|
+
spinner.stop();
|
|
84
|
+
|
|
85
|
+
// Handle 402 Payment Required
|
|
86
|
+
if (res.status === 402) {
|
|
87
|
+
console.log('');
|
|
88
|
+
log.warn(chalk.bold('Payment Required (HTTP 402)'));
|
|
89
|
+
console.log('');
|
|
90
|
+
|
|
91
|
+
let paymentInfo;
|
|
92
|
+
try {
|
|
93
|
+
paymentInfo = await res.json();
|
|
94
|
+
} catch {
|
|
95
|
+
log.error('Could not parse payment details');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const price = paymentInfo.payment_details?.amount || paymentInfo.price;
|
|
100
|
+
const payTo = paymentInfo.payment_details?.recipient || paymentInfo.payment_details?.walletAddress || paymentInfo.paymentAddress;
|
|
101
|
+
|
|
102
|
+
if (price) {
|
|
103
|
+
log.info(`Price: ${chalk.cyan.bold(`${price} USDC`)}`);
|
|
104
|
+
}
|
|
105
|
+
if (payTo) {
|
|
106
|
+
log.dim(` Pay to: ${payTo}`);
|
|
107
|
+
}
|
|
108
|
+
console.log('');
|
|
109
|
+
|
|
110
|
+
// Auto-pay if key is available
|
|
111
|
+
if (autoPay && price && payTo) {
|
|
112
|
+
await handleAutoPayment(privateKey, payTo, price, finalUrl, fetchOptions);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// No auto-pay — show instructions
|
|
117
|
+
log.separator();
|
|
118
|
+
console.log('');
|
|
119
|
+
log.info('To pay automatically, provide your private key:');
|
|
120
|
+
console.log('');
|
|
121
|
+
log.dim(' Option 1: Environment variable');
|
|
122
|
+
log.dim(' export X402_PRIVATE_KEY=0xYourPrivateKey');
|
|
123
|
+
log.dim(' npx x402-bazaar call /api/weather --param city=Paris');
|
|
124
|
+
console.log('');
|
|
125
|
+
log.dim(' Option 2: --key flag');
|
|
126
|
+
log.dim(' npx x402-bazaar call /api/weather --param city=Paris --key 0xYourKey');
|
|
127
|
+
console.log('');
|
|
128
|
+
log.dim(' Option 3: Generate a new wallet');
|
|
129
|
+
log.dim(' npx x402-bazaar wallet --setup');
|
|
130
|
+
console.log('');
|
|
131
|
+
log.dim(' Option 4: Use the MCP server (via Claude/Cursor)');
|
|
132
|
+
log.dim(' npx x402-bazaar init');
|
|
133
|
+
console.log('');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Handle other errors
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
console.log('');
|
|
140
|
+
log.error(`HTTP ${res.status}: ${res.statusText}`);
|
|
141
|
+
try {
|
|
142
|
+
const errorBody = await res.text();
|
|
143
|
+
if (errorBody) {
|
|
144
|
+
console.log('');
|
|
145
|
+
log.dim('Response:');
|
|
146
|
+
console.log(chalk.red(errorBody));
|
|
147
|
+
}
|
|
148
|
+
} catch { /* ignore */ }
|
|
149
|
+
console.log('');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Success
|
|
154
|
+
await displayResponse(res);
|
|
155
|
+
|
|
156
|
+
} catch (err) {
|
|
157
|
+
spinner.fail('Request failed');
|
|
158
|
+
console.log('');
|
|
159
|
+
|
|
160
|
+
if (err.name === 'AbortError') {
|
|
161
|
+
log.error('Request timeout (30s)');
|
|
162
|
+
log.dim(' Try again or check status: npx x402-bazaar status');
|
|
163
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
164
|
+
log.error('Cannot connect to server');
|
|
165
|
+
log.dim(` Server URL: ${serverUrl}`);
|
|
166
|
+
} else {
|
|
167
|
+
log.error(err.message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log('');
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolve private key from: --key flag > X402_PRIVATE_KEY env > ~/.x402-bazaar/wallet.json
|
|
177
|
+
*/
|
|
178
|
+
function resolvePrivateKey(options) {
|
|
179
|
+
if (options.key) {
|
|
180
|
+
return normalizeKey(options.key);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (process.env.X402_PRIVATE_KEY) {
|
|
184
|
+
return normalizeKey(process.env.X402_PRIVATE_KEY);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Try local wallet file
|
|
188
|
+
try {
|
|
189
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
190
|
+
const walletPath = path.join(home, '.x402-bazaar', 'wallet.json');
|
|
191
|
+
if (fs.existsSync(walletPath)) {
|
|
192
|
+
const data = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
|
|
193
|
+
if (data.privateKey) {
|
|
194
|
+
return normalizeKey(data.privateKey);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch { /* ignore */ }
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeKey(key) {
|
|
203
|
+
key = key.trim();
|
|
204
|
+
if (!key.startsWith('0x')) key = '0x' + key;
|
|
205
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(key)) return null;
|
|
206
|
+
return key;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Handle automatic x402 payment and retry
|
|
211
|
+
*/
|
|
212
|
+
async function handleAutoPayment(privateKey, payTo, price, url, fetchOptions) {
|
|
213
|
+
const spinner = ora(`Sending ${price} USDC on Base mainnet...`).start();
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const { sendUsdcPayment } = await import('../lib/payment.js');
|
|
217
|
+
const payment = await sendUsdcPayment(privateKey, payTo, price);
|
|
218
|
+
|
|
219
|
+
spinner.succeed(`Payment confirmed: ${chalk.hex('#34D399').bold(`${price} USDC`)}`);
|
|
220
|
+
log.dim(` Tx: ${payment.explorer}`);
|
|
221
|
+
console.log('');
|
|
222
|
+
|
|
223
|
+
// Retry with payment proof
|
|
224
|
+
const retrySpinner = ora('Retrying with payment proof...').start();
|
|
225
|
+
|
|
226
|
+
const retryRes = await fetch(url, {
|
|
227
|
+
...fetchOptions,
|
|
228
|
+
headers: {
|
|
229
|
+
...fetchOptions.headers,
|
|
230
|
+
'X-Payment-TxHash': payment.txHash,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
retrySpinner.stop();
|
|
235
|
+
|
|
236
|
+
if (!retryRes.ok) {
|
|
237
|
+
console.log('');
|
|
238
|
+
log.error(`HTTP ${retryRes.status}: ${retryRes.statusText}`);
|
|
239
|
+
try {
|
|
240
|
+
const body = await retryRes.text();
|
|
241
|
+
if (body) console.log(chalk.red(body));
|
|
242
|
+
} catch { /* ignore */ }
|
|
243
|
+
console.log('');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await displayResponse(retryRes);
|
|
248
|
+
|
|
249
|
+
} catch (err) {
|
|
250
|
+
spinner.fail('Payment failed');
|
|
251
|
+
console.log('');
|
|
252
|
+
|
|
253
|
+
if (err.message.includes('Insufficient USDC')) {
|
|
254
|
+
log.error(err.message);
|
|
255
|
+
log.dim(' Fund your wallet with USDC on Base.');
|
|
256
|
+
log.dim(' Check balance: npx x402-bazaar wallet --address <your-address>');
|
|
257
|
+
} else {
|
|
258
|
+
log.error(err.message);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log('');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Display API response with JSON highlighting
|
|
268
|
+
*/
|
|
269
|
+
async function displayResponse(res) {
|
|
270
|
+
console.log('');
|
|
271
|
+
log.success(`${chalk.bold(res.status)} ${res.statusText}`);
|
|
272
|
+
console.log('');
|
|
273
|
+
|
|
274
|
+
const contentType = res.headers.get('content-type') || '';
|
|
275
|
+
|
|
276
|
+
if (contentType.includes('application/json')) {
|
|
277
|
+
const responseData = await res.json();
|
|
278
|
+
log.separator();
|
|
279
|
+
console.log('');
|
|
280
|
+
log.info('Response (JSON):');
|
|
281
|
+
console.log('');
|
|
282
|
+
console.log(highlightJson(JSON.stringify(responseData, null, 2)));
|
|
283
|
+
console.log('');
|
|
284
|
+
log.separator();
|
|
285
|
+
} else {
|
|
286
|
+
const responseText = await res.text();
|
|
287
|
+
log.separator();
|
|
288
|
+
console.log('');
|
|
289
|
+
log.info('Response:');
|
|
290
|
+
console.log('');
|
|
291
|
+
console.log(chalk.white(responseText));
|
|
292
|
+
console.log('');
|
|
293
|
+
log.separator();
|
|
294
|
+
}
|
|
295
|
+
console.log('');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function highlightJson(jsonString) {
|
|
299
|
+
return jsonString
|
|
300
|
+
.replace(/"([^"]+)":/g, chalk.hex('#60A5FA')('"$1"') + ':')
|
|
301
|
+
.replace(/: "([^"]+)"/g, ': ' + chalk.hex('#34D399')('"$1"'))
|
|
302
|
+
.replace(/: (\d+\.?\d*)/g, ': ' + chalk.hex('#FBBF24')('$1'))
|
|
303
|
+
.replace(/: (true|false)/g, ': ' + chalk.hex('#9333EA')('$1'))
|
|
304
|
+
.replace(/: null/g, ': ' + chalk.hex('#6B7280')('null'));
|
|
305
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
const CATEGORIES = [
|
|
6
|
+
'ai', 'data', 'automation', 'blockchain', 'weather', 'finance',
|
|
7
|
+
'social', 'image', 'video', 'audio', 'search', 'translation', 'other'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export async function listCommand(options) {
|
|
11
|
+
const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
12
|
+
|
|
13
|
+
log.banner();
|
|
14
|
+
log.info(`Fetching services from ${chalk.bold('x402 Bazaar')}`);
|
|
15
|
+
console.log('');
|
|
16
|
+
|
|
17
|
+
// Build query params
|
|
18
|
+
const params = new URLSearchParams();
|
|
19
|
+
if (options.chain) params.append('chain', options.chain);
|
|
20
|
+
if (options.category) params.append('category', options.category);
|
|
21
|
+
if (options.free) params.append('free', 'true');
|
|
22
|
+
|
|
23
|
+
const url = `${serverUrl}/api/services${params.toString() ? '?' + params.toString() : ''}`;
|
|
24
|
+
|
|
25
|
+
const spinner = ora('Loading services...').start();
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
29
|
+
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const data = await res.json();
|
|
35
|
+
const services = data.services || data || [];
|
|
36
|
+
|
|
37
|
+
spinner.succeed(`Found ${chalk.bold(services.length)} service${services.length !== 1 ? 's' : ''}`);
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
if (services.length === 0) {
|
|
41
|
+
log.warn('No services found with these filters.');
|
|
42
|
+
log.dim(' Try: x402-bazaar list --category ai');
|
|
43
|
+
console.log('');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Display services table
|
|
48
|
+
log.separator();
|
|
49
|
+
console.log('');
|
|
50
|
+
|
|
51
|
+
services.forEach((service, idx) => {
|
|
52
|
+
const name = service.name || service.title || 'Unnamed Service';
|
|
53
|
+
const price = parseFloat(service.price || 0);
|
|
54
|
+
const priceLabel = price === 0
|
|
55
|
+
? chalk.hex('#34D399').bold('FREE')
|
|
56
|
+
: chalk.cyan(`$${price.toFixed(3)} USDC`);
|
|
57
|
+
|
|
58
|
+
// Extract category from tags
|
|
59
|
+
let category = 'other';
|
|
60
|
+
if (service.tags && Array.isArray(service.tags)) {
|
|
61
|
+
const realCategories = service.tags.filter(t => CATEGORIES.includes(t.toLowerCase()));
|
|
62
|
+
if (realCategories.length > 0) {
|
|
63
|
+
category = realCategories[0];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const chain = service.chain || 'base';
|
|
68
|
+
const chainLabel = chain === 'skale'
|
|
69
|
+
? chalk.hex('#9333EA')('SKALE')
|
|
70
|
+
: chalk.hex('#0052FF')('Base');
|
|
71
|
+
|
|
72
|
+
console.log(
|
|
73
|
+
chalk.hex('#FF9900').bold(`${(idx + 1).toString().padStart(2, ' ')}. `) +
|
|
74
|
+
chalk.white.bold(name)
|
|
75
|
+
);
|
|
76
|
+
console.log(` ${priceLabel} ${chalk.dim('|')} ${chalk.hex('#60A5FA')(category)} ${chalk.dim('|')} ${chainLabel}`);
|
|
77
|
+
|
|
78
|
+
if (service.description) {
|
|
79
|
+
const desc = service.description.length > 80
|
|
80
|
+
? service.description.substring(0, 77) + '...'
|
|
81
|
+
: service.description;
|
|
82
|
+
console.log(` ${chalk.hex('#6B7280')(desc)}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (service.endpoint) {
|
|
86
|
+
console.log(` ${chalk.dim('Endpoint:')} ${chalk.hex('#FBBF24')(service.endpoint)}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
log.separator();
|
|
93
|
+
console.log('');
|
|
94
|
+
log.info(`Total: ${chalk.bold(services.length)} service${services.length !== 1 ? 's' : ''}`);
|
|
95
|
+
|
|
96
|
+
// Show filter info
|
|
97
|
+
if (options.chain || options.category || options.free) {
|
|
98
|
+
console.log('');
|
|
99
|
+
log.dim(' Active filters:');
|
|
100
|
+
if (options.chain) log.dim(` Chain: ${options.chain}`);
|
|
101
|
+
if (options.category) log.dim(` Category: ${options.category}`);
|
|
102
|
+
if (options.free) log.dim(' Price: free only');
|
|
103
|
+
log.dim(' Run without options to see all services');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('');
|
|
107
|
+
|
|
108
|
+
} catch (err) {
|
|
109
|
+
spinner.fail('Failed to fetch services');
|
|
110
|
+
|
|
111
|
+
if (err.name === 'AbortError') {
|
|
112
|
+
log.error('Request timeout — server may be sleeping (Render free tier)');
|
|
113
|
+
log.dim(' Try again in 30 seconds or check status: npx x402-bazaar status');
|
|
114
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
115
|
+
log.error('Cannot connect to server');
|
|
116
|
+
log.dim(` Server URL: ${serverUrl}`);
|
|
117
|
+
log.dim(' Check your internet connection or try: npx x402-bazaar status');
|
|
118
|
+
} else {
|
|
119
|
+
log.error(err.message);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log('');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
const CATEGORIES = [
|
|
6
|
+
'ai', 'data', 'automation', 'blockchain', 'weather', 'finance',
|
|
7
|
+
'social', 'image', 'video', 'audio', 'search', 'translation', 'other'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export async function searchCommand(query, options) {
|
|
11
|
+
if (!query || query.trim().length === 0) {
|
|
12
|
+
log.error('Search query is required');
|
|
13
|
+
log.dim(' Usage: x402-bazaar search <query>');
|
|
14
|
+
log.dim(' Example: x402-bazaar search "weather API"');
|
|
15
|
+
console.log('');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const serverUrl = options.serverUrl || 'https://x402-api.onrender.com';
|
|
20
|
+
|
|
21
|
+
log.banner();
|
|
22
|
+
log.info(`Searching for: ${chalk.bold(query)}`);
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
const url = `${serverUrl}/api/services?search=${encodeURIComponent(query)}`;
|
|
26
|
+
const spinner = ora('Searching...').start();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
30
|
+
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
const services = data.services || data || [];
|
|
37
|
+
|
|
38
|
+
if (services.length === 0) {
|
|
39
|
+
spinner.fail('No results found');
|
|
40
|
+
console.log('');
|
|
41
|
+
log.warn(`No services match "${query}"`);
|
|
42
|
+
log.dim(' Tips:');
|
|
43
|
+
log.dim(' • Check your spelling');
|
|
44
|
+
log.dim(' • Try broader keywords (e.g., "AI" instead of "GPT-4")');
|
|
45
|
+
log.dim(' • Browse all services: x402-bazaar list');
|
|
46
|
+
console.log('');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
spinner.succeed(`Found ${chalk.bold(services.length)} result${services.length !== 1 ? 's' : ''}`);
|
|
51
|
+
console.log('');
|
|
52
|
+
|
|
53
|
+
// Display results
|
|
54
|
+
log.separator();
|
|
55
|
+
console.log('');
|
|
56
|
+
|
|
57
|
+
services.forEach((service, idx) => {
|
|
58
|
+
const name = service.name || service.title || 'Unnamed Service';
|
|
59
|
+
const price = parseFloat(service.price || 0);
|
|
60
|
+
const priceLabel = price === 0
|
|
61
|
+
? chalk.hex('#34D399').bold('FREE')
|
|
62
|
+
: chalk.cyan(`$${price.toFixed(3)} USDC`);
|
|
63
|
+
|
|
64
|
+
// Extract category from tags
|
|
65
|
+
let category = 'other';
|
|
66
|
+
if (service.tags && Array.isArray(service.tags)) {
|
|
67
|
+
const realCategories = service.tags.filter(t => CATEGORIES.includes(t.toLowerCase()));
|
|
68
|
+
if (realCategories.length > 0) {
|
|
69
|
+
category = realCategories[0];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const chain = service.chain || 'base';
|
|
74
|
+
const chainLabel = chain === 'skale'
|
|
75
|
+
? chalk.hex('#9333EA')('SKALE')
|
|
76
|
+
: chalk.hex('#0052FF')('Base');
|
|
77
|
+
|
|
78
|
+
console.log(
|
|
79
|
+
chalk.hex('#FF9900').bold(`${(idx + 1).toString().padStart(2, ' ')}. `) +
|
|
80
|
+
chalk.white.bold(name)
|
|
81
|
+
);
|
|
82
|
+
console.log(` ${priceLabel} ${chalk.dim('|')} ${chalk.hex('#60A5FA')(category)} ${chalk.dim('|')} ${chainLabel}`);
|
|
83
|
+
|
|
84
|
+
if (service.description) {
|
|
85
|
+
const desc = service.description.length > 80
|
|
86
|
+
? service.description.substring(0, 77) + '...'
|
|
87
|
+
: service.description;
|
|
88
|
+
console.log(` ${chalk.hex('#6B7280')(desc)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (service.endpoint) {
|
|
92
|
+
console.log(` ${chalk.dim('Endpoint:')} ${chalk.hex('#FBBF24')(service.endpoint)}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
log.separator();
|
|
99
|
+
console.log('');
|
|
100
|
+
log.info(`Total: ${chalk.bold(services.length)} result${services.length !== 1 ? 's' : ''}`);
|
|
101
|
+
console.log('');
|
|
102
|
+
|
|
103
|
+
} catch (err) {
|
|
104
|
+
spinner.fail('Search failed');
|
|
105
|
+
|
|
106
|
+
if (err.name === 'AbortError') {
|
|
107
|
+
log.error('Request timeout — server may be sleeping (Render free tier)');
|
|
108
|
+
log.dim(' Try again in 30 seconds or check status: npx x402-bazaar status');
|
|
109
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
110
|
+
log.error('Cannot connect to server');
|
|
111
|
+
log.dim(` Server URL: ${serverUrl}`);
|
|
112
|
+
log.dim(' Check your internet connection or try: npx x402-bazaar status');
|
|
113
|
+
} else {
|
|
114
|
+
log.error(err.message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { log } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
const BASE_RPC_URL = 'https://mainnet.base.org';
|
|
8
|
+
const USDC_CONTRACT_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
9
|
+
const BALANCE_OF_SELECTOR = '0x70a08231';
|
|
10
|
+
|
|
11
|
+
export async function walletCommand(options) {
|
|
12
|
+
log.banner();
|
|
13
|
+
|
|
14
|
+
// Handle --setup: generate a new wallet
|
|
15
|
+
if (options.setup) {
|
|
16
|
+
await setupWallet();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!options.address) {
|
|
21
|
+
log.info('Check USDC balance or generate a new wallet.');
|
|
22
|
+
console.log('');
|
|
23
|
+
log.dim(' Usage:');
|
|
24
|
+
log.dim(' x402-bazaar wallet --address 0xYourAddress');
|
|
25
|
+
log.dim(' x402-bazaar wallet --setup');
|
|
26
|
+
console.log('');
|
|
27
|
+
log.dim(' Examples:');
|
|
28
|
+
log.dim(' x402-bazaar wallet --address 0xA986540F0AaDFB5Ba5ceb2b1d81d90DBE479084b');
|
|
29
|
+
log.dim(' x402-bazaar wallet --setup (generate a new wallet for auto-payment)');
|
|
30
|
+
console.log('');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const address = options.address.trim();
|
|
35
|
+
|
|
36
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
|
|
37
|
+
log.error('Invalid Ethereum address format');
|
|
38
|
+
log.dim(' Expected: 0x followed by 40 hexadecimal characters');
|
|
39
|
+
log.dim(` Got: ${address}`);
|
|
40
|
+
console.log('');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
log.info(`Checking wallet: ${chalk.bold(maskAddress(address))}`);
|
|
45
|
+
console.log('');
|
|
46
|
+
|
|
47
|
+
const spinner = ora('Fetching USDC balance from Base...').start();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const paddedAddress = address.slice(2).padStart(64, '0');
|
|
51
|
+
const data = BALANCE_OF_SELECTOR + paddedAddress;
|
|
52
|
+
|
|
53
|
+
const rpcPayload = {
|
|
54
|
+
jsonrpc: '2.0',
|
|
55
|
+
id: 1,
|
|
56
|
+
method: 'eth_call',
|
|
57
|
+
params: [{ to: USDC_CONTRACT_BASE, data }, 'latest'],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const res = await fetch(BASE_RPC_URL, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify(rpcPayload),
|
|
64
|
+
signal: AbortSignal.timeout(15000),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
68
|
+
|
|
69
|
+
const rpcResponse = await res.json();
|
|
70
|
+
if (rpcResponse.error) throw new Error(rpcResponse.error.message || 'RPC error');
|
|
71
|
+
if (!rpcResponse.result) throw new Error('No result from RPC');
|
|
72
|
+
|
|
73
|
+
const balanceRaw = BigInt(rpcResponse.result);
|
|
74
|
+
const balanceUsdc = Number(balanceRaw) / 1_000_000;
|
|
75
|
+
|
|
76
|
+
spinner.succeed('Balance fetched');
|
|
77
|
+
console.log('');
|
|
78
|
+
log.separator();
|
|
79
|
+
console.log('');
|
|
80
|
+
|
|
81
|
+
log.info(`Address: ${chalk.hex('#34D399')(maskAddress(address))}`);
|
|
82
|
+
log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')} (Chain ID: 8453)`);
|
|
83
|
+
log.info(`Balance: ${chalk.cyan.bold(balanceUsdc.toFixed(6))} ${chalk.dim('USDC')}`);
|
|
84
|
+
|
|
85
|
+
console.log('');
|
|
86
|
+
log.separator();
|
|
87
|
+
console.log('');
|
|
88
|
+
log.dim(` Explorer: https://basescan.org/address/${address}`);
|
|
89
|
+
|
|
90
|
+
if (balanceUsdc === 0) {
|
|
91
|
+
console.log('');
|
|
92
|
+
log.warn('This wallet has no USDC.');
|
|
93
|
+
log.dim(' Send USDC on Base to this address to start using paid APIs.');
|
|
94
|
+
} else if (balanceUsdc < 0.1) {
|
|
95
|
+
console.log('');
|
|
96
|
+
log.warn('Low balance — consider adding more USDC.');
|
|
97
|
+
log.dim(' Most x402 Bazaar APIs cost $0.001-$0.05 per call.');
|
|
98
|
+
} else {
|
|
99
|
+
console.log('');
|
|
100
|
+
log.success('Wallet is funded and ready!');
|
|
101
|
+
const estimatedCalls = Math.floor(balanceUsdc / 0.005);
|
|
102
|
+
log.dim(` Estimated API calls: ~${estimatedCalls} (at avg $0.005/call)`);
|
|
103
|
+
}
|
|
104
|
+
console.log('');
|
|
105
|
+
|
|
106
|
+
} catch (err) {
|
|
107
|
+
spinner.fail('Failed to fetch balance');
|
|
108
|
+
console.log('');
|
|
109
|
+
|
|
110
|
+
if (err.name === 'AbortError') {
|
|
111
|
+
log.error('Request timeout — Base RPC may be slow');
|
|
112
|
+
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
113
|
+
log.error('Cannot connect to Base RPC');
|
|
114
|
+
} else {
|
|
115
|
+
log.error(err.message);
|
|
116
|
+
}
|
|
117
|
+
console.log('');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Generate a new wallet and save to ~/.x402-bazaar/wallet.json
|
|
124
|
+
*/
|
|
125
|
+
async function setupWallet() {
|
|
126
|
+
log.info('Generating a new wallet for x402 Bazaar auto-payment...');
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
130
|
+
const dir = path.join(home, '.x402-bazaar');
|
|
131
|
+
const walletPath = path.join(dir, 'wallet.json');
|
|
132
|
+
|
|
133
|
+
// Check if wallet already exists
|
|
134
|
+
if (fs.existsSync(walletPath)) {
|
|
135
|
+
try {
|
|
136
|
+
const existing = JSON.parse(fs.readFileSync(walletPath, 'utf-8'));
|
|
137
|
+
if (existing.privateKey) {
|
|
138
|
+
const { getAddressFromKey } = await import('../lib/payment.js');
|
|
139
|
+
const address = getAddressFromKey(existing.privateKey.startsWith('0x') ? existing.privateKey : '0x' + existing.privateKey);
|
|
140
|
+
console.log('');
|
|
141
|
+
log.warn('A wallet already exists!');
|
|
142
|
+
log.info(`Address: ${chalk.hex('#34D399')(address)}`);
|
|
143
|
+
log.dim(` File: ${walletPath}`);
|
|
144
|
+
console.log('');
|
|
145
|
+
log.dim(' To use a different wallet, delete the file and run --setup again.');
|
|
146
|
+
log.dim(' To check balance: npx x402-bazaar wallet --address ' + address);
|
|
147
|
+
console.log('');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
} catch { /* ignore parse errors, will overwrite */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const spinner = ora('Generating cryptographic key pair...').start();
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const { generatePrivateKey, privateKeyToAccount } = await import('viem/accounts');
|
|
157
|
+
const privateKey = generatePrivateKey();
|
|
158
|
+
const account = privateKeyToAccount(privateKey);
|
|
159
|
+
|
|
160
|
+
// Create directory
|
|
161
|
+
if (!fs.existsSync(dir)) {
|
|
162
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Save wallet (private key stored locally)
|
|
166
|
+
fs.writeFileSync(walletPath, JSON.stringify({
|
|
167
|
+
address: account.address,
|
|
168
|
+
privateKey: privateKey,
|
|
169
|
+
network: 'base',
|
|
170
|
+
created: new Date().toISOString(),
|
|
171
|
+
}, null, 2), 'utf-8');
|
|
172
|
+
|
|
173
|
+
spinner.succeed('Wallet generated!');
|
|
174
|
+
console.log('');
|
|
175
|
+
log.separator();
|
|
176
|
+
console.log('');
|
|
177
|
+
|
|
178
|
+
log.info(`Address: ${chalk.hex('#34D399').bold(account.address)}`);
|
|
179
|
+
log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')}`);
|
|
180
|
+
log.info(`Saved to: ${chalk.dim(walletPath)}`);
|
|
181
|
+
|
|
182
|
+
console.log('');
|
|
183
|
+
log.separator();
|
|
184
|
+
console.log('');
|
|
185
|
+
|
|
186
|
+
log.info('Next steps:');
|
|
187
|
+
console.log('');
|
|
188
|
+
log.dim(' 1. Fund this wallet with USDC on Base:');
|
|
189
|
+
log.dim(` Send USDC to ${chalk.hex('#34D399')(account.address)}`);
|
|
190
|
+
log.dim(' + a tiny amount of ETH for gas (~$0.01)');
|
|
191
|
+
console.log('');
|
|
192
|
+
log.dim(' 2. Call paid APIs automatically:');
|
|
193
|
+
log.dim(` ${chalk.cyan('npx x402-bazaar call /api/weather --param city=Paris')}`);
|
|
194
|
+
log.dim(' (auto-payment will use your saved wallet)');
|
|
195
|
+
console.log('');
|
|
196
|
+
log.dim(' 3. Or set the env variable for any tool:');
|
|
197
|
+
log.dim(` export X402_PRIVATE_KEY=${privateKey}`);
|
|
198
|
+
console.log('');
|
|
199
|
+
|
|
200
|
+
log.warn('Keep your private key safe! Anyone with it can spend your funds.');
|
|
201
|
+
log.dim(` Backup: ${walletPath}`);
|
|
202
|
+
console.log('');
|
|
203
|
+
|
|
204
|
+
} catch (err) {
|
|
205
|
+
spinner.fail('Wallet generation failed');
|
|
206
|
+
console.log('');
|
|
207
|
+
log.error(err.message);
|
|
208
|
+
console.log('');
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function maskAddress(address) {
|
|
214
|
+
if (address.length < 12) return address;
|
|
215
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
216
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createWalletClient, createPublicClient, http, parseUnits, encodeFunctionData } from 'viem';
|
|
2
|
+
import { base } from 'viem/chains';
|
|
3
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
4
|
+
|
|
5
|
+
const USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
6
|
+
const USDC_ABI = [
|
|
7
|
+
{
|
|
8
|
+
name: 'transfer',
|
|
9
|
+
type: 'function',
|
|
10
|
+
inputs: [
|
|
11
|
+
{ name: 'to', type: 'address' },
|
|
12
|
+
{ name: 'amount', type: 'uint256' },
|
|
13
|
+
],
|
|
14
|
+
outputs: [{ type: 'bool' }],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'balanceOf',
|
|
18
|
+
type: 'function',
|
|
19
|
+
inputs: [{ name: 'account', type: 'address' }],
|
|
20
|
+
outputs: [{ type: 'uint256' }],
|
|
21
|
+
stateMutability: 'view',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Send USDC payment on Base mainnet
|
|
27
|
+
* @param {string} privateKey - Hex private key (with 0x prefix)
|
|
28
|
+
* @param {string} toAddress - Recipient wallet address
|
|
29
|
+
* @param {number} amountUsdc - Amount in USDC (e.g., 0.005)
|
|
30
|
+
* @returns {{ txHash: string, explorer: string }}
|
|
31
|
+
*/
|
|
32
|
+
export async function sendUsdcPayment(privateKey, toAddress, amountUsdc) {
|
|
33
|
+
const account = privateKeyToAccount(privateKey);
|
|
34
|
+
|
|
35
|
+
const walletClient = createWalletClient({
|
|
36
|
+
account,
|
|
37
|
+
chain: base,
|
|
38
|
+
transport: http('https://mainnet.base.org'),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const publicClient = createPublicClient({
|
|
42
|
+
chain: base,
|
|
43
|
+
transport: http('https://mainnet.base.org'),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Convert USDC amount to 6-decimal units
|
|
47
|
+
const amount = parseUnits(amountUsdc.toString(), 6);
|
|
48
|
+
|
|
49
|
+
// Check balance first
|
|
50
|
+
const balance = await publicClient.readContract({
|
|
51
|
+
address: USDC_CONTRACT,
|
|
52
|
+
abi: USDC_ABI,
|
|
53
|
+
functionName: 'balanceOf',
|
|
54
|
+
args: [account.address],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (balance < amount) {
|
|
58
|
+
const balanceUsdc = Number(balance) / 1_000_000;
|
|
59
|
+
throw new Error(`Insufficient USDC balance: ${balanceUsdc.toFixed(6)} USDC (need ${amountUsdc} USDC)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Send the USDC transfer
|
|
63
|
+
const txHash = await walletClient.writeContract({
|
|
64
|
+
address: USDC_CONTRACT,
|
|
65
|
+
abi: USDC_ABI,
|
|
66
|
+
functionName: 'transfer',
|
|
67
|
+
args: [toAddress, amount],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Wait for confirmation
|
|
71
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
txHash,
|
|
75
|
+
explorer: `https://basescan.org/tx/${txHash}`,
|
|
76
|
+
from: account.address,
|
|
77
|
+
amount: amountUsdc,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the wallet address from a private key
|
|
83
|
+
*/
|
|
84
|
+
export function getAddressFromKey(privateKey) {
|
|
85
|
+
const account = privateKeyToAccount(privateKey);
|
|
86
|
+
return account.address;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get USDC balance for an address
|
|
91
|
+
*/
|
|
92
|
+
export async function getUsdcBalance(address) {
|
|
93
|
+
const publicClient = createPublicClient({
|
|
94
|
+
chain: base,
|
|
95
|
+
transport: http('https://mainnet.base.org'),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const balance = await publicClient.readContract({
|
|
99
|
+
address: USDC_CONTRACT,
|
|
100
|
+
abi: USDC_ABI,
|
|
101
|
+
functionName: 'balanceOf',
|
|
102
|
+
args: [address],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return Number(balance) / 1_000_000;
|
|
106
|
+
}
|