sovereignclaw 0.1.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.
@@ -0,0 +1,557 @@
1
+ /**
2
+ * Bungee Auto Integration
3
+ * Cross-chain swaps and bridges via Bungee (mainnet)
4
+ */
5
+
6
+ import { createWalletClient, createPublicClient, http } from 'viem';
7
+ import { privateKeyToAccount, mnemonicToAccount } from 'viem/accounts';
8
+ import { base, arbitrum, optimism, mainnet, polygon } from 'viem/chains';
9
+ import { createWallet, importWallet, loadWallet, getWalletAddress, walletExists } from './wallet.js';
10
+
11
+ const BUNGEE_API = 'https://public-backend.bungee.exchange';
12
+
13
+ // Treasury for fee collection (20 bps = 0.2%)
14
+ const FEE_TAKER_ADDRESS = '0x02Bc8c352b58d929Cc3D60545511872c85F30650';
15
+ const FEE_BPS = '20'; // 0.2%
16
+
17
+ const CHAINS: Record<number, typeof base> = {
18
+ 1: mainnet,
19
+ 8453: base,
20
+ 42161: arbitrum,
21
+ 10: optimism,
22
+ 137: polygon,
23
+ };
24
+
25
+ // ============ Token List ============
26
+
27
+ export interface TokenBalance {
28
+ chainId: number;
29
+ address: string;
30
+ name: string;
31
+ symbol: string;
32
+ decimals: number;
33
+ balance: string;
34
+ balanceInUsd: number;
35
+ }
36
+
37
+ export async function getTokenBalances(userAddress: string): Promise<TokenBalance[]> {
38
+ const url = `${BUNGEE_API}/api/v1/tokens/list?userAddress=${userAddress}`;
39
+ const response = await fetch(url);
40
+ const data = await response.json() as { success: boolean; result: Record<string, TokenBalance[]> };
41
+
42
+ if (!data.success) {
43
+ throw new Error('Failed to fetch token list');
44
+ }
45
+
46
+ const allTokens: TokenBalance[] = [];
47
+ for (const [chainId, tokens] of Object.entries(data.result)) {
48
+ for (const token of tokens) {
49
+ if (token.balanceInUsd > 0) {
50
+ allTokens.push(token);
51
+ }
52
+ }
53
+ }
54
+
55
+ return allTokens.sort((a, b) => b.balanceInUsd - a.balanceInUsd);
56
+ }
57
+
58
+ export function formatPortfolio(tokens: TokenBalance[]): string {
59
+ if (tokens.length === 0) return 'No tokens found.';
60
+
61
+ const totalUsd = tokens.reduce((sum, t) => sum + t.balanceInUsd, 0);
62
+ const lines = tokens.map(t => {
63
+ const amount = (Number(t.balance) / Math.pow(10, t.decimals)).toFixed(t.decimals > 6 ? 6 : 2);
64
+ return ` ${t.symbol} (${getChainName(t.chainId)}): ${amount} ($${t.balanceInUsd.toFixed(2)})`;
65
+ });
66
+
67
+ return `Portfolio: $${totalUsd.toFixed(2)}\n${lines.join('\n')}`;
68
+ }
69
+
70
+ function getChainName(chainId: number): string {
71
+ const names: Record<number, string> = {
72
+ 1: 'Ethereum',
73
+ 8453: 'Base',
74
+ 42161: 'Arbitrum',
75
+ 10: 'Optimism',
76
+ 137: 'Polygon',
77
+ };
78
+ return names[chainId] || `Chain ${chainId}`;
79
+ }
80
+
81
+ // ============ Bungee Auto ============
82
+
83
+ interface QuoteResult {
84
+ quoteId: string;
85
+ requestType: string;
86
+ witness: any;
87
+ signTypedData: any;
88
+ approvalData: any;
89
+ txData: { to: string; data: string; value: string; chainId: number } | null;
90
+ userOp: string | null; // 'tx' for native tokens, null for permit2
91
+ requestHash: string | null; // For native token flow
92
+ inputAmount: string;
93
+ outputAmount: string;
94
+ outputToken: string;
95
+ originChain: number;
96
+ destChain: number;
97
+ }
98
+
99
+ export async function getQuote(params: {
100
+ userAddress: string;
101
+ receiverAddress?: string;
102
+ originChainId: number;
103
+ destinationChainId: number;
104
+ inputToken: string;
105
+ outputToken: string;
106
+ inputAmount: string;
107
+ }): Promise<QuoteResult> {
108
+ const quoteParams = new URLSearchParams({
109
+ userAddress: params.userAddress,
110
+ receiverAddress: params.receiverAddress || params.userAddress,
111
+ originChainId: String(params.originChainId),
112
+ destinationChainId: String(params.destinationChainId),
113
+ inputToken: params.inputToken,
114
+ outputToken: params.outputToken,
115
+ inputAmount: params.inputAmount,
116
+ feeTakerAddress: FEE_TAKER_ADDRESS,
117
+ feeBps: FEE_BPS,
118
+ });
119
+
120
+ const url = `${BUNGEE_API}/api/v1/bungee/quote?${quoteParams}`;
121
+ const response = await fetch(url);
122
+ const data = await response.json() as any;
123
+ const serverReqId = response.headers.get('server-req-id');
124
+
125
+ if (!data.success) {
126
+ throw new Error(`Quote error: ${data.message}. server-req-id: ${serverReqId}`);
127
+ }
128
+
129
+ if (!data.result.autoRoute) {
130
+ throw new Error(`No autoRoute available. server-req-id: ${serverReqId}`);
131
+ }
132
+
133
+ const auto = data.result.autoRoute;
134
+
135
+ return {
136
+ quoteId: auto.quoteId,
137
+ requestType: auto.requestType,
138
+ witness: auto.signTypedData?.values?.witness || null,
139
+ signTypedData: auto.signTypedData,
140
+ approvalData: auto.approvalData,
141
+ txData: auto.txData || null,
142
+ userOp: auto.userOp || null,
143
+ requestHash: auto.requestHash || null,
144
+ inputAmount: params.inputAmount,
145
+ outputAmount: auto.output?.amount || auto.outputAmount,
146
+ outputToken: params.outputToken,
147
+ originChain: params.originChainId,
148
+ destChain: params.destinationChainId,
149
+ };
150
+ }
151
+
152
+ export async function executeSwap(
153
+ privateKeyOrMnemonic: string,
154
+ quote: QuoteResult,
155
+ onStatus?: (status: string) => void
156
+ ): Promise<{ requestHash: string; status: any }> {
157
+ const log = onStatus || console.log;
158
+
159
+ // Handle mnemonic vs private key
160
+ let account;
161
+ if (privateKeyOrMnemonic.includes(' ')) {
162
+ account = mnemonicToAccount(privateKeyOrMnemonic);
163
+ } else {
164
+ const normalizedKey = privateKeyOrMnemonic.startsWith('0x') ? privateKeyOrMnemonic : `0x${privateKeyOrMnemonic}`;
165
+ account = privateKeyToAccount(normalizedKey as `0x${string}`);
166
+ }
167
+
168
+ const chain = CHAINS[quote.originChain];
169
+ if (!chain) throw new Error(`Unsupported chain: ${quote.originChain}`);
170
+
171
+ const publicClient = createPublicClient({ chain, transport: http() });
172
+ const walletClient = createWalletClient({ account, chain, transport: http() });
173
+
174
+ // Native token flow (userOp = 'tx')
175
+ if (quote.userOp === 'tx' && quote.txData) {
176
+ return executeNativeTokenSwap(publicClient, walletClient, quote, log);
177
+ }
178
+
179
+ // ERC20 token flow (permit2 signature)
180
+ return executePermit2Swap(publicClient, walletClient, account, quote, log);
181
+ }
182
+
183
+ async function executeNativeTokenSwap(
184
+ publicClient: any,
185
+ walletClient: any,
186
+ quote: QuoteResult,
187
+ log: (msg: string) => void
188
+ ): Promise<{ requestHash: string; status: any }> {
189
+ if (!quote.txData) throw new Error('Missing txData for native token swap');
190
+ if (!quote.requestHash) throw new Error('Missing requestHash for native token swap');
191
+
192
+ log('🔄 Native token swap (direct tx)');
193
+
194
+ // Step 1: Send the transaction
195
+ log('📤 Step 1/2: Sending transaction...');
196
+ const hash = await walletClient.sendTransaction({
197
+ to: quote.txData.to as `0x${string}`,
198
+ data: quote.txData.data as `0x${string}`,
199
+ value: BigInt(quote.txData.value),
200
+ });
201
+ log(` TX hash: ${hash}`);
202
+
203
+ // Wait for confirmation
204
+ log(' Waiting for confirmation...');
205
+ await publicClient.waitForTransactionReceipt({ hash });
206
+ log(' ✅ Transaction confirmed');
207
+
208
+ // Step 2: Poll for completion using requestHash from quote
209
+ log('âŗ Step 2/2: Waiting for Bungee completion...');
210
+ const finalStatus = await pollStatus(quote.requestHash, log);
211
+
212
+ return { requestHash: quote.requestHash, status: finalStatus };
213
+ }
214
+
215
+ async function executePermit2Swap(
216
+ publicClient: any,
217
+ walletClient: any,
218
+ account: any,
219
+ quote: QuoteResult,
220
+ log: (msg: string) => void
221
+ ): Promise<{ requestHash: string; status: any }> {
222
+ log('🔄 ERC20 token swap (permit2)');
223
+
224
+ // Step 1: Handle approval if needed
225
+ if (quote.approvalData?.tokenAddress) {
226
+ log('✅ Step 1/4: Checking token approval...');
227
+ await handleApproval(publicClient, walletClient, account.address, quote.approvalData, log);
228
+ } else {
229
+ log('✅ Step 1/4: No approval needed');
230
+ }
231
+
232
+ // Step 2: Sign the typed data
233
+ if (!quote.signTypedData || !quote.witness) {
234
+ throw new Error('Missing signTypedData or witness - cannot proceed');
235
+ }
236
+
237
+ log('âœī¸ Step 2/4: Signing permit...');
238
+ const signature = await account.signTypedData({
239
+ types: quote.signTypedData.types,
240
+ primaryType: 'PermitWitnessTransferFrom',
241
+ message: quote.signTypedData.values,
242
+ domain: quote.signTypedData.domain,
243
+ });
244
+
245
+ // Step 3: Submit to Bungee
246
+ log('📤 Step 3/4: Submitting to Bungee...');
247
+ const submitPayload = {
248
+ requestType: quote.requestType,
249
+ request: quote.witness,
250
+ userSignature: signature,
251
+ quoteId: quote.quoteId,
252
+ };
253
+
254
+ const submitResponse = await fetch(`${BUNGEE_API}/api/v1/bungee/submit`, {
255
+ method: 'POST',
256
+ headers: { 'Content-Type': 'application/json' },
257
+ body: JSON.stringify(submitPayload),
258
+ });
259
+
260
+ const submitData = await submitResponse.json() as any;
261
+ if (!submitData.success) {
262
+ throw new Error(`Submit error: ${submitData.error?.message || JSON.stringify(submitData)}`);
263
+ }
264
+
265
+ const requestHash = submitData.result.requestHash;
266
+ log(` Request hash: ${requestHash}`);
267
+
268
+ // Step 4: Poll for completion
269
+ log('âŗ Step 4/4: Waiting for completion...');
270
+ const finalStatus = await pollStatus(requestHash, log);
271
+
272
+ return { requestHash, status: finalStatus };
273
+ }
274
+
275
+ async function handleApproval(
276
+ publicClient: any,
277
+ walletClient: any,
278
+ userAddress: string,
279
+ approvalData: any,
280
+ log: (msg: string) => void
281
+ ) {
282
+ const erc20Abi = [
283
+ {
284
+ inputs: [
285
+ { name: 'owner', type: 'address' },
286
+ { name: 'spender', type: 'address' },
287
+ ],
288
+ name: 'allowance',
289
+ outputs: [{ name: '', type: 'uint256' }],
290
+ stateMutability: 'view',
291
+ type: 'function',
292
+ },
293
+ {
294
+ inputs: [
295
+ { name: 'spender', type: 'address' },
296
+ { name: 'amount', type: 'uint256' },
297
+ ],
298
+ name: 'approve',
299
+ outputs: [{ name: '', type: 'bool' }],
300
+ stateMutability: 'nonpayable',
301
+ type: 'function',
302
+ },
303
+ ] as const;
304
+
305
+ const spender = approvalData.spenderAddress === '0'
306
+ ? '0x000000000022D473030F116dDEE9F6B43aC78BA3' // Permit2
307
+ : approvalData.spenderAddress;
308
+
309
+ const currentAllowance = await publicClient.readContract({
310
+ address: approvalData.tokenAddress,
311
+ abi: erc20Abi,
312
+ functionName: 'allowance',
313
+ args: [userAddress, spender],
314
+ });
315
+
316
+ if (BigInt(currentAllowance) >= BigInt(approvalData.amount)) {
317
+ log(' Already approved.');
318
+ return;
319
+ }
320
+
321
+ log(' Sending approval...');
322
+ const hash = await walletClient.writeContract({
323
+ address: approvalData.tokenAddress,
324
+ abi: erc20Abi,
325
+ functionName: 'approve',
326
+ args: [spender, approvalData.amount],
327
+ });
328
+
329
+ log(` Approval TX: ${hash}`);
330
+ await publicClient.waitForTransactionReceipt({ hash });
331
+ log(' Approval confirmed.');
332
+ }
333
+
334
+ async function pollStatus(
335
+ requestHash: string,
336
+ log: (msg: string) => void,
337
+ maxAttempts = 60
338
+ ): Promise<any> {
339
+ for (let i = 0; i < maxAttempts; i++) {
340
+ const response = await fetch(`${BUNGEE_API}/api/v1/bungee/status?requestHash=${requestHash}`);
341
+ const data = await response.json() as any;
342
+
343
+ if (!data.success) {
344
+ throw new Error(`Status error: ${data.error?.message}`);
345
+ }
346
+
347
+ const status = data.result[0];
348
+ const code = status?.bungeeStatusCode;
349
+
350
+ if (code === 3 || code === 4) {
351
+ log(` ✅ Complete! Dest TX: ${status.destinationData?.txHash}`);
352
+ return status;
353
+ }
354
+ if (code === 5) throw new Error('Request expired');
355
+ if (code === 6) throw new Error('Request cancelled');
356
+ if (code === 7) throw new Error('Request refunded');
357
+
358
+ if (i % 6 === 0 && i > 0) {
359
+ log(` Still waiting... (${i * 5}s)`);
360
+ }
361
+ await new Promise(r => setTimeout(r, 5000));
362
+ }
363
+
364
+ throw new Error('Polling timed out');
365
+ }
366
+
367
+ // ============ CLI ============
368
+
369
+ async function main() {
370
+ const args = process.argv.slice(2);
371
+ const [action] = args;
372
+
373
+ // Wallet commands
374
+ if (action === 'wallet-create') {
375
+ if (walletExists()) {
376
+ console.error('❌ Wallet already exists. Delete .wallet.enc to create a new one.');
377
+ process.exit(1);
378
+ }
379
+
380
+ const { mnemonic, address } = createWallet();
381
+ console.log(`
382
+ 🔐 SovereignClaw Treasury Created
383
+
384
+ âš ī¸ CRITICAL: Save this seed phrase NOW.
385
+ It will NEVER be shown again.
386
+
387
+ ┌────────────────────────────────────────────────────────────┐
388
+ ${mnemonic}
389
+ └────────────────────────────────────────────────────────────┘
390
+
391
+ 📍 Address: ${address}
392
+
393
+ After you've saved the seed phrase securely, you can use:
394
+ npx tsx bungee.ts portfolio
395
+ npx tsx bungee.ts swap ...
396
+ `);
397
+ return;
398
+ }
399
+
400
+ if (action === 'wallet-import') {
401
+ const keyOrMnemonic = args.slice(1).join(' ');
402
+ if (!keyOrMnemonic) {
403
+ console.error('Usage: npx tsx bungee.ts wallet-import <private-key-or-mnemonic>');
404
+ process.exit(1);
405
+ }
406
+
407
+ const address = importWallet(keyOrMnemonic);
408
+ console.log(`✅ Wallet imported: ${address}`);
409
+ console.log('âš ī¸ Delete the message/command containing your key!');
410
+ return;
411
+ }
412
+
413
+ if (action === 'wallet-address') {
414
+ const address = getWalletAddress();
415
+ if (!address) {
416
+ console.error('No wallet found. Run: npx tsx bungee.ts wallet-create');
417
+ process.exit(1);
418
+ }
419
+ console.log(address);
420
+ return;
421
+ }
422
+
423
+ if (action === 'portfolio') {
424
+ const address = args[1] || getWalletAddress() || process.env.USER_ADDRESS;
425
+ if (!address) {
426
+ console.error('Usage: npx tsx bungee.ts portfolio [address]');
427
+ console.error('Or create a wallet first: npx tsx bungee.ts wallet-create');
428
+ process.exit(1);
429
+ }
430
+ const tokens = await getTokenBalances(address);
431
+ console.log(formatPortfolio(tokens));
432
+ return;
433
+ }
434
+
435
+ if (action === 'status') {
436
+ const requestHash = args[1];
437
+ if (!requestHash) {
438
+ console.error('Usage: npx tsx bungee.ts status <requestHash>');
439
+ process.exit(1);
440
+ }
441
+
442
+ const response = await fetch(`${BUNGEE_API}/api/v1/bungee/status?requestHash=${requestHash}`);
443
+ const data = await response.json() as any;
444
+
445
+ if (!data.success) {
446
+ console.error('Error:', data.error?.message || 'Unknown error');
447
+ process.exit(1);
448
+ }
449
+
450
+ const status = data.result[0];
451
+ const codes: Record<number, string> = {
452
+ 1: 'âŗ PENDING',
453
+ 2: '🔄 IN PROGRESS',
454
+ 3: '✅ COMPLETED',
455
+ 4: '✅ COMPLETED (partial)',
456
+ 5: '❌ EXPIRED',
457
+ 6: '❌ CANCELLED',
458
+ 7: 'â†Šī¸ REFUNDED',
459
+ };
460
+
461
+ console.log(`Status: ${codes[status?.bungeeStatusCode] || 'UNKNOWN'}`);
462
+ if (status?.originData?.txHash) {
463
+ console.log(`Origin TX: ${status.originData.txHash}`);
464
+ }
465
+ if (status?.destinationData?.txHash) {
466
+ console.log(`Dest TX: ${status.destinationData.txHash}`);
467
+ }
468
+ console.log(`\nSocketScan: https://socketscan.io/tx/${requestHash}`);
469
+ return;
470
+ }
471
+
472
+ if (action === 'quote') {
473
+ // quote <originChain> <destChain> <inputToken> <outputToken> <amount> <userAddress>
474
+ const [, originChain, destChain, inputToken, outputToken, amount, userAddress] = args;
475
+ if (!userAddress) {
476
+ console.error('Usage: npx tsx bungee.ts quote <originChain> <destChain> <inputToken> <outputToken> <amount> <userAddress>');
477
+ process.exit(1);
478
+ }
479
+ const quote = await getQuote({
480
+ userAddress,
481
+ originChainId: parseInt(originChain),
482
+ destinationChainId: parseInt(destChain),
483
+ inputToken,
484
+ outputToken,
485
+ inputAmount: amount,
486
+ });
487
+ console.log('Quote:', JSON.stringify(quote, null, 2));
488
+ return;
489
+ }
490
+
491
+ if (action === 'swap') {
492
+ // Try stored wallet first, then env var
493
+ const wallet = loadWallet();
494
+ const privateKey = wallet?.privateKey || process.env.PRIVATE_KEY;
495
+
496
+ if (!privateKey) {
497
+ console.error('No wallet found. Either:');
498
+ console.error(' 1. Create wallet: npx tsx bungee.ts wallet-create');
499
+ console.error(' 2. Set PRIVATE_KEY env var');
500
+ process.exit(1);
501
+ }
502
+
503
+ // swap <originChain> <destChain> <inputToken> <outputToken> <amount>
504
+ const [, originChain, destChain, inputToken, outputToken, amount] = args;
505
+
506
+ // Handle mnemonic vs private key
507
+ let account;
508
+ if (privateKey.includes(' ')) {
509
+ account = mnemonicToAccount(privateKey);
510
+ } else {
511
+ const normalized = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}`;
512
+ account = privateKeyToAccount(normalized);
513
+ }
514
+
515
+ console.log('Getting quote...');
516
+ const quote = await getQuote({
517
+ userAddress: account.address,
518
+ originChainId: parseInt(originChain),
519
+ destinationChainId: parseInt(destChain),
520
+ inputToken,
521
+ outputToken,
522
+ inputAmount: amount,
523
+ });
524
+
525
+ console.log('Executing swap...');
526
+ const result = await executeSwap(privateKey, quote);
527
+ console.log('Done!', result);
528
+ return;
529
+ }
530
+
531
+ console.log(`
532
+ SovereignClaw — Economic Sovereignty for AI Agents
533
+
534
+ Wallet:
535
+ wallet-create Create new wallet (shows seed phrase ONCE)
536
+ wallet-import <key|phrase> Import existing wallet
537
+ wallet-address Show wallet address
538
+
539
+ Trading:
540
+ portfolio [address] View cross-chain portfolio
541
+ quote <params...> Get a swap quote
542
+ swap <origin> <dest> <in> <out> <amount> Execute a swap
543
+ status <requestHash> Check transaction status
544
+
545
+ Examples:
546
+ npx tsx bungee.ts wallet-create
547
+ npx tsx bungee.ts portfolio
548
+ npx tsx bungee.ts swap 8453 8453 0xEeee...EEEE 0x833589...02913 1000000000000000
549
+ npx tsx bungee.ts status 0xa6b977a6d65f2be870bd7b2b1464d15759a69aa4b321bffafa1f8cbf19343c58
550
+
551
+ Environment:
552
+ WALLET_KEY - Encryption key for stored wallet (optional)
553
+ PRIVATE_KEY - Direct private key (alternative to wallet-create)
554
+ `);
555
+ }
556
+
557
+ main().catch(console.error);