UNPKG

@coinbase/onchaintestkit

Version:

End-to-end testing toolkit for blockchain applications, powered by Playwright

234 lines (193 loc) 7.08 kB
#!/usr/bin/env node /** * This script downloads and extracts the Phantom Wallet extension * to be used by Playwright tests. * * Run this before running tests in CI to ensure the extension * is ready and to avoid race conditions during test execution. */ import path from "path" import { fileURLToPath } from "url" import extract from "extract-zip" import fs from "fs-extra" import fetch from "node-fetch" // Support for ES modules const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) // Constants for Phantom Wallet const PHANTOM_VERSION = "25.27.0" const EXTENSION_ID = "bfnaelmomeimhlpmgjnjophhpkkoljpa" const DOWNLOAD_URL = `https://update.googleapis.com/service/update2/crx?response=redirect&os=win&arch=x64&os_arch=x86_64&nacl_arch=x86-64&prod=chromiumcrx&prodchannel=unknown&prodversion=120.0.0.0&acceptformat=crx3&x=id%3D${EXTENSION_ID}%26uc` const EXTRACTION_COMPLETE_FLAG = ".extraction_complete" /** * Set up the cache directory structure */ async function setupCacheDir(cacheDirName) { const projectRoot = process.cwd() const cacheDirPath = path.join(projectRoot, "e2e", ".cache", cacheDirName) // Ensure the cache directory exists await fs.ensureDir(cacheDirPath) return cacheDirPath } /** * Download with retry logic for CI environments */ async function downloadWithRetry(url, options, maxRetries = 3) { let lastError for (let attempt = 1; attempt <= maxRetries; attempt++) { try { console.log(`Download attempt ${attempt} of ${maxRetries}...`) const response = await fetch(url, options) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } return response } catch (error) { lastError = error console.error(`Attempt ${attempt} failed: ${error.message}`) if (attempt < maxRetries) { const waitTime = Math.min(1000 * 2 ** (attempt - 1), 10000) console.log(`Waiting ${waitTime}ms before retry...`) await new Promise(resolve => setTimeout(resolve, waitTime)) } } } throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`) } /** * Prepares the Phantom Wallet extension by downloading and extracting it */ async function setupPhantomExtraction() { console.log(`Preparing Phantom Wallet extension v${PHANTOM_VERSION}...`) // Set up the cache directory const cacheDir = await setupCacheDir("phantom-extension") const extractionPath = path.join(cacheDir, `phantom-${PHANTOM_VERSION}`) const flagPath = path.join(extractionPath, EXTRACTION_COMPLETE_FLAG) // Download the CRX file const crxPath = path.join(cacheDir, `phantom-${PHANTOM_VERSION}.crx`) console.log(`Downloading Phantom Wallet extension to: ${crxPath}`) // Check if file already exists let cached = false if (await fs.pathExists(crxPath)) { const stats = await fs.stat(crxPath) if (stats.size > 0) { console.log(`Using cached download at ${crxPath}`) cached = true } } // Download if not cached if (!cached) { console.log("Downloading from Chrome Web Store...") try { // Attempt download with retry logic const response = await downloadWithRetry( DOWNLOAD_URL, { redirect: "follow", follow: 20, }, 3, ) const contentType = response.headers.get("content-type") console.log(`Response content-type: ${contentType}`) const buffer = await response.arrayBuffer() await fs.writeFile(crxPath, Buffer.from(buffer)) console.log("Download complete") // Verify the download const downloadedStats = await fs.stat(crxPath) if (downloadedStats.size === 0) { await fs.remove(crxPath) throw new Error( "Downloaded file is empty. The Chrome Web Store might be blocking automated downloads.", ) } } catch (error) { throw new Error(`Extension download failed: ${error.message}`) } } // Clean any existing extraction directory if (await fs.pathExists(extractionPath)) { console.log(`Cleaning existing directory: ${extractionPath}`) await fs.emptyDir(extractionPath) } // Extract the CRX file console.log(`Extracting to: ${extractionPath}`) try { // Read the CRX file const crxBuffer = await fs.readFile(crxPath) console.log(`CRX file size: ${crxBuffer.length} bytes`) // CRX3 files start with "Cr24" magic number const crx3Magic = Buffer.from("Cr24") let zipStart = -1 if (crxBuffer.subarray(0, 4).equals(crx3Magic)) { console.log("Detected CRX3 format") // CRX3 format: // 4 bytes: "Cr24" magic // 4 bytes: version (3) // 4 bytes: header length const version = crxBuffer.readUInt32LE(4) const headerLength = crxBuffer.readUInt32LE(8) console.log(`CRX version: ${version}, header length: ${headerLength}`) // ZIP content starts after the header zipStart = 12 + headerLength } else { throw new Error( "Invalid CRX file format - expected CRX3 with 'Cr24' header", ) } // Extract the ZIP portion const zipBuffer = crxBuffer.subarray(zipStart) console.log( `Extracting ZIP content from offset ${zipStart}, size: ${zipBuffer.length} bytes`, ) const zipPath = path.join(cacheDir, `phantom-${PHANTOM_VERSION}.zip`) await fs.writeFile(zipPath, zipBuffer) // Now extract the ZIP file await extract(zipPath, { dir: extractionPath }) // Clean up the temporary ZIP file await fs.remove(zipPath) // Verify extraction succeeded const manifestPath = path.join(extractionPath, "manifest.json") if (!(await fs.pathExists(manifestPath))) { throw new Error( `Extraction failed: manifest.json not found at ${manifestPath}`, ) } // Create flag file to indicate successful extraction await fs.writeFile(flagPath, new Date().toISOString()) console.log( `Phantom Wallet extension successfully prepared at: ${extractionPath}`, ) return extractionPath } catch (error) { console.error(`Error extracting CRX: ${error.message}`) // Clean up on failure try { await fs.emptyDir(extractionPath) } catch (cleanupError) { console.error(`Failed to clean up: ${cleanupError.message}`) } throw new Error(`Extraction failed: ${error.message}`) } } async function main() { try { console.log("Preparing Phantom Wallet extension for tests...") // Run the setup function const extensionPath = await setupPhantomExtraction() console.log( `Phantom Wallet extension prepared successfully at: ${extensionPath}`, ) console.log("You can now run the tests!") process.exit(0) } catch (error) { console.error("Failed to prepare Phantom Wallet extension:") console.error(error) process.exit(1) } } // Run the main function with proper promise handling main().catch(error => { console.error("Unhandled error in main function:", error) process.exit(1) })