@paylobster/cli 4.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.
Files changed (89) hide show
  1. package/BUILD_SUMMARY.md +429 -0
  2. package/CHANGELOG.md +78 -0
  3. package/CONTRIBUTING.md +368 -0
  4. package/EXAMPLES.md +432 -0
  5. package/LICENSE +21 -0
  6. package/QUICKSTART.md +189 -0
  7. package/README.md +377 -0
  8. package/TEST_REPORT.md +191 -0
  9. package/bin/plob.js +9 -0
  10. package/bin/plob.ts +9 -0
  11. package/demo.sh +154 -0
  12. package/dist/bin/plob.d.ts +7 -0
  13. package/dist/bin/plob.d.ts.map +1 -0
  14. package/dist/bin/plob.js +10 -0
  15. package/dist/bin/plob.js.map +1 -0
  16. package/dist/src/commands/auth.d.ts +3 -0
  17. package/dist/src/commands/auth.d.ts.map +1 -0
  18. package/dist/src/commands/auth.js +75 -0
  19. package/dist/src/commands/auth.js.map +1 -0
  20. package/dist/src/commands/config.d.ts +3 -0
  21. package/dist/src/commands/config.d.ts.map +1 -0
  22. package/dist/src/commands/config.js +79 -0
  23. package/dist/src/commands/config.js.map +1 -0
  24. package/dist/src/commands/escrow.d.ts +3 -0
  25. package/dist/src/commands/escrow.d.ts.map +1 -0
  26. package/dist/src/commands/escrow.js +193 -0
  27. package/dist/src/commands/escrow.js.map +1 -0
  28. package/dist/src/commands/mandate.d.ts +8 -0
  29. package/dist/src/commands/mandate.d.ts.map +1 -0
  30. package/dist/src/commands/mandate.js +54 -0
  31. package/dist/src/commands/mandate.js.map +1 -0
  32. package/dist/src/commands/pay.d.ts +6 -0
  33. package/dist/src/commands/pay.d.ts.map +1 -0
  34. package/dist/src/commands/pay.js +77 -0
  35. package/dist/src/commands/pay.js.map +1 -0
  36. package/dist/src/commands/register.d.ts +3 -0
  37. package/dist/src/commands/register.d.ts.map +1 -0
  38. package/dist/src/commands/register.js +51 -0
  39. package/dist/src/commands/register.js.map +1 -0
  40. package/dist/src/commands/reputation.d.ts +3 -0
  41. package/dist/src/commands/reputation.d.ts.map +1 -0
  42. package/dist/src/commands/reputation.js +116 -0
  43. package/dist/src/commands/reputation.js.map +1 -0
  44. package/dist/src/commands/status.d.ts +3 -0
  45. package/dist/src/commands/status.d.ts.map +1 -0
  46. package/dist/src/commands/status.js +82 -0
  47. package/dist/src/commands/status.js.map +1 -0
  48. package/dist/src/index.d.ts +3 -0
  49. package/dist/src/index.d.ts.map +1 -0
  50. package/dist/src/index.js +59 -0
  51. package/dist/src/index.js.map +1 -0
  52. package/dist/src/lib/config.d.ts +26 -0
  53. package/dist/src/lib/config.d.ts.map +1 -0
  54. package/dist/src/lib/config.js +91 -0
  55. package/dist/src/lib/config.js.map +1 -0
  56. package/dist/src/lib/contracts.d.ts +18798 -0
  57. package/dist/src/lib/contracts.d.ts.map +1 -0
  58. package/dist/src/lib/contracts.js +361 -0
  59. package/dist/src/lib/contracts.js.map +1 -0
  60. package/dist/src/lib/display.d.ts +83 -0
  61. package/dist/src/lib/display.d.ts.map +1 -0
  62. package/dist/src/lib/display.js +293 -0
  63. package/dist/src/lib/display.js.map +1 -0
  64. package/dist/src/lib/types.d.ts +49 -0
  65. package/dist/src/lib/types.d.ts.map +1 -0
  66. package/dist/src/lib/types.js +3 -0
  67. package/dist/src/lib/types.js.map +1 -0
  68. package/dist/src/lib/wallet.d.ts +30 -0
  69. package/dist/src/lib/wallet.d.ts.map +1 -0
  70. package/dist/src/lib/wallet.js +143 -0
  71. package/dist/src/lib/wallet.js.map +1 -0
  72. package/jest.config.js +15 -0
  73. package/package.json +55 -0
  74. package/src/__tests__/cli.test.ts +38 -0
  75. package/src/commands/auth.ts +75 -0
  76. package/src/commands/config.ts +84 -0
  77. package/src/commands/escrow.ts +222 -0
  78. package/src/commands/mandate.ts +56 -0
  79. package/src/commands/pay.ts +96 -0
  80. package/src/commands/register.ts +57 -0
  81. package/src/commands/reputation.ts +84 -0
  82. package/src/commands/status.ts +91 -0
  83. package/src/index.ts +63 -0
  84. package/src/lib/config.ts +90 -0
  85. package/src/lib/contracts.ts +392 -0
  86. package/src/lib/display.ts +265 -0
  87. package/src/lib/types.ts +57 -0
  88. package/src/lib/wallet.ts +146 -0
  89. package/tsconfig.json +21 -0
@@ -0,0 +1,91 @@
1
+ import { Command } from 'commander';
2
+ import { getAgentInfo, getReputation, getCreditStatus, getBalance, getUserEscrowCount, formatUSDC, formatETH } from '../lib/contracts';
3
+ import { getWalletAddress } from '../lib/wallet';
4
+ import { loadConfig } from '../lib/config';
5
+ import { error, outputJSON, printAgentHeader, formatBoolean, formatNumber, printReputation, formatNetwork } from '../lib/display';
6
+ import type { OutputOptions } from '../lib/types';
7
+
8
+ export function createStatusCommand(): Command {
9
+ const cmd = new Command('status')
10
+ .description('Show agent status and balances')
11
+ .option('--json', 'Output as JSON')
12
+ .action(async (options: OutputOptions) => {
13
+ try {
14
+ const address = getWalletAddress() as `0x${string}`;
15
+ const config = loadConfig();
16
+
17
+ // Fetch all data in parallel
18
+ const [agentInfo, reputation, creditStatus, balance, escrowCount] = await Promise.all([
19
+ getAgentInfo(address),
20
+ getReputation(address),
21
+ getCreditStatus(address),
22
+ getBalance(address),
23
+ getUserEscrowCount(address),
24
+ ]);
25
+
26
+ // Calculate reputation score (0-100)
27
+ const reputationScore = Number(reputation.score);
28
+
29
+ // Format balances
30
+ const usdcBalance = formatUSDC(balance.usdc);
31
+ const ethBalance = formatETH(balance.eth);
32
+
33
+ // Format credit
34
+ const creditLimit = formatUSDC(creditStatus.limit);
35
+ const creditAvailable = formatUSDC(creditStatus.available);
36
+ const creditInUse = formatUSDC(creditStatus.inUse);
37
+
38
+ if (outputJSON({
39
+ name: agentInfo.name,
40
+ address,
41
+ network: config.network,
42
+ registered: agentInfo.registered,
43
+ tokenId: agentInfo.tokenId.toString(),
44
+ balance: {
45
+ usdc: usdcBalance,
46
+ eth: ethBalance,
47
+ },
48
+ credit: {
49
+ limit: creditLimit,
50
+ available: creditAvailable,
51
+ inUse: creditInUse,
52
+ },
53
+ reputation: reputationScore,
54
+ escrows: escrowCount,
55
+ }, options)) {
56
+ return;
57
+ }
58
+
59
+ // Pretty output
60
+ printAgentHeader(agentInfo.name || 'Not Registered', address);
61
+
62
+ console.log('Network: ', formatNetwork(config.network));
63
+ console.log('Registered: ', formatBoolean(agentInfo.registered));
64
+
65
+ if (agentInfo.registered) {
66
+ console.log('Agent ID: ', agentInfo.tokenId.toString());
67
+ }
68
+
69
+ console.log();
70
+ console.log('💰 Balances');
71
+ console.log(' USDC: ', formatNumber(usdcBalance), 'USDC');
72
+ console.log(' ETH: ', formatNumber(ethBalance), 'ETH');
73
+
74
+ console.log();
75
+ console.log('💳 Credit');
76
+ console.log(' Limit: ', formatNumber(creditLimit), 'USDC');
77
+ console.log(' Available:', formatNumber(creditAvailable), 'USDC');
78
+ console.log(' In Use: ', formatNumber(creditInUse), 'USDC');
79
+
80
+ console.log();
81
+ console.log('⭐ Reputation:', printReputation(reputationScore));
82
+ console.log('📦 Escrows: ', escrowCount, 'total');
83
+ console.log();
84
+ } catch (err) {
85
+ error(`Failed to fetch status: ${err}`);
86
+ process.exit(1);
87
+ }
88
+ });
89
+
90
+ return cmd;
91
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createAuthCommand } from './commands/auth';
5
+ import { createConfigCommand } from './commands/config';
6
+ import { createRegisterCommand } from './commands/register';
7
+ import { createStatusCommand } from './commands/status';
8
+ import { createEscrowCommand } from './commands/escrow';
9
+ import { createMandateCommand } from './commands/mandate';
10
+ import { createPayCommand } from './commands/pay';
11
+ import { createReputationCommand } from './commands/reputation';
12
+ import chalk from 'chalk';
13
+
14
+ // Load environment variables from .env file if present
15
+ import dotenv from 'dotenv';
16
+ dotenv.config();
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name('plob')
22
+ .description('🦞 PayLobster CLI - Payment infrastructure for AI agents')
23
+ .version('4.0.0');
24
+
25
+ // ASCII art banner
26
+ const banner = `
27
+ ${chalk.cyan('┌─────────────────────────────────┐')}
28
+ ${chalk.cyan('│')} ${chalk.bold.cyan('🦞 PayLobster CLI')} ${chalk.cyan('│')}
29
+ ${chalk.cyan('│')} Payment infrastructure for AI ${chalk.cyan('│')}
30
+ ${chalk.cyan('└─────────────────────────────────┘')}
31
+ `;
32
+
33
+ // Show banner on help
34
+ program.on('--help', () => {
35
+ console.log(banner);
36
+ });
37
+
38
+ // Add commands
39
+ program.addCommand(createAuthCommand());
40
+ program.addCommand(createConfigCommand());
41
+ program.addCommand(createRegisterCommand());
42
+ program.addCommand(createStatusCommand());
43
+ program.addCommand(createEscrowCommand());
44
+ program.addCommand(createMandateCommand());
45
+ program.addCommand(createPayCommand());
46
+ program.addCommand(createReputationCommand());
47
+
48
+ // Handle errors
49
+ program.exitOverride((err) => {
50
+ if (err.code === 'commander.help') {
51
+ console.log(banner);
52
+ }
53
+ process.exit(err.exitCode);
54
+ });
55
+
56
+ // Parse arguments
57
+ program.parse(process.argv);
58
+
59
+ // Show help if no command provided
60
+ if (!process.argv.slice(2).length) {
61
+ console.log(banner);
62
+ program.outputHelp();
63
+ }
@@ -0,0 +1,90 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import type { CLIConfig, Network } from './types';
5
+
6
+ const CONFIG_DIR = path.join(os.homedir(), '.plob');
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
8
+
9
+ // Default configuration
10
+ const DEFAULT_CONFIG: CLIConfig = {
11
+ network: 'sepolia',
12
+ indexerUrl: 'https://api.thegraph.com/subgraphs/name/paylobster/registry',
13
+ };
14
+
15
+ /**
16
+ * Load CLI configuration from ~/.plob/config.json
17
+ */
18
+ export function loadConfig(): CLIConfig {
19
+ try {
20
+ if (!fs.existsSync(CONFIG_FILE)) {
21
+ return DEFAULT_CONFIG;
22
+ }
23
+ const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
24
+ return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
25
+ } catch (error) {
26
+ return DEFAULT_CONFIG;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Save CLI configuration to ~/.plob/config.json
32
+ */
33
+ export function saveConfig(config: Partial<CLIConfig>): void {
34
+ try {
35
+ // Ensure config directory exists
36
+ if (!fs.existsSync(CONFIG_DIR)) {
37
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
+ }
39
+
40
+ const currentConfig = loadConfig();
41
+ const newConfig = { ...currentConfig, ...config };
42
+
43
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
44
+ } catch (error) {
45
+ throw new Error(`Failed to save config: ${error}`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get a specific config value
51
+ */
52
+ export function getConfigValue(key: keyof CLIConfig): any {
53
+ const config = loadConfig();
54
+ return config[key];
55
+ }
56
+
57
+ /**
58
+ * Set a specific config value
59
+ */
60
+ export function setConfigValue(key: keyof CLIConfig, value: any): void {
61
+ const config = loadConfig();
62
+ (config as any)[key] = value;
63
+ saveConfig(config);
64
+ }
65
+
66
+ /**
67
+ * Get RPC URL for the configured network
68
+ */
69
+ export function getRpcUrl(network?: Network): string {
70
+ const config = loadConfig();
71
+ const net = network || config.network;
72
+
73
+ if (config.rpcUrl) {
74
+ return config.rpcUrl;
75
+ }
76
+
77
+ // Default public RPCs
78
+ return net === 'mainnet'
79
+ ? 'https://mainnet.base.org'
80
+ : 'https://sepolia.base.org';
81
+ }
82
+
83
+ /**
84
+ * Clear all configuration
85
+ */
86
+ export function clearConfig(): void {
87
+ if (fs.existsSync(CONFIG_FILE)) {
88
+ fs.unlinkSync(CONFIG_FILE);
89
+ }
90
+ }
@@ -0,0 +1,392 @@
1
+ import { createPublicClient, http, type Address, formatUnits, parseUnits } from 'viem';
2
+ import { base, baseSepolia } from 'viem/chains';
3
+ import { loadWallet } from './wallet';
4
+ import { loadConfig, getRpcUrl } from './config';
5
+ import type { Network, AgentInfo, Reputation, CreditStatus, EscrowInfo, Balance } from './types';
6
+
7
+ // Contract addresses
8
+ const CONTRACTS_MAINNET = {
9
+ IDENTITY: '0xA174ee274F870631B3c330a85EBCad74120BE662' as Address,
10
+ REPUTATION: '0x02bb4132a86134684976E2a52E43D59D89E64b29' as Address,
11
+ CREDIT: '0xD9241Ce8a721Ef5fcCAc5A11983addC526eC80E1' as Address,
12
+ ESCROW: '0x49EdEe04c78B7FeD5248A20706c7a6c540748806' as Address,
13
+ USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as Address,
14
+ };
15
+
16
+ const CONTRACTS_SEPOLIA = {
17
+ IDENTITY: '0x3dfA02Ed4F0e4F10E8031d7a4cB8Ea0bBbFbCB8c' as Address,
18
+ // NOTE: Reputation address provided has 41 hex chars (invalid). Using placeholder.
19
+ // Original (invalid): 0xb0033901e3b94f4F36dA0b3e396942C1e78205C7d
20
+ REPUTATION: '0x0000000000000000000000000000000000000000' as Address, // TODO: Fix this address
21
+ CREDIT: '0xBA64e2b2F2a80D03A4B13b3396942C1e78205C7d' as Address,
22
+ ESCROW: '0x78D1f50a1965dE34f6b5a3D3546C94FE1809Cd82' as Address,
23
+ USDC: '0x036CbD53842c5426634e7929541eC2318f3dCF7e' as Address,
24
+ };
25
+
26
+ // ABIs
27
+ const IDENTITY_ABI = [
28
+ {
29
+ inputs: [
30
+ { name: 'agentURI', type: 'string' },
31
+ { name: 'name', type: 'string' },
32
+ { name: 'capabilities', type: 'string' },
33
+ ],
34
+ name: 'register',
35
+ outputs: [{ name: 'agentId', type: 'uint256' }],
36
+ stateMutability: 'nonpayable',
37
+ type: 'function',
38
+ },
39
+ {
40
+ inputs: [{ name: 'user', type: 'address' }],
41
+ name: 'getAgentInfo',
42
+ outputs: [
43
+ { name: 'name', type: 'string' },
44
+ { name: 'tokenId', type: 'uint256' },
45
+ { name: 'registered', type: 'bool' },
46
+ ],
47
+ stateMutability: 'view',
48
+ type: 'function',
49
+ },
50
+ ] as const;
51
+
52
+ const REPUTATION_ABI = [
53
+ {
54
+ inputs: [{ name: 'user', type: 'address' }],
55
+ name: 'getReputation',
56
+ outputs: [
57
+ { name: 'score', type: 'uint256' },
58
+ { name: 'trustVector', type: 'uint256' },
59
+ ],
60
+ stateMutability: 'view',
61
+ type: 'function',
62
+ },
63
+ ] as const;
64
+
65
+ const CREDIT_ABI = [
66
+ {
67
+ inputs: [{ name: 'user', type: 'address' }],
68
+ name: 'getCreditStatus',
69
+ outputs: [
70
+ { name: 'limit', type: 'uint256' },
71
+ { name: 'available', type: 'uint256' },
72
+ { name: 'inUse', type: 'uint256' },
73
+ ],
74
+ stateMutability: 'view',
75
+ type: 'function',
76
+ },
77
+ ] as const;
78
+
79
+ const ESCROW_ABI = [
80
+ {
81
+ inputs: [
82
+ { name: 'recipient', type: 'address' },
83
+ { name: 'amount', type: 'uint256' },
84
+ { name: 'token', type: 'address' },
85
+ { name: 'description', type: 'string' },
86
+ ],
87
+ name: 'createEscrow',
88
+ outputs: [{ name: 'escrowId', type: 'uint256' }],
89
+ stateMutability: 'nonpayable',
90
+ type: 'function',
91
+ },
92
+ {
93
+ inputs: [{ name: 'escrowId', type: 'uint256' }],
94
+ name: 'releaseEscrow',
95
+ outputs: [],
96
+ stateMutability: 'nonpayable',
97
+ type: 'function',
98
+ },
99
+ {
100
+ inputs: [{ name: 'escrowId', type: 'uint256' }],
101
+ name: 'getEscrow',
102
+ outputs: [
103
+ { name: 'sender', type: 'address' },
104
+ { name: 'recipient', type: 'address' },
105
+ { name: 'amount', type: 'uint256' },
106
+ { name: 'token', type: 'address' },
107
+ { name: 'status', type: 'uint8' },
108
+ ],
109
+ stateMutability: 'view',
110
+ type: 'function',
111
+ },
112
+ ] as const;
113
+
114
+ const USDC_ABI = [
115
+ {
116
+ inputs: [
117
+ { name: 'spender', type: 'address' },
118
+ { name: 'amount', type: 'uint256' },
119
+ ],
120
+ name: 'approve',
121
+ outputs: [{ name: '', type: 'bool' }],
122
+ stateMutability: 'nonpayable',
123
+ type: 'function',
124
+ },
125
+ {
126
+ inputs: [{ name: 'account', type: 'address' }],
127
+ name: 'balanceOf',
128
+ outputs: [{ name: '', type: 'uint256' }],
129
+ stateMutability: 'view',
130
+ type: 'function',
131
+ },
132
+ ] as const;
133
+
134
+ /**
135
+ * Get contract addresses for network
136
+ */
137
+ export function getContracts(network: Network) {
138
+ return network === 'mainnet' ? CONTRACTS_MAINNET : CONTRACTS_SEPOLIA;
139
+ }
140
+
141
+ /**
142
+ * Create public client for reading
143
+ */
144
+ export function getPublicClient(network?: Network) {
145
+ const config = loadConfig();
146
+ const net = network || config.network;
147
+ const chain = net === 'mainnet' ? base : baseSepolia;
148
+
149
+ return createPublicClient({
150
+ chain,
151
+ transport: http(getRpcUrl(net)),
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Register agent identity
157
+ */
158
+ export async function registerAgent(name: string, capabilities: string): Promise<bigint> {
159
+ const { client, account } = await loadWallet();
160
+ const config = loadConfig();
161
+ const contracts = getContracts(config.network);
162
+ const chain = config.network === 'mainnet' ? base : baseSepolia;
163
+
164
+ const hash = await client.writeContract({
165
+ address: contracts.IDENTITY,
166
+ abi: IDENTITY_ABI,
167
+ functionName: 'register',
168
+ args: ['', name, capabilities],
169
+ account,
170
+ chain,
171
+ });
172
+
173
+ return BigInt(hash);
174
+ }
175
+
176
+ /**
177
+ * Get agent info
178
+ */
179
+ export async function getAgentInfo(address: Address): Promise<AgentInfo> {
180
+ const config = loadConfig();
181
+ const publicClient = getPublicClient(config.network);
182
+ const contracts = getContracts(config.network);
183
+
184
+ const result = await publicClient.readContract({
185
+ address: contracts.IDENTITY,
186
+ abi: IDENTITY_ABI,
187
+ functionName: 'getAgentInfo',
188
+ args: [address],
189
+ });
190
+
191
+ return {
192
+ name: result[0],
193
+ tokenId: result[1],
194
+ registered: result[2],
195
+ address,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Get reputation score
201
+ */
202
+ export async function getReputation(address: Address): Promise<Reputation> {
203
+ const config = loadConfig();
204
+ const publicClient = getPublicClient(config.network);
205
+ const contracts = getContracts(config.network);
206
+
207
+ const result = await publicClient.readContract({
208
+ address: contracts.REPUTATION,
209
+ abi: REPUTATION_ABI,
210
+ functionName: 'getReputation',
211
+ args: [address],
212
+ });
213
+
214
+ return {
215
+ score: result[0],
216
+ trustVector: result[1],
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Get credit status
222
+ */
223
+ export async function getCreditStatus(address: Address): Promise<CreditStatus> {
224
+ const config = loadConfig();
225
+ const publicClient = getPublicClient(config.network);
226
+ const contracts = getContracts(config.network);
227
+
228
+ const result = await publicClient.readContract({
229
+ address: contracts.CREDIT,
230
+ abi: CREDIT_ABI,
231
+ functionName: 'getCreditStatus',
232
+ args: [address],
233
+ });
234
+
235
+ return {
236
+ limit: result[0],
237
+ available: result[1],
238
+ inUse: result[2],
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Get USDC balance
244
+ */
245
+ export async function getUSDCBalance(address: Address): Promise<bigint> {
246
+ const config = loadConfig();
247
+ const publicClient = getPublicClient(config.network);
248
+ const contracts = getContracts(config.network);
249
+
250
+ const balance = await publicClient.readContract({
251
+ address: contracts.USDC,
252
+ abi: USDC_ABI,
253
+ functionName: 'balanceOf',
254
+ args: [address],
255
+ });
256
+
257
+ return balance;
258
+ }
259
+
260
+ /**
261
+ * Get ETH balance
262
+ */
263
+ export async function getETHBalance(address: Address): Promise<bigint> {
264
+ const config = loadConfig();
265
+ const publicClient = getPublicClient(config.network);
266
+
267
+ const balance = await publicClient.getBalance({ address });
268
+ return balance;
269
+ }
270
+
271
+ /**
272
+ * Get full balance info
273
+ */
274
+ export async function getBalance(address: Address): Promise<Balance> {
275
+ const [usdc, eth] = await Promise.all([
276
+ getUSDCBalance(address),
277
+ getETHBalance(address),
278
+ ]);
279
+
280
+ return { usdc, eth };
281
+ }
282
+
283
+ /**
284
+ * Create escrow
285
+ */
286
+ export async function createEscrow(
287
+ recipient: Address,
288
+ amount: string,
289
+ description: string
290
+ ): Promise<bigint> {
291
+ const { client, account } = await loadWallet();
292
+ const config = loadConfig();
293
+ const contracts = getContracts(config.network);
294
+ const chain = config.network === 'mainnet' ? base : baseSepolia;
295
+
296
+ // Parse amount (USDC has 6 decimals)
297
+ const amountWei = parseUnits(amount, 6);
298
+
299
+ // First approve USDC
300
+ const approveHash = await client.writeContract({
301
+ address: contracts.USDC,
302
+ abi: USDC_ABI,
303
+ functionName: 'approve',
304
+ args: [contracts.ESCROW, amountWei],
305
+ account,
306
+ chain,
307
+ });
308
+
309
+ // Wait for approval
310
+ const publicClient = getPublicClient(config.network);
311
+ await publicClient.waitForTransactionReceipt({ hash: approveHash });
312
+
313
+ // Create escrow
314
+ const hash = await client.writeContract({
315
+ address: contracts.ESCROW,
316
+ abi: ESCROW_ABI,
317
+ functionName: 'createEscrow',
318
+ args: [recipient, amountWei, contracts.USDC, description],
319
+ account,
320
+ chain,
321
+ });
322
+
323
+ return BigInt(hash);
324
+ }
325
+
326
+ /**
327
+ * Release escrow
328
+ */
329
+ export async function releaseEscrow(escrowId: bigint): Promise<string> {
330
+ const { client, account } = await loadWallet();
331
+ const config = loadConfig();
332
+ const contracts = getContracts(config.network);
333
+ const chain = config.network === 'mainnet' ? base : baseSepolia;
334
+
335
+ const hash = await client.writeContract({
336
+ address: contracts.ESCROW,
337
+ abi: ESCROW_ABI,
338
+ functionName: 'releaseEscrow',
339
+ args: [escrowId],
340
+ account,
341
+ chain,
342
+ });
343
+
344
+ return hash;
345
+ }
346
+
347
+ /**
348
+ * Get escrow details
349
+ */
350
+ export async function getEscrow(escrowId: bigint): Promise<EscrowInfo> {
351
+ const config = loadConfig();
352
+ const publicClient = getPublicClient(config.network);
353
+ const contracts = getContracts(config.network);
354
+
355
+ const result = await publicClient.readContract({
356
+ address: contracts.ESCROW,
357
+ abi: ESCROW_ABI,
358
+ functionName: 'getEscrow',
359
+ args: [escrowId],
360
+ });
361
+
362
+ return {
363
+ sender: result[0],
364
+ recipient: result[1],
365
+ amount: result[2],
366
+ token: result[3],
367
+ status: result[4],
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Get user's escrow count
373
+ * NOTE: This function is not yet implemented on the deployed contract
374
+ */
375
+ export async function getUserEscrowCount(address: Address): Promise<number> {
376
+ // TODO: Implement when Escrow V3 is deployed with getUserTransactionCount
377
+ return 0;
378
+ }
379
+
380
+ /**
381
+ * Format USDC amount for display
382
+ */
383
+ export function formatUSDC(amount: bigint): string {
384
+ return formatUnits(amount, 6);
385
+ }
386
+
387
+ /**
388
+ * Format ETH amount for display
389
+ */
390
+ export function formatETH(amount: bigint): string {
391
+ return formatUnits(amount, 18);
392
+ }