x402-bazaar 3.0.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -4
- package/bin/cli.js +109 -109
- package/package.json +50 -50
- package/src/commands/call.js +419 -305
- package/src/commands/init.js +566 -565
- package/src/commands/wallet.js +217 -216
- package/src/lib/payment.js +149 -23
package/src/commands/wallet.js
CHANGED
|
@@ -1,216 +1,217 @@
|
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
log
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
log.info(`
|
|
180
|
-
log.info(`
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
log
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
log
|
|
189
|
-
log.dim(
|
|
190
|
-
log.dim(
|
|
191
|
-
|
|
192
|
-
log
|
|
193
|
-
log.dim(
|
|
194
|
-
log.dim(
|
|
195
|
-
|
|
196
|
-
log
|
|
197
|
-
log.dim(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
log.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
log
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
try { fs.chmodSync(walletPath, 0o600); } catch {}
|
|
173
|
+
|
|
174
|
+
spinner.succeed('Wallet generated!');
|
|
175
|
+
console.log('');
|
|
176
|
+
log.separator();
|
|
177
|
+
console.log('');
|
|
178
|
+
|
|
179
|
+
log.info(`Address: ${chalk.hex('#34D399').bold(account.address)}`);
|
|
180
|
+
log.info(`Network: ${chalk.hex('#0052FF').bold('Base Mainnet')}`);
|
|
181
|
+
log.info(`Saved to: ${chalk.dim(walletPath)}`);
|
|
182
|
+
|
|
183
|
+
console.log('');
|
|
184
|
+
log.separator();
|
|
185
|
+
console.log('');
|
|
186
|
+
|
|
187
|
+
log.info('Next steps:');
|
|
188
|
+
console.log('');
|
|
189
|
+
log.dim(' 1. Fund this wallet with USDC on Base:');
|
|
190
|
+
log.dim(` Send USDC to ${chalk.hex('#34D399')(account.address)}`);
|
|
191
|
+
log.dim(' + a tiny amount of ETH for gas (~$0.01)');
|
|
192
|
+
console.log('');
|
|
193
|
+
log.dim(' 2. Call paid APIs automatically:');
|
|
194
|
+
log.dim(` ${chalk.cyan('npx x402-bazaar call /api/weather --param city=Paris')}`);
|
|
195
|
+
log.dim(' (auto-payment will use your saved wallet)');
|
|
196
|
+
console.log('');
|
|
197
|
+
log.dim(' 3. The CLI reads your wallet file automatically — no need to export the key.');
|
|
198
|
+
log.dim(` The private key is stored securely in: ${walletPath}`);
|
|
199
|
+
console.log('');
|
|
200
|
+
|
|
201
|
+
log.warn('Keep your private key safe! Anyone with it can spend your funds.');
|
|
202
|
+
log.dim(` Backup: ${walletPath}`);
|
|
203
|
+
console.log('');
|
|
204
|
+
|
|
205
|
+
} catch (err) {
|
|
206
|
+
spinner.fail('Wallet generation failed');
|
|
207
|
+
console.log('');
|
|
208
|
+
log.error(err.message);
|
|
209
|
+
console.log('');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function maskAddress(address) {
|
|
215
|
+
if (address.length < 12) return address;
|
|
216
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
217
|
+
}
|
package/src/lib/payment.js
CHANGED
|
@@ -22,27 +22,59 @@ const USDC_ABI = [
|
|
|
22
22
|
},
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
+
/** Minimum amount in micro-USDC (6 decimals) to allow a split payment. */
|
|
26
|
+
const MIN_SPLIT_AMOUNT_RAW = 100n; // 0.0001 USDC
|
|
27
|
+
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
27
|
-
* @param {string} privateKey
|
|
28
|
-
* @
|
|
29
|
-
* @param {number} amountUsdc - Amount in USDC (e.g., 0.005)
|
|
30
|
-
* @returns {{ txHash: string, explorer: string }}
|
|
29
|
+
* Build viem wallet + public clients for Base mainnet.
|
|
30
|
+
* @param {string} privateKey
|
|
31
|
+
* @returns {{ walletClient, publicClient, account }}
|
|
31
32
|
*/
|
|
32
|
-
|
|
33
|
+
function buildClients(privateKey) {
|
|
33
34
|
const account = privateKeyToAccount(privateKey);
|
|
35
|
+
const transport = http('https://mainnet.base.org');
|
|
36
|
+
const walletClient = createWalletClient({ account, chain: base, transport });
|
|
37
|
+
const publicClient = createPublicClient({ chain: base, transport });
|
|
38
|
+
return { walletClient, publicClient, account };
|
|
39
|
+
}
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Send a single USDC transfer on Base mainnet and wait for confirmation.
|
|
43
|
+
* Caller is responsible for balance checks.
|
|
44
|
+
*
|
|
45
|
+
* @param {{ walletClient, publicClient }} clients
|
|
46
|
+
* @param {string} toAddress
|
|
47
|
+
* @param {bigint} amountRaw - amount in micro-USDC (6 decimals)
|
|
48
|
+
* @returns {{ txHash: string, explorer: string }}
|
|
49
|
+
*/
|
|
50
|
+
async function sendUsdcRaw(clients, toAddress, amountRaw) {
|
|
51
|
+
const { walletClient, publicClient } = clients;
|
|
40
52
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
const txHash = await walletClient.writeContract({
|
|
54
|
+
address: USDC_CONTRACT,
|
|
55
|
+
abi: USDC_ABI,
|
|
56
|
+
functionName: 'transfer',
|
|
57
|
+
args: [toAddress, amountRaw],
|
|
44
58
|
});
|
|
45
59
|
|
|
60
|
+
await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
txHash,
|
|
64
|
+
explorer: `https://basescan.org/tx/${txHash}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Send USDC payment on Base mainnet (legacy mode — 100% to one recipient).
|
|
70
|
+
* @param {string} privateKey - Hex private key (with 0x prefix)
|
|
71
|
+
* @param {string} toAddress - Recipient wallet address
|
|
72
|
+
* @param {number} amountUsdc - Amount in USDC (e.g., 0.005)
|
|
73
|
+
* @returns {{ txHash: string, explorer: string, from: string, amount: number }}
|
|
74
|
+
*/
|
|
75
|
+
export async function sendUsdcPayment(privateKey, toAddress, amountUsdc) {
|
|
76
|
+
const { walletClient, publicClient, account } = buildClients(privateKey);
|
|
77
|
+
|
|
46
78
|
// Convert USDC amount to 6-decimal units
|
|
47
79
|
const amount = parseUnits(amountUsdc.toString(), 6);
|
|
48
80
|
|
|
@@ -59,22 +91,116 @@ export async function sendUsdcPayment(privateKey, toAddress, amountUsdc) {
|
|
|
59
91
|
throw new Error(`Insufficient USDC balance: ${balanceUsdc.toFixed(6)} USDC (need ${amountUsdc} USDC)`);
|
|
60
92
|
}
|
|
61
93
|
|
|
62
|
-
|
|
63
|
-
|
|
94
|
+
const { txHash, explorer } = await sendUsdcRaw({ walletClient, publicClient }, toAddress, amount);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
txHash,
|
|
98
|
+
explorer,
|
|
99
|
+
from: account.address,
|
|
100
|
+
amount: amountUsdc,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Send a split USDC payment on Base mainnet (native split mode — 95% to provider, 5% to platform).
|
|
106
|
+
*
|
|
107
|
+
* The split amounts are derived from the server-provided `split` object when available,
|
|
108
|
+
* or computed with floor arithmetic to guarantee provider + platform = total exactly.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} privateKey - Hex private key (with 0x prefix)
|
|
111
|
+
* @param {object} splitDetails
|
|
112
|
+
* @param {number} splitDetails.totalAmountUsdc - Total price in USDC (e.g., 0.01)
|
|
113
|
+
* @param {string} splitDetails.providerWallet - Provider wallet address (95%)
|
|
114
|
+
* @param {string} splitDetails.platformWallet - Platform wallet address (5%)
|
|
115
|
+
* @param {object|null} [splitDetails.serverSplit] - Optional split object from server 402 response
|
|
116
|
+
* @param {number} [splitDetails.serverSplit.provider_amount] - Provider amount in USDC from server
|
|
117
|
+
* @param {number} [splitDetails.serverSplit.platform_amount] - Platform amount in USDC from server
|
|
118
|
+
*
|
|
119
|
+
* @returns {{
|
|
120
|
+
* txHashProvider: string,
|
|
121
|
+
* txHashPlatform: string,
|
|
122
|
+
* explorerProvider: string,
|
|
123
|
+
* explorerPlatform: string,
|
|
124
|
+
* from: string,
|
|
125
|
+
* providerAmountUsdc: number,
|
|
126
|
+
* platformAmountUsdc: number,
|
|
127
|
+
* }}
|
|
128
|
+
*
|
|
129
|
+
* @throws {Error} If total amount is too small for a meaningful split (< 0.0001 USDC)
|
|
130
|
+
* @throws {Error} If USDC balance is insufficient for the total amount
|
|
131
|
+
*/
|
|
132
|
+
export async function sendSplitUsdcPayment(privateKey, splitDetails) {
|
|
133
|
+
const {
|
|
134
|
+
totalAmountUsdc,
|
|
135
|
+
providerWallet,
|
|
136
|
+
platformWallet,
|
|
137
|
+
serverSplit = null,
|
|
138
|
+
} = splitDetails;
|
|
139
|
+
|
|
140
|
+
const { walletClient, publicClient, account } = buildClients(privateKey);
|
|
141
|
+
|
|
142
|
+
// Compute raw amounts (6 decimals).
|
|
143
|
+
// Use server-provided amounts when present to avoid client/server rounding divergence.
|
|
144
|
+
let providerAmountRaw;
|
|
145
|
+
let platformAmountRaw;
|
|
146
|
+
|
|
147
|
+
if (serverSplit && serverSplit.provider_amount != null && serverSplit.platform_amount != null) {
|
|
148
|
+
providerAmountRaw = parseUnits(serverSplit.provider_amount.toString(), 6);
|
|
149
|
+
platformAmountRaw = parseUnits(serverSplit.platform_amount.toString(), 6);
|
|
150
|
+
} else {
|
|
151
|
+
const totalRaw = parseUnits(totalAmountUsdc.toString(), 6);
|
|
152
|
+
providerAmountRaw = (totalRaw * 95n) / 100n; // floor division via BigInt
|
|
153
|
+
platformAmountRaw = totalRaw - providerAmountRaw; // guarantees sum = total
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const totalRawForCheck = providerAmountRaw + platformAmountRaw;
|
|
157
|
+
|
|
158
|
+
// Guard: minimum split amount
|
|
159
|
+
if (providerAmountRaw < MIN_SPLIT_AMOUNT_RAW || platformAmountRaw === 0n) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Amount too small for split payment (minimum 0.0001 USDC). ` +
|
|
162
|
+
`Provider share would be ${Number(providerAmountRaw)} micro-USDC.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check balance for the full total
|
|
167
|
+
const balance = await publicClient.readContract({
|
|
64
168
|
address: USDC_CONTRACT,
|
|
65
169
|
abi: USDC_ABI,
|
|
66
|
-
functionName: '
|
|
67
|
-
args: [
|
|
170
|
+
functionName: 'balanceOf',
|
|
171
|
+
args: [account.address],
|
|
68
172
|
});
|
|
69
173
|
|
|
70
|
-
|
|
71
|
-
|
|
174
|
+
if (balance < totalRawForCheck) {
|
|
175
|
+
const balanceUsdc = Number(balance) / 1_000_000;
|
|
176
|
+
const needUsdc = Number(totalRawForCheck) / 1_000_000;
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Insufficient USDC balance: ${balanceUsdc.toFixed(6)} USDC (need ${needUsdc.toFixed(6)} USDC for split payment)`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Transaction 1 — provider (95%)
|
|
183
|
+
const providerResult = await sendUsdcRaw(
|
|
184
|
+
{ walletClient, publicClient },
|
|
185
|
+
providerWallet,
|
|
186
|
+
providerAmountRaw
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Transaction 2 — platform (5%)
|
|
190
|
+
const platformResult = await sendUsdcRaw(
|
|
191
|
+
{ walletClient, publicClient },
|
|
192
|
+
platformWallet,
|
|
193
|
+
platformAmountRaw
|
|
194
|
+
);
|
|
72
195
|
|
|
73
196
|
return {
|
|
74
|
-
txHash,
|
|
75
|
-
|
|
197
|
+
txHashProvider: providerResult.txHash,
|
|
198
|
+
txHashPlatform: platformResult.txHash,
|
|
199
|
+
explorerProvider: providerResult.explorer,
|
|
200
|
+
explorerPlatform: platformResult.explorer,
|
|
76
201
|
from: account.address,
|
|
77
|
-
|
|
202
|
+
providerAmountUsdc: Number(providerAmountRaw) / 1_000_000,
|
|
203
|
+
platformAmountUsdc: Number(platformAmountRaw) / 1_000_000,
|
|
78
204
|
};
|
|
79
205
|
}
|
|
80
206
|
|