-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbot.js
More file actions
475 lines (411 loc) · 17.2 KB
/
Copy pathbot.js
File metadata and controls
475 lines (411 loc) · 17.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
// bot.js — Main entry point: runs all Miden testnet tasks in sequence
import 'dotenv/config';
import { launchBrowser, screenshot, sleep, log } from './utils/browser.js';
import { isWalletSetup, getAccountId } from './utils/miden-wallet.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { spawnSync } from 'child_process';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const STATE_FILE = path.join(__dirname, 'data', 'state.json');
// ─────────────────────────────────────────────
// CONFIGURATION
// ─────────────────────────────────────────────
const EXTENSION_ID = process.env.MIDEN_EXTENSION_ID || 'ablmompanofnodfdkgchkpmphailefpb';
const FAUCET_URL = 'https://faucet.testnet.miden.io/';
const PLAYGROUND_URL = 'https://playground.testnet.miden.io/';
const SLOW_MO = parseInt(process.env.SLOW_MO || '500');
// ─────────────────────────────────────────────
// STATE MANAGEMENT
// ─────────────────────────────────────────────
function loadState() {
try {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
} catch {
return {};
}
}
function saveState(state) {
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
// ─────────────────────────────────────────────
// MAIN ORCHESTRATOR
// ─────────────────────────────────────────────
async function main() {
log('🚀 Miden Testnet Bot Starting...');
log(`Extension ID: ${EXTENSION_ID}`);
const state = loadState();
// ── Launch browser with extension ──────────
log('Launching browser with Miden Wallet extension...');
const { chromium } = await import('playwright');
const userDataDir = path.join(__dirname, 'data', 'chrome-profile');
fs.mkdirSync(userDataDir, { recursive: true });
fs.mkdirSync(path.join(__dirname, 'output', 'screenshots'), { recursive: true });
const context = await chromium.launchPersistentContext(userDataDir, {
headless: false, // Extensions require non-headless
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled',
'--lang=en-US',
'--window-size=1366,768',
],
viewport: { width: 1366, height: 768 },
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
locale: 'en-US',
timezoneId: 'America/New_York',
slowMo: SLOW_MO,
});
try {
// ── STEP 1: Setup Wallet ─────────────────
log('\n═══════════════════════════════════════');
log('STEP 1: Setting up Miden Wallet');
log('═══════════════════════════════════════');
const accountId = await stepSetupWallet(context, state);
state.accountId = accountId;
saveState(state);
await sleep(3000);
// ── STEP 2: Get Faucet Tokens ────────────
log('\n═══════════════════════════════════════');
log('STEP 2: Getting tokens from faucet');
log('═══════════════════════════════════════');
await stepFaucet(context, state);
state.faucetDone = true;
saveState(state);
await sleep(5000);
// ── STEP 3: Playground - All Tasks ───────
log('\n═══════════════════════════════════════');
log('STEP 3: Playground - All Tasks');
log('═══════════════════════════════════════');
await stepPlayground(context, state);
state.playgroundDone = true;
saveState(state);
log('\n✅ All tasks completed successfully!');
log(`State saved to: ${STATE_FILE}`);
} catch (err) {
log(`❌ Bot error: ${err.message}`);
console.error(err);
} finally {
await sleep(5000);
await context.close();
}
}
// ─────────────────────────────────────────────
// STEP 1: WALLET SETUP
// ─────────────────────────────────────────────
async function stepSetupWallet(context, state) {
await sleep(3000); // Let extension initialize
const walletUrl = `chrome-extension://${EXTENSION_ID}/index.html`;
log(`Opening wallet: ${walletUrl}`);
const page = await context.newPage();
await page.goto(walletUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
await sleep(4000);
await screenshot(page, '01-wallet-open');
// Read page content to understand state
const bodyText = await page.textContent('body').catch(() => '');
log(`Wallet page content (first 300 chars): ${bodyText.substring(0, 300)}`);
// Check if needs setup
const needsSetup = bodyText.includes('Create') || bodyText.includes('New Account') ||
bodyText.includes('Get Started') || bodyText.includes('Welcome') ||
bodyText.includes('Import');
if (needsSetup) {
log('Wallet needs setup, creating new account...');
// Click any create/get-started button
const createSelectors = [
'button:has-text("Create Account")',
'button:has-text("New Account")',
'button:has-text("Get Started")',
'button:has-text("Create")',
'button:has-text("Add Account")',
];
for (const sel of createSelectors) {
const btn = page.locator(sel).first();
if (await btn.count() > 0) {
log(`Clicking: ${sel}`);
await btn.click();
await sleep(3000);
break;
}
}
} else {
log('Wallet already has account set up');
}
await screenshot(page, '01-wallet-after-setup');
// Extract account ID
let accountId = state.accountId;
const text = await page.textContent('body').catch(() => '');
// Look for account ID patterns (hex-like strings)
const matches = text.match(/0x[0-9a-fA-F]{8,}/g);
if (matches && matches.length > 0) {
accountId = matches[0];
log(`Found account ID: ${accountId}`);
} else {
log('⚠️ Could not extract account ID from wallet, check screenshot');
accountId = accountId || 'unknown';
}
await page.close();
return accountId;
}
// ─────────────────────────────────────────────
// STEP 2: FAUCET
// ─────────────────────────────────────────────
async function stepFaucet(context, state) {
const page = await context.newPage();
log(`Opening faucet: ${FAUCET_URL}`);
await page.goto(FAUCET_URL, { waitUntil: 'networkidle', timeout: 90000 });
await sleep(5000);
await screenshot(page, '02-faucet-open');
// Wait for WASM
try {
await page.waitForFunction(
() => !document.body.textContent.includes('Loading'),
{ timeout: 30000 }
);
} catch {
log('⚠️ Timeout waiting for loading to finish');
}
await sleep(3000);
await screenshot(page, '02-faucet-loaded');
const bodyText = await page.textContent('body');
log(`Faucet page content: ${bodyText.substring(0, 500)}`);
// Check if there's an account ID input
const inputs = await page.locator('input').all();
log(`Found ${inputs.length} input(s) on faucet page`);
for (let i = 0; i < inputs.length; i++) {
const placeholder = await inputs[i].getAttribute('placeholder') || '';
const inputType = await inputs[i].getAttribute('type') || '';
log(` Input ${i}: type=${inputType}, placeholder="${placeholder}"`);
if (placeholder.toLowerCase().includes('account') ||
placeholder.toLowerCase().includes('id') ||
placeholder.toLowerCase().includes('address')) {
if (state.accountId && state.accountId !== 'unknown') {
log(`Filling account ID: ${state.accountId}`);
await inputs[i].fill(state.accountId);
await sleep(500);
}
}
}
// Get all buttons
const buttons = await page.locator('button').all();
log(`Found ${buttons.length} button(s) on faucet page`);
for (let i = 0; i < buttons.length; i++) {
const text = await buttons[i].textContent() || '';
log(` Button ${i}: "${text.trim()}"`);
}
// Try clicking faucet buttons
const faucetButtons = [
'button:has-text("Public")',
'button:has-text("Private")',
'button:has-text("Request")',
'button:has-text("Get")',
'button:has-text("Claim")',
'button:has-text("Faucet")',
'button[type="submit"]',
];
let clicked = false;
for (const sel of faucetButtons) {
const btns = page.locator(sel);
const count = await btns.count();
if (count > 0) {
log(`Clicking faucet button: ${sel}`);
await btns.first().click();
await sleep(5000);
await screenshot(page, '02-faucet-clicked');
clicked = true;
// If there are more (public + private), click others too
if (count > 1) {
for (let j = 1; j < count; j++) {
await btns.nth(j).click();
await sleep(3000);
}
}
break;
}
}
if (!clicked) {
log('⚠️ Could not find faucet button, trying any button...');
if (buttons.length > 0) {
await buttons[0].click();
await sleep(3000);
}
}
// Handle wallet confirmation popup
const popup = await context.waitForEvent('page', { timeout: 8000 }).catch(() => null);
if (popup) {
log('Wallet confirmation popup appears!');
await sleep(2000);
await screenshot(popup, '02-faucet-wallet-popup');
for (const sel of ['button:has-text("Approve")', 'button:has-text("Confirm")', 'button:has-text("Accept")']) {
if (await popup.locator(sel).count() > 0) {
await popup.locator(sel).first().click();
break;
}
}
await sleep(2000);
}
await screenshot(page, '02-faucet-final');
await page.close();
log('✅ Faucet step done');
}
// ─────────────────────────────────────────────
// STEP 3: PLAYGROUND - ALL TASKS
// ─────────────────────────────────────────────
async function stepPlayground(context, state) {
const page = await context.newPage();
log(`Opening Playground: ${PLAYGROUND_URL}`);
await page.goto(PLAYGROUND_URL, { waitUntil: 'networkidle', timeout: 90000 });
await sleep(8000);
await screenshot(page, '03-playground-open');
const bodyText = await page.textContent('body').catch(() => '');
log(`Playground content (first 500): ${bodyText.substring(0, 500)}`);
// Get all nav items / tabs
const navItems = await page.locator('nav a, nav button, [role="tab"], .tab, .nav-item').all();
log(`Found ${navItems.length} nav items`);
for (const item of navItems) {
const text = await item.textContent().catch(() => '');
log(` Nav: "${text.trim()}"`);
}
// Get all buttons
const buttons = await page.locator('button').all();
log(`Found ${buttons.length} buttons on playground`);
for (const btn of buttons) {
const text = await btn.textContent().catch(() => '');
log(` Button: "${text.trim()}"`);
}
// Connect wallet first
await tryConnectWallet(page, context);
await sleep(3000);
// Now iterate through all available sections/tabs
const taskSections = [
{ names: ['Send', 'Transfer', 'P2ID'], handler: 'send' },
{ names: ['Swap', 'Exchange'], handler: 'swap' },
{ names: ['Smart Contract', 'Contract'], handler: 'smartContract' },
{ names: ['Deploy', 'Deployment'], handler: 'deploy' },
{ names: ['Network', 'Network Actions'], handler: 'network' },
{ names: ['Notes', 'Note'], handler: 'notes' },
{ names: ['Consume', 'Consume Note'], handler: 'consume' },
{ names: ['Mint', 'Issue'], handler: 'mint' },
{ names: ['Burn'], handler: 'burn' },
{ names: ['Reclaim'], handler: 'reclaim' },
];
for (const task of taskSections) {
for (const name of task.names) {
const sel = `nav a:has-text("${name}"), nav button:has-text("${name}"), [role="tab"]:has-text("${name}"), button:has-text("${name}"), a:has-text("${name}")`;
const el = page.locator(sel).first();
const count = await el.count();
if (count > 0) {
log(`\n--- Executing task: ${name} ---`);
await el.click();
await sleep(3000);
await screenshot(page, `03-task-${name.replace(/\s+/g, '-').toLowerCase()}`);
await executeCurrentTask(page, name, state, context);
await sleep(2000);
break;
}
}
}
await screenshot(page, '03-playground-complete');
await page.close();
}
async function tryConnectWallet(page, context) {
const connectSels = [
'button:has-text("Connect")',
'button:has-text("Connect Wallet")',
'button:has-text("Sign in")',
'button:has-text("Login")',
];
for (const sel of connectSels) {
const btn = page.locator(sel).first();
if (await btn.count() > 0) {
log(`Connecting wallet via: ${sel}`);
await btn.click();
await sleep(2000);
// Handle popup
const popup = await context.waitForEvent('page', { timeout: 8000 }).catch(() => null);
if (popup) {
await sleep(2000);
for (const ps of ['button:has-text("Approve")', 'button:has-text("Connect")', 'button:has-text("Confirm")']) {
if (await popup.locator(ps).count() > 0) {
await popup.locator(ps).first().click();
break;
}
}
await sleep(2000);
}
break;
}
}
await screenshot(page, '03-after-connect');
}
async function executeCurrentTask(page, taskName, state, context) {
// Generic task executor: fill inputs and submit
const inputs = await page.locator('input:visible').all();
log(` Found ${inputs.length} visible inputs in ${taskName}`);
for (const inp of inputs) {
const placeholder = (await inp.getAttribute('placeholder') || '').toLowerCase();
const name = (await inp.getAttribute('name') || '').toLowerCase();
if (placeholder.includes('account') || placeholder.includes('recipient') ||
placeholder.includes('address') || name.includes('to') || name.includes('account')) {
const val = state.accountId && state.accountId !== 'unknown' ? state.accountId : '';
if (val) {
await inp.fill(val);
log(` Filled recipient input: ${val}`);
}
} else if (placeholder.includes('amount') || name.includes('amount') ||
placeholder.includes('value') || name.includes('value')) {
await inp.fill('1');
log(' Filled amount: 1');
} else if (placeholder.includes('note') || name.includes('note')) {
await inp.fill('test');
log(' Filled note field');
}
}
await sleep(500);
// Find and click submit/execute button
const submitSels = [
`button:has-text("${taskName}")`,
'button:has-text("Execute")',
'button:has-text("Submit")',
'button:has-text("Confirm")',
'button:has-text("Send")',
'button:has-text("Deploy")',
'button:has-text("Consume")',
'button:has-text("Mint")',
'button:has-text("Burn")',
'button[type="submit"]',
];
for (const sel of submitSels) {
const btn = page.locator(sel).last();
if (await btn.count() > 0) {
const txt = await btn.textContent().catch(() => '');
log(` Clicking: "${txt.trim()}" (${sel})`);
await btn.click();
await sleep(5000);
// Handle any wallet approval popup
const popup = await context.waitForEvent('page', { timeout: 5000 }).catch(() => null);
if (popup) {
log(' Wallet approval popup detected');
await sleep(2000);
for (const ps of ['button:has-text("Approve")', 'button:has-text("Confirm")', 'button:has-text("Sign")']) {
if (await popup.locator(ps).count() > 0) {
await popup.locator(ps).first().click();
break;
}
}
await sleep(3000);
}
await screenshot(page, `03-task-${taskName.replace(/\s+/g, '-').toLowerCase()}-done`);
break;
}
}
}
// ─────────────────────────────────────────────
// RUN
// ─────────────────────────────────────────────
main().catch(e => {
log(`💥 Fatal error: ${e.message}`);
console.error(e);
process.exit(1);
});