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.
- package/README.md +187 -0
- package/SKILL.md +186 -0
- package/bin/sovereignclaw.js +146 -0
- package/package.json +48 -0
- package/scripts/bridge.ts +431 -0
- package/scripts/bungee.ts +557 -0
- package/scripts/wallet.ts +195 -0
|
@@ -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);
|