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,431 @@
1
+ /**
2
+ * CCTP Bridge Script
3
+ * Bridges USDC across chains using Circle's Cross-Chain Transfer Protocol
4
+ */
5
+
6
+ import { createWalletClient, createPublicClient, http, encodeFunctionData, parseUnits, formatUnits } from 'viem';
7
+ import { privateKeyToAccount } from 'viem/accounts';
8
+ import { sepolia, baseSepolia, arbitrumSepolia, optimismSepolia } from 'viem/chains';
9
+
10
+ // ============ Configuration ============
11
+
12
+ interface ChainConfig {
13
+ name: string;
14
+ domain: number;
15
+ chainId: number;
16
+ chain: any;
17
+ usdc: `0x${string}`;
18
+ rpcUrl?: string;
19
+ }
20
+
21
+ const TESTNET_CHAINS: Record<string, ChainConfig> = {
22
+ 'ethereum-sepolia': {
23
+ name: 'Ethereum Sepolia',
24
+ domain: 0,
25
+ chainId: 11155111,
26
+ chain: sepolia,
27
+ usdc: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
28
+ rpcUrl: 'https://ethereum-sepolia-rpc.publicnode.com',
29
+ },
30
+ 'base-sepolia': {
31
+ name: 'Base Sepolia',
32
+ domain: 6,
33
+ chainId: 84532,
34
+ chain: baseSepolia,
35
+ usdc: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
36
+ rpcUrl: 'https://base-sepolia-rpc.publicnode.com',
37
+ },
38
+ 'arbitrum-sepolia': {
39
+ name: 'Arbitrum Sepolia',
40
+ domain: 3,
41
+ chainId: 421614,
42
+ chain: arbitrumSepolia,
43
+ usdc: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
44
+ },
45
+ 'op-sepolia': {
46
+ name: 'OP Sepolia',
47
+ domain: 2,
48
+ chainId: 11155420,
49
+ chain: optimismSepolia,
50
+ usdc: '0x5fd84259d66Cd46123540766Be93DFE6D43130D7',
51
+ },
52
+ };
53
+
54
+ // CCTP Contracts (same on all testnet chains)
55
+ const CCTP_CONTRACTS = {
56
+ testnet: {
57
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA' as `0x${string}`,
58
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275' as `0x${string}`,
59
+ attestationApi: 'https://iris-api-sandbox.circle.com',
60
+ },
61
+ mainnet: {
62
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d' as `0x${string}`,
63
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64' as `0x${string}`,
64
+ attestationApi: 'https://iris-api.circle.com',
65
+ },
66
+ };
67
+
68
+ // ABIs
69
+ const ERC20_ABI = [
70
+ {
71
+ type: 'function',
72
+ name: 'approve',
73
+ stateMutability: 'nonpayable',
74
+ inputs: [
75
+ { name: 'spender', type: 'address' },
76
+ { name: 'amount', type: 'uint256' },
77
+ ],
78
+ outputs: [{ name: '', type: 'bool' }],
79
+ },
80
+ {
81
+ type: 'function',
82
+ name: 'balanceOf',
83
+ stateMutability: 'view',
84
+ inputs: [{ name: 'account', type: 'address' }],
85
+ outputs: [{ name: '', type: 'uint256' }],
86
+ },
87
+ {
88
+ type: 'function',
89
+ name: 'allowance',
90
+ stateMutability: 'view',
91
+ inputs: [
92
+ { name: 'owner', type: 'address' },
93
+ { name: 'spender', type: 'address' },
94
+ ],
95
+ outputs: [{ name: '', type: 'uint256' }],
96
+ },
97
+ ] as const;
98
+
99
+ const TOKEN_MESSENGER_ABI = [
100
+ {
101
+ type: 'function',
102
+ name: 'depositForBurn',
103
+ stateMutability: 'nonpayable',
104
+ inputs: [
105
+ { name: 'amount', type: 'uint256' },
106
+ { name: 'destinationDomain', type: 'uint32' },
107
+ { name: 'mintRecipient', type: 'bytes32' },
108
+ { name: 'burnToken', type: 'address' },
109
+ { name: 'destinationCaller', type: 'bytes32' },
110
+ { name: 'maxFee', type: 'uint256' },
111
+ { name: 'minFinalityThreshold', type: 'uint32' },
112
+ ],
113
+ outputs: [],
114
+ },
115
+ ] as const;
116
+
117
+ const MESSAGE_TRANSMITTER_ABI = [
118
+ {
119
+ type: 'function',
120
+ name: 'receiveMessage',
121
+ stateMutability: 'nonpayable',
122
+ inputs: [
123
+ { name: 'message', type: 'bytes' },
124
+ { name: 'attestation', type: 'bytes' },
125
+ ],
126
+ outputs: [{ name: 'success', type: 'bool' }],
127
+ },
128
+ ] as const;
129
+
130
+ // ============ Helper Functions ============
131
+
132
+ function addressToBytes32(address: string): `0x${string}` {
133
+ return `0x000000000000000000000000${address.slice(2)}` as `0x${string}`;
134
+ }
135
+
136
+ const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`;
137
+
138
+ async function sleep(ms: number): Promise<void> {
139
+ return new Promise(resolve => setTimeout(resolve, ms));
140
+ }
141
+
142
+ // ============ Bridge Class ============
143
+
144
+ export class CCTPBridge {
145
+ private privateKey: `0x${string}`;
146
+ private isTestnet: boolean;
147
+ private contracts: typeof CCTP_CONTRACTS.testnet;
148
+
149
+ constructor(privateKey: string, isTestnet = true) {
150
+ this.privateKey = privateKey as `0x${string}`;
151
+ this.isTestnet = isTestnet;
152
+ this.contracts = isTestnet ? CCTP_CONTRACTS.testnet : CCTP_CONTRACTS.mainnet;
153
+ }
154
+
155
+ private getChainConfig(chainName: string): ChainConfig {
156
+ const normalized = chainName.toLowerCase().replace(/\s+/g, '-');
157
+ const config = TESTNET_CHAINS[normalized];
158
+ if (!config) {
159
+ throw new Error(`Unsupported chain: ${chainName}. Supported: ${Object.keys(TESTNET_CHAINS).join(', ')}`);
160
+ }
161
+ return config;
162
+ }
163
+
164
+ private createClients(chainConfig: ChainConfig) {
165
+ const account = privateKeyToAccount(this.privateKey);
166
+ const transport = chainConfig.rpcUrl ? http(chainConfig.rpcUrl) : http();
167
+ const walletClient = createWalletClient({
168
+ chain: chainConfig.chain,
169
+ transport,
170
+ account,
171
+ });
172
+ const publicClient = createPublicClient({
173
+ chain: chainConfig.chain,
174
+ transport,
175
+ });
176
+ return { walletClient, publicClient, account };
177
+ }
178
+
179
+ async getBalance(chainName: string): Promise<string> {
180
+ const chainConfig = this.getChainConfig(chainName);
181
+ const { publicClient, account } = this.createClients(chainConfig);
182
+
183
+ const balance = await publicClient.readContract({
184
+ address: chainConfig.usdc,
185
+ abi: ERC20_ABI,
186
+ functionName: 'balanceOf',
187
+ args: [account.address],
188
+ });
189
+
190
+ return formatUnits(balance, 6);
191
+ }
192
+
193
+ async quote(
194
+ sourceChain: string,
195
+ destChain: string,
196
+ amount: string
197
+ ): Promise<{
198
+ sourceChain: string;
199
+ destChain: string;
200
+ amount: string;
201
+ estimatedReceived: string;
202
+ fee: string;
203
+ estimatedTime: string;
204
+ }> {
205
+ const source = this.getChainConfig(sourceChain);
206
+ const dest = this.getChainConfig(destChain);
207
+
208
+ // CCTP fee is minimal (~0.0005 USDC)
209
+ const fee = '0.0005';
210
+ const received = (parseFloat(amount) - parseFloat(fee)).toFixed(4);
211
+
212
+ return {
213
+ sourceChain: source.name,
214
+ destChain: dest.name,
215
+ amount,
216
+ estimatedReceived: received,
217
+ fee,
218
+ estimatedTime: '1-2 minutes (Fast Transfer)',
219
+ };
220
+ }
221
+
222
+ async bridge(
223
+ sourceChain: string,
224
+ destChain: string,
225
+ amount: string,
226
+ recipient?: string,
227
+ onStatus?: (status: string) => void
228
+ ): Promise<{
229
+ burnTxHash: string;
230
+ mintTxHash: string;
231
+ amountReceived: string;
232
+ }> {
233
+ const log = onStatus || console.log;
234
+
235
+ const source = this.getChainConfig(sourceChain);
236
+ const dest = this.getChainConfig(destChain);
237
+ const amountWei = parseUnits(amount, 6);
238
+
239
+ const { walletClient: sourceWallet, publicClient: sourcePublic, account } = this.createClients(source);
240
+ const { walletClient: destWallet } = this.createClients(dest);
241
+
242
+ const recipientAddress = recipient || account.address;
243
+ const recipientBytes32 = addressToBytes32(recipientAddress);
244
+
245
+ // Step 1: Check balance
246
+ log('📊 Checking USDC balance...');
247
+ const balance = await sourcePublic.readContract({
248
+ address: source.usdc,
249
+ abi: ERC20_ABI,
250
+ functionName: 'balanceOf',
251
+ args: [account.address],
252
+ });
253
+
254
+ if (balance < amountWei) {
255
+ throw new Error(`Insufficient USDC. Have: ${formatUnits(balance, 6)}, Need: ${amount}`);
256
+ }
257
+
258
+ // Step 2: Approve USDC
259
+ log('✅ Step 1/4: Approving USDC...');
260
+ const allowance = await sourcePublic.readContract({
261
+ address: source.usdc,
262
+ abi: ERC20_ABI,
263
+ functionName: 'allowance',
264
+ args: [account.address, this.contracts.tokenMessenger],
265
+ });
266
+
267
+ if (allowance < amountWei) {
268
+ const approveTx = await sourceWallet.sendTransaction({
269
+ to: source.usdc,
270
+ data: encodeFunctionData({
271
+ abi: ERC20_ABI,
272
+ functionName: 'approve',
273
+ args: [this.contracts.tokenMessenger, amountWei * 10n], // Approve extra for future txs
274
+ }),
275
+ });
276
+ log(` Approval TX: ${approveTx}`);
277
+ await sourcePublic.waitForTransactionReceipt({ hash: approveTx });
278
+ } else {
279
+ log(' Already approved.');
280
+ }
281
+
282
+ // Step 3: Burn USDC
283
+ log('🔥 Step 2/4: Burning USDC on source chain...');
284
+ const maxFee = 100000n; // 0.1 USDC - higher fee for faster attestation
285
+ const minFinalityThreshold = 1000; // Fast transfer
286
+
287
+ const burnTx = await sourceWallet.sendTransaction({
288
+ to: this.contracts.tokenMessenger,
289
+ data: encodeFunctionData({
290
+ abi: TOKEN_MESSENGER_ABI,
291
+ functionName: 'depositForBurn',
292
+ args: [
293
+ amountWei,
294
+ dest.domain,
295
+ recipientBytes32,
296
+ source.usdc,
297
+ ZERO_BYTES32,
298
+ maxFee,
299
+ minFinalityThreshold,
300
+ ],
301
+ }),
302
+ });
303
+ log(` Burn TX: ${burnTx}`);
304
+ await sourcePublic.waitForTransactionReceipt({ hash: burnTx });
305
+
306
+ // Step 4: Wait for attestation
307
+ log('⏳ Step 3/4: Waiting for attestation...');
308
+ const attestation = await this.waitForAttestation(source.domain, burnTx, log);
309
+ log(' ✅ Attestation received!');
310
+
311
+ // Step 5: Mint on destination
312
+ log('🪙 Step 4/4: Minting USDC on destination chain...');
313
+ const mintTx = await destWallet.sendTransaction({
314
+ to: this.contracts.messageTransmitter,
315
+ data: encodeFunctionData({
316
+ abi: MESSAGE_TRANSMITTER_ABI,
317
+ functionName: 'receiveMessage',
318
+ args: [attestation.message as `0x${string}`, attestation.attestation as `0x${string}`],
319
+ }),
320
+ });
321
+ log(` Mint TX: ${mintTx}`);
322
+
323
+ const destPublic = createPublicClient({
324
+ chain: dest.chain,
325
+ transport: http(),
326
+ });
327
+ await destPublic.waitForTransactionReceipt({ hash: mintTx });
328
+
329
+ log('✅ Bridge complete!');
330
+
331
+ return {
332
+ burnTxHash: burnTx,
333
+ mintTxHash: mintTx,
334
+ amountReceived: formatUnits(amountWei - maxFee, 6),
335
+ };
336
+ }
337
+
338
+ private async waitForAttestation(
339
+ sourceDomain: number,
340
+ txHash: string,
341
+ log: (msg: string) => void
342
+ ): Promise<{ message: string; attestation: string }> {
343
+ const url = `${this.contracts.attestationApi}/v2/messages/${sourceDomain}?transactionHash=${txHash}`;
344
+ let attempts = 0;
345
+ const maxAttempts = 60; // 5 minutes max
346
+
347
+ while (attempts < maxAttempts) {
348
+ try {
349
+ const response = await fetch(url);
350
+
351
+ if (response.ok) {
352
+ const data = await response.json() as { messages?: Array<{ status: string; message: string; attestation: string }> };
353
+ if (data.messages?.[0]?.status === 'complete') {
354
+ return {
355
+ message: data.messages[0].message,
356
+ attestation: data.messages[0].attestation,
357
+ };
358
+ }
359
+ }
360
+
361
+ attempts++;
362
+ if (attempts % 6 === 0) {
363
+ log(` Still waiting... (${attempts * 5}s)`);
364
+ }
365
+ await sleep(5000);
366
+ } catch (error) {
367
+ attempts++;
368
+ await sleep(5000);
369
+ }
370
+ }
371
+
372
+ throw new Error(`Attestation timeout after ${maxAttempts * 5}s. TX: ${txHash}`);
373
+ }
374
+ }
375
+
376
+ // ============ CLI Interface ============
377
+
378
+ async function main() {
379
+ const args = process.argv.slice(2);
380
+
381
+ if (args.length < 4) {
382
+ console.log(`
383
+ Usage: npx tsx bridge.ts <action> <source> <dest> <amount> [recipient]
384
+
385
+ Actions:
386
+ quote - Get a quote without executing
387
+ bridge - Execute the bridge
388
+
389
+ Examples:
390
+ npx tsx bridge.ts quote ethereum-sepolia base-sepolia 10
391
+ npx tsx bridge.ts bridge ethereum-sepolia base-sepolia 10
392
+
393
+ Environment:
394
+ PRIVATE_KEY - Your wallet private key
395
+ `);
396
+ process.exit(1);
397
+ }
398
+
399
+ const [action, source, dest, amount, recipient] = args;
400
+ const privateKey = process.env.PRIVATE_KEY;
401
+
402
+ if (action === 'bridge' && !privateKey) {
403
+ console.error('Error: PRIVATE_KEY environment variable required for bridging');
404
+ process.exit(1);
405
+ }
406
+
407
+ const bridge = new CCTPBridge(privateKey || '0x0', true);
408
+
409
+ if (action === 'quote') {
410
+ const quote = await bridge.quote(source, dest, amount);
411
+ console.log(`
412
+ 🔄 CCTP Bridge Quote: ${quote.amount} USDC
413
+ 📤 From: ${quote.sourceChain}
414
+ 📥 To: ${quote.destChain}
415
+
416
+ 💰 You receive: ~${quote.estimatedReceived} USDC
417
+ ⛽ Fee: ~${quote.fee} USDC
418
+ ⏱️ Est. time: ${quote.estimatedTime}
419
+ `);
420
+ } else if (action === 'bridge') {
421
+ const result = await bridge.bridge(source, dest, amount, recipient);
422
+ console.log(`
423
+ ✅ Bridge Complete!
424
+ 🔥 Burn TX: ${result.burnTxHash}
425
+ 🪙 Mint TX: ${result.mintTxHash}
426
+ 💰 Received: ${result.amountReceived} USDC
427
+ `);
428
+ }
429
+ }
430
+
431
+ main().catch(console.error);