UNPKG

@ledgerhq/coin-tester

Version:
130 lines 5.26 kB
import path from "path"; import chalk from "chalk"; import fs from "fs/promises"; import * as compose from "docker-compose"; import axios, { AxiosError } from "axios"; import { DeviceManagementKitTransportSpeculos } from "@ledgerhq/live-dmk-speculos"; import { deviceControllerClientFactory } from "@ledgerhq/speculos-device-controller"; const { SPECULOS_API_PORT } = process.env; const cwd = path.join(__dirname); export const delay = (timing) => new Promise(resolve => setTimeout(resolve, timing)); export function ensureEnv() { const mandatory_env_variables = ["SEED", "SPECULOS_API_PORT", "GH_TOKEN"]; const optional_env_variables = ["SPECULOS_IMAGE"]; if (!mandatory_env_variables.every(variable => !!process.env[variable])) { throw new Error(`Missing env variables. Make sure that ${mandatory_env_variables.join(",")} are in your .env`); } optional_env_variables.forEach(envVariable => { if (!process.env[envVariable]) { console.warn(`Variable ${envVariable} missing from .env. Using default value.`); } }); } async function downloadApp(nanoAppEndpoint) { const { data: blob } = await axios({ url: `https://raw.githubusercontent.com/LedgerHQ/coin-apps/master/nanox${nanoAppEndpoint}`, method: "GET", responseType: "stream", headers: { Authorization: `Bearer ${process.env.GH_TOKEN}`, }, }); return blob; } export async function spawnSpeculos(nanoAppEndpoint, options) { ensureEnv(); console.log(`Starting speculos...`); const libraryArgs = []; try { const blob = await downloadApp(nanoAppEndpoint); await fs.mkdir(path.resolve(cwd, "tmp"), { recursive: true }); await fs.writeFile(path.resolve(cwd, "tmp/app.elf"), blob, "binary"); if (options?.libraries) { for (const library of options.libraries) { const libBlob = await downloadApp(library.endpoint); const libFileName = `${library.name.toLowerCase()}.elf`; await fs.writeFile(path.resolve(cwd, "tmp", libFileName), libBlob, "binary"); libraryArgs.push(`-l ${library.name}:./apps/${libFileName}`); } } } catch (err) { if (err instanceof AxiosError) { throw new Error(`${err.status}: Failed to download the app.elf file from ${nanoAppEndpoint}\nMake sure that your GH_TOKEN is correct and has the right permissions.`); } throw err; } await compose.upOne("speculos", { cwd, log: Boolean(process.env.DEBUG), env: { ...process.env, SPECULOS_EXTRA_ARGS: libraryArgs.join(" "), }, }); async function checkSpeculosLogs() { const { out } = await compose.logs("speculos", { cwd, env: process.env }); if (out.includes("Server started")) { console.log(chalk.bgYellowBright.black(" - SPECULOS READY ✅ - ")); return DeviceManagementKitTransportSpeculos.open({ apiPort: SPECULOS_API_PORT, }); } await delay(200); return checkSpeculosLogs(); } function getOnSpeculosConfirmation(approvalText = "Accept") { return async function onSpeculosConfirmation(e) { if (e?.type !== "device-signature-requested") return; const baseURL = `http://127.0.0.1:${SPECULOS_API_PORT}`; const buttonClient = deviceControllerClientFactory(baseURL).buttonFactory(); const maxAttempts = 80; const delayMs = 250; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { const { data } = await axios.get(`${baseURL}/events?currentscreenonly=true`, { timeout: 2000, }); const text = data?.events?.[0]?.text; if (text === approvalText) { await buttonClient.both(); return; } // not on the approval screen yet—scroll right and retry await buttonClient.right(); } catch (err) { // retry on network issues if (axios.isAxiosError(err)) { // fall through to retry } else { throw err; } } await delay(delayMs); } throw new Error(`Speculos confirmation not reached after ${maxAttempts} attempts`); }; } return checkSpeculosLogs().then(transport => { return { transport, getOnSpeculosConfirmation, }; }); } export async function killSpeculos() { console.log("Stopping speculos..."); await compose.down({ cwd, log: Boolean(process.env.DEBUG), env: process.env, commandOptions: ["--remove-orphans"], }); } ["exit", "SIGINT", "SIGQUIT", "SIGTERM", "SIGUSR1", "SIGUSR2", "uncaughtException"].map(e => process.on(e, async () => { await killSpeculos(); })); //# sourceMappingURL=speculos.js.map