@zondax/zemu
Advanced tools
Comparing version 0.34.0 to 0.35.0-beta.1
@@ -1,2 +0,18 @@ | ||
export declare const DEFAULT_EMU_IMG = "zondax/builder-zemu@sha256:7cae0f781ea6f6a58c39f273763bb61176b377bd0d6c713e59ae38e0531ae4ab"; | ||
/** ****************************************************************************** | ||
* (c) 2018 - 2023 Zondax AG | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
******************************************************************************* */ | ||
import { type IDeviceWindow, type IStartOptions } from "./types"; | ||
export declare const DEFAULT_EMU_IMG = "zondax/builder-zemu@sha256:8d7b06cedf2d018b9464f4af4b7a8357c3fbb180f3ab153f8cb8f138defb22a4"; | ||
export declare const DEFAULT_MODEL = "nanos"; | ||
@@ -10,2 +26,8 @@ export declare const DEFAULT_START_TEXT = "Ready"; | ||
export declare const KILL_TIMEOUT = 5000; | ||
export declare const DEFAULT_METHOD_TIMEOUT = 10000; | ||
export declare const DEFAULT_NANO_APPROVE_KEYWORD = "APPROVE"; | ||
export declare const DEFAULT_NANO_REJECT_KEYWORD = "REJECT"; | ||
export declare const DEFAULT_STAX_APPROVE_KEYWORD = "APPROVE"; | ||
export declare const DEFAULT_STAX_REJECT_KEYWORD = "Cancel"; | ||
export declare const DEFAULT_START_OPTIONS: IStartOptions; | ||
export declare const KEYS: { | ||
@@ -17,13 +39,4 @@ NOT_PRESSED: number; | ||
}; | ||
export declare const WINDOW_S: { | ||
x: number; | ||
y: number; | ||
width: number; | ||
height: number; | ||
}; | ||
export declare const WINDOW_X: { | ||
x: number; | ||
y: number; | ||
width: number; | ||
height: number; | ||
}; | ||
export declare const WINDOW_S: IDeviceWindow; | ||
export declare const WINDOW_X: IDeviceWindow; | ||
export declare const WINDOW_STAX: IDeviceWindow; |
"use strict"; | ||
exports.__esModule = true; | ||
exports.WINDOW_X = exports.WINDOW_S = exports.KEYS = exports.KILL_TIMEOUT = exports.DEFAULT_START_TIMEOUT = exports.BASE_NAME = exports.DEFAULT_HOST = exports.DEFAULT_KEY_DELAY = exports.DEFAULT_START_DELAY = exports.DEFAULT_START_TEXT = exports.DEFAULT_MODEL = exports.DEFAULT_EMU_IMG = void 0; | ||
exports.DEFAULT_EMU_IMG = 'zondax/builder-zemu@sha256:7cae0f781ea6f6a58c39f273763bb61176b377bd0d6c713e59ae38e0531ae4ab'; | ||
exports.DEFAULT_MODEL = 'nanos'; | ||
exports.DEFAULT_START_TEXT = 'Ready'; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.WINDOW_STAX = exports.WINDOW_X = exports.WINDOW_S = exports.KEYS = exports.DEFAULT_START_OPTIONS = exports.DEFAULT_STAX_REJECT_KEYWORD = exports.DEFAULT_STAX_APPROVE_KEYWORD = exports.DEFAULT_NANO_REJECT_KEYWORD = exports.DEFAULT_NANO_APPROVE_KEYWORD = exports.DEFAULT_METHOD_TIMEOUT = exports.KILL_TIMEOUT = exports.DEFAULT_START_TIMEOUT = exports.BASE_NAME = exports.DEFAULT_HOST = exports.DEFAULT_KEY_DELAY = exports.DEFAULT_START_DELAY = exports.DEFAULT_START_TEXT = exports.DEFAULT_MODEL = exports.DEFAULT_EMU_IMG = void 0; | ||
exports.DEFAULT_EMU_IMG = "zondax/builder-zemu@sha256:8d7b06cedf2d018b9464f4af4b7a8357c3fbb180f3ab153f8cb8f138defb22a4"; | ||
exports.DEFAULT_MODEL = "nanos"; | ||
exports.DEFAULT_START_TEXT = "Ready"; | ||
exports.DEFAULT_START_DELAY = 20000; | ||
exports.DEFAULT_KEY_DELAY = 100; | ||
exports.DEFAULT_HOST = '127.0.0.1'; | ||
exports.BASE_NAME = 'zemu-test-'; | ||
exports.DEFAULT_HOST = "127.0.0.1"; | ||
exports.BASE_NAME = "zemu-test-"; | ||
exports.DEFAULT_START_TIMEOUT = 20000; | ||
exports.KILL_TIMEOUT = 5000; | ||
exports.DEFAULT_METHOD_TIMEOUT = 10000; | ||
exports.DEFAULT_NANO_APPROVE_KEYWORD = "APPROVE"; | ||
exports.DEFAULT_NANO_REJECT_KEYWORD = "REJECT"; | ||
exports.DEFAULT_STAX_APPROVE_KEYWORD = "APPROVE"; | ||
exports.DEFAULT_STAX_REJECT_KEYWORD = "Cancel"; | ||
exports.DEFAULT_START_OPTIONS = { | ||
logging: false, | ||
startDelay: exports.DEFAULT_START_DELAY, | ||
custom: "", | ||
model: exports.DEFAULT_MODEL, | ||
sdk: "", | ||
startText: exports.DEFAULT_START_TEXT, | ||
caseSensitive: false, | ||
startTimeout: exports.DEFAULT_START_TIMEOUT, | ||
approveAction: 9 /* ButtonKind.ApproveHoldButton */, | ||
approveKeyword: "", | ||
rejectKeyword: "", | ||
}; | ||
exports.KEYS = { | ||
@@ -17,3 +35,3 @@ NOT_PRESSED: 0, | ||
LEFT: 0xff51, | ||
RIGHT: 0xff53 | ||
RIGHT: 0xff53, | ||
}; | ||
@@ -24,3 +42,3 @@ exports.WINDOW_S = { | ||
width: 128, | ||
height: 32 | ||
height: 32, | ||
}; | ||
@@ -31,3 +49,9 @@ exports.WINDOW_X = { | ||
width: 128, | ||
height: 64 | ||
height: 64, | ||
}; | ||
exports.WINDOW_STAX = { | ||
x: 0, | ||
y: 0, | ||
width: 400, | ||
height: 672, | ||
}; |
@@ -9,9 +9,7 @@ export declare const DEV_CERT_PRIVATE_KEY = "ff701d781f43ce106f72dc26a46b6a83e053b5d07bb3d4ceab79c91ca822a66b"; | ||
private readonly image; | ||
private libElfs; | ||
private currentContainer; | ||
constructor(elfLocalPath: string, libElfs: { | ||
[p: string]: string; | ||
}, image: string, name: string); | ||
static killContainerByName(name: string): Promise<void>; | ||
static checkAndPullImage(imageName: string): Promise<any>; | ||
private readonly libElfs; | ||
private currentContainer?; | ||
constructor(elfLocalPath: string, libElfs: Record<string, string>, image: string, name: string); | ||
static killContainerByName(name: string): void; | ||
static checkAndPullImage(imageName: string): void; | ||
log(message: string): void; | ||
@@ -18,0 +16,0 @@ runContainer(options: { |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
exports.__esModule = true; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.DEFAULT_APP_PATH = exports.BOLOS_SDK = exports.DEV_CERT_PRIVATE_KEY = void 0; | ||
/** ****************************************************************************** | ||
* (c) 2020 Zondax GmbH | ||
* (c) 2018 - 2023 Zondax AG | ||
* | ||
@@ -58,10 +22,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
******************************************************************************* */ | ||
var path_1 = __importDefault(require("path")); | ||
var dockerode_1 = __importDefault(require("dockerode")); | ||
exports.DEV_CERT_PRIVATE_KEY = 'ff701d781f43ce106f72dc26a46b6a83e053b5d07bb3d4ceab79c91ca822a66b'; | ||
exports.BOLOS_SDK = '/project/deps/nanos-secure-sdk'; | ||
exports.DEFAULT_APP_PATH = '/project/app/bin'; | ||
var EmuContainer = /** @class */ (function () { | ||
function EmuContainer(elfLocalPath, libElfs, image, name) { | ||
// eslint-disable-next-line global-require | ||
const dockerode_1 = __importDefault(require("dockerode")); | ||
const path_1 = __importDefault(require("path")); | ||
exports.DEV_CERT_PRIVATE_KEY = "ff701d781f43ce106f72dc26a46b6a83e053b5d07bb3d4ceab79c91ca822a66b"; | ||
exports.BOLOS_SDK = "/project/deps/nanos-secure-sdk"; | ||
exports.DEFAULT_APP_PATH = "/project/app/bin"; | ||
class EmuContainer { | ||
constructor(elfLocalPath, libElfs, image, name) { | ||
this.image = image; | ||
@@ -73,186 +36,130 @@ this.elfLocalPath = elfLocalPath; | ||
} | ||
EmuContainer.killContainerByName = function (name) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var docker; | ||
return __generator(this, function (_a) { | ||
docker = new dockerode_1["default"](); | ||
return [2 /*return*/, new Promise(function (resolve) { | ||
docker.listContainers({ all: true, filters: { name: [name] } }, function (listError, containers) { | ||
if (listError) | ||
throw listError; | ||
if (!(containers === null || containers === void 0 ? void 0 : containers.length)) { | ||
console.log('No containers found'); | ||
return; | ||
} | ||
containers.forEach(function (containerInfo) { | ||
docker.getContainer(containerInfo.Id).remove({ force: true }, function (removeError) { | ||
if (removeError) | ||
throw removeError; | ||
}); | ||
}); | ||
}); | ||
resolve(); | ||
})]; | ||
static killContainerByName(name) { | ||
const docker = new dockerode_1.default(); | ||
docker.listContainers({ all: true, filters: { name: [name] } }, (listError, containers) => { | ||
if (listError != null) | ||
throw listError; | ||
if (containers == null || containers.length === 0) { | ||
console.log("No containers found"); | ||
return; | ||
} | ||
containers.forEach((containerInfo) => { | ||
docker.getContainer(containerInfo.Id).remove({ force: true }, (removeError) => { | ||
if (removeError != null) | ||
throw removeError; | ||
}); | ||
}); | ||
}); | ||
}; | ||
EmuContainer.checkAndPullImage = function (imageName) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var docker; | ||
return __generator(this, function (_a) { | ||
docker = new dockerode_1["default"](); | ||
return [2 /*return*/, docker.pull(imageName, function (err, stream) { | ||
function onProgress(event) { | ||
// eslint-disable-next-line no-prototype-builtins | ||
var progress = event.hasOwnProperty('progress') ? event.progress : ''; | ||
// eslint-disable-next-line no-prototype-builtins | ||
var status = event.hasOwnProperty('status') ? event.status : ''; | ||
process.stdout.write("[DOCKER] ".concat(status, ": ").concat(progress, "\n")); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
function onFinished(err, output) { | ||
if (err) { | ||
process.stdout.write("[DOCKER] ".concat(err, "\n")); | ||
throw err; | ||
} | ||
} | ||
if (err) { | ||
process.stdout.write("[DOCKER] ".concat(err, "\n")); | ||
throw new Error(err); | ||
} | ||
docker.modem.followProgress(stream, onFinished, onProgress); | ||
})]; | ||
}); | ||
} | ||
static checkAndPullImage(imageName) { | ||
const docker = new dockerode_1.default(); | ||
docker.pull(imageName, {}, (err, stream) => { | ||
function onProgress(event) { | ||
const progress = event?.progress ?? ""; | ||
const status = event?.status ?? ""; | ||
process.stdout.write(`[DOCKER] ${status}: ${progress}\n`); | ||
} | ||
function onFinished(err, _output) { | ||
if (err != null) { | ||
process.stdout.write(`[DOCKER] ${err}\n`); | ||
throw err; | ||
} | ||
} | ||
if (err != null) { | ||
process.stdout.write(`[DOCKER] ${err}\n`); | ||
throw new Error(err); | ||
} | ||
docker.modem.followProgress(stream, onFinished, onProgress); | ||
}); | ||
}; | ||
EmuContainer.prototype.log = function (message) { | ||
var _a; | ||
if ((_a = this.logging) !== null && _a !== void 0 ? _a : false) { | ||
process.stdout.write("".concat(message, "\n")); | ||
} | ||
log(message) { | ||
if (this.logging ?? false) | ||
process.stdout.write(`${message}\n`); | ||
} | ||
async runContainer(options) { | ||
const docker = new dockerode_1.default(); | ||
this.logging = options.logging; | ||
const appFilename = path_1.default.basename(this.elfLocalPath); | ||
const appDir = path_1.default.dirname(this.elfLocalPath); | ||
const dirBindings = [`${appDir}:${exports.DEFAULT_APP_PATH}`]; | ||
let libArgs = ""; | ||
Object.entries(this.libElfs).forEach(([libName, libPath]) => { | ||
const libFilename = path_1.default.basename(libPath); | ||
libArgs += ` -l ${libName}:${exports.DEFAULT_APP_PATH}/${libFilename}`; | ||
}); | ||
const modelOptions = options.model !== "" ? options.model : "nanos"; | ||
if (modelOptions === "nanosp" && options.sdk === "") | ||
options.sdk = "1.0.3"; | ||
const sdkOption = options.sdk !== "" ? `-k ${options.sdk}` : ""; | ||
if (sdkOption !== "") | ||
this.log(`[ZEMU] Using SDK ${modelOptions} with version ${options.sdk}`); | ||
const customOptions = options.custom; | ||
const displaySetting = "--display headless"; | ||
const command = `/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN ${displaySetting} ${customOptions} -m ${modelOptions} ${sdkOption} ${exports.DEFAULT_APP_PATH}/${appFilename} ${libArgs}`; | ||
this.log(`[ZEMU] Command: ${command}`); | ||
const portBindings = { | ||
[`9998/tcp`]: [{ HostPort: options.transportPort }], | ||
[`5000/tcp`]: [{ HostPort: options.speculosApiPort }], | ||
}; | ||
if (customOptions.includes("--debug")) { | ||
portBindings[`1234/tcp`] = [{ HostPort: "1234" }]; | ||
} | ||
}; | ||
EmuContainer.prototype.runContainer = function (options) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var docker, appFilename, appDir, dirBindings, libArgs, modelOptions, sdkOption, customOptions, displaySetting, command, portBindings, displayEnvironment, environment, _b; | ||
var _c; | ||
return __generator(this, function (_d) { | ||
switch (_d.label) { | ||
case 0: | ||
docker = new dockerode_1["default"](); | ||
this.logging = options.logging; | ||
appFilename = path_1["default"].basename(this.elfLocalPath); | ||
appDir = path_1["default"].dirname(this.elfLocalPath); | ||
dirBindings = ["".concat(appDir, ":").concat(exports.DEFAULT_APP_PATH)]; | ||
libArgs = ''; | ||
Object.entries(this.libElfs).forEach(function (_a) { | ||
var libName = _a[0], libPath = _a[1]; | ||
var libFilename = path_1["default"].basename(libPath); | ||
libArgs += " -l ".concat(libName, ":").concat(exports.DEFAULT_APP_PATH, "/").concat(libFilename); | ||
}); | ||
modelOptions = (options === null || options === void 0 ? void 0 : options.model) ? options.model : 'nanos'; | ||
if (modelOptions === 'nanosp') | ||
options.sdk = '1.0.3'; | ||
sdkOption = (options === null || options === void 0 ? void 0 : options.sdk) ? " -k ".concat(options.sdk, " ") : ''; | ||
if (sdkOption) | ||
this.log("[ZEMU] Using SDK ".concat(modelOptions, " with version ").concat(options.sdk)); | ||
customOptions = ''; | ||
if (options.custom) { | ||
customOptions = options.custom; | ||
} | ||
displaySetting = '--display headless'; | ||
command = "/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN ".concat(displaySetting, " ").concat(customOptions, " -m ").concat(modelOptions, " ").concat(sdkOption, " ").concat(exports.DEFAULT_APP_PATH, "/").concat(appFilename, " ").concat(libArgs); | ||
this.log("[ZEMU] Command: ".concat(command)); | ||
portBindings = (_c = {}, | ||
_c["9998/tcp"] = [{ HostPort: options.transportPort }], | ||
_c["5000/tcp"] = [{ HostPort: options.speculosApiPort }], | ||
_c); | ||
if (customOptions.indexOf('--debug') > -1) { | ||
portBindings["1234/tcp"] = [{ HostPort: '1234' }]; | ||
} | ||
displayEnvironment = process.platform === 'darwin' ? 'host.docker.internal:0' : (_a = process.env.DISPLAY) !== null && _a !== void 0 ? _a : ''; | ||
environment = [ | ||
"SCP_PRIVKEY='".concat(exports.DEV_CERT_PRIVATE_KEY, "'"), | ||
"BOLOS_SDK='".concat(exports.BOLOS_SDK, "'"), | ||
"BOLOS_ENV='/opt/bolos'", | ||
"DISPLAY='".concat(displayEnvironment, "'"), | ||
]; | ||
this.log("[ZEMU] Creating Container ".concat(this.image, " - ").concat(this.name, " ")); | ||
_b = this; | ||
return [4 /*yield*/, docker.createContainer({ | ||
Image: this.image, | ||
name: this.name, | ||
Tty: true, | ||
AttachStdout: true, | ||
AttachStderr: true, | ||
User: '1000', | ||
Env: environment, | ||
HostConfig: { | ||
PortBindings: portBindings, | ||
Binds: dirBindings | ||
}, | ||
Cmd: [command] | ||
})]; | ||
case 1: | ||
_b.currentContainer = _d.sent(); | ||
this.log("[ZEMU] Connected ".concat(this.currentContainer.id)); | ||
if (this.logging) { | ||
this.currentContainer.attach({ stream: true, stdout: true, stderr: true }, function (err, stream) { | ||
stream.pipe(process.stdout); | ||
}); | ||
this.log("[ZEMU] Attached ".concat(this.currentContainer.id)); | ||
} | ||
return [4 /*yield*/, this.currentContainer.start()]; | ||
case 2: | ||
_d.sent(); | ||
this.log("[ZEMU] Started ".concat(this.currentContainer.id)); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
const displayEnvironment = process.platform === "darwin" ? "host.docker.internal:0" : process.env.DISPLAY ?? ""; | ||
const environment = [ | ||
`SCP_PRIVKEY='${exports.DEV_CERT_PRIVATE_KEY}'`, | ||
`BOLOS_SDK='${exports.BOLOS_SDK}'`, | ||
`BOLOS_ENV='/opt/bolos'`, | ||
`DISPLAY='${displayEnvironment}'`, | ||
]; | ||
this.log(`[ZEMU] Creating Container ${this.image} - ${this.name} `); | ||
this.currentContainer = await docker.createContainer({ | ||
Image: this.image, | ||
name: this.name, | ||
Tty: true, | ||
AttachStdout: true, | ||
AttachStderr: true, | ||
User: "1000", | ||
Env: environment, | ||
HostConfig: { | ||
PortBindings: portBindings, | ||
Binds: dirBindings, | ||
}, | ||
Cmd: [command], | ||
}); | ||
}; | ||
EmuContainer.prototype.stop = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var container, e_1, err_1; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!this.currentContainer) return [3 /*break*/, 9]; | ||
container = this.currentContainer; | ||
this.currentContainer = null; | ||
this.log("[ZEMU] Stopping container"); | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, container.stop({ t: 0 })]; | ||
case 2: | ||
_a.sent(); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
e_1 = _a.sent(); | ||
this.log("[ZEMU] Stopping: ".concat(e_1)); | ||
throw e_1; | ||
case 4: | ||
this.log("[ZEMU] Stopped"); | ||
_a.label = 5; | ||
case 5: | ||
_a.trys.push([5, 7, , 8]); | ||
return [4 /*yield*/, container.remove()]; | ||
case 6: | ||
_a.sent(); | ||
return [3 /*break*/, 8]; | ||
case 7: | ||
err_1 = _a.sent(); | ||
this.log('[ZEMU] Unable to remove container'); | ||
throw err_1; | ||
case 8: | ||
this.log("[ZEMU] Removed"); | ||
_a.label = 9; | ||
case 9: return [2 /*return*/]; | ||
} | ||
this.log(`[ZEMU] Connected ${this.currentContainer.id}`); | ||
if (this.logging) { | ||
this.currentContainer.attach({ stream: true, stdout: true, stderr: true }, (err, stream) => { | ||
if (err != null) | ||
throw err; | ||
stream.pipe(process.stdout); | ||
}); | ||
}); | ||
}; | ||
return EmuContainer; | ||
}()); | ||
exports["default"] = EmuContainer; | ||
this.log(`[ZEMU] Attached ${this.currentContainer.id}`); | ||
} | ||
await this.currentContainer.start(); | ||
this.log(`[ZEMU] Started ${this.currentContainer.id}`); | ||
} | ||
async stop() { | ||
if (this.currentContainer != null) { | ||
const container = this.currentContainer; | ||
delete this.currentContainer; | ||
this.log(`[ZEMU] Stopping container`); | ||
try { | ||
await container.stop({ t: 0 }); | ||
} | ||
catch (e) { | ||
this.log(`[ZEMU] Stopping: ${e}`); | ||
throw e; | ||
} | ||
this.log(`[ZEMU] Stopped`); | ||
try { | ||
await container.remove(); | ||
} | ||
catch (err) { | ||
this.log("[ZEMU] Unable to remove container"); | ||
throw err; | ||
} | ||
this.log(`[ZEMU] Removed`); | ||
} | ||
} | ||
} | ||
exports.default = EmuContainer; |
export default class GRPCRouter { | ||
private httpTransport; | ||
private serverAddress; | ||
private server; | ||
constructor(ip: string, port: number, options: { | ||
debug?: any; | ||
}, transport: any); | ||
startServer(): Promise<void>; | ||
private readonly httpTransport; | ||
private readonly serverAddress; | ||
private readonly server; | ||
constructor(ip: string, port: number, transport: any); | ||
startServer(): void; | ||
stopServer(): void; | ||
} |
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
exports.__esModule = true; | ||
var PROTO_PATH = "".concat(__dirname, "/zemu.proto"); | ||
var protoLoader = require('@grpc/proto-loader'); | ||
var grpc = require('@grpc/grpc-js'); | ||
var GRPCRouter = /** @class */ (function () { | ||
function GRPCRouter(ip, port, options, transport) { | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const grpc_js_1 = require("@grpc/grpc-js"); | ||
const proto_loader_1 = require("@grpc/proto-loader"); | ||
const path_1 = require("path"); | ||
const PROTO_PATH = (0, path_1.resolve)(__dirname, "zemu.proto"); | ||
class GRPCRouter { | ||
constructor(ip, port, transport) { | ||
this.httpTransport = transport; | ||
this.serverAddress = "".concat(ip, ":").concat(port); | ||
this.server = new grpc.Server(); | ||
this.serverAddress = `${ip}:${port}`; | ||
this.server = new grpc_js_1.Server(); | ||
} | ||
GRPCRouter.prototype.startServer = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var packageDefinition, rpcDefinition, self; | ||
var _this = this; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, protoLoader.load(PROTO_PATH, { | ||
keepCase: true, | ||
longs: String, | ||
enums: String, | ||
defaults: true, | ||
oneofs: true | ||
})]; | ||
case 1: | ||
packageDefinition = _a.sent(); | ||
rpcDefinition = grpc.loadPackageDefinition(packageDefinition); | ||
self = this; | ||
this.server.addService(rpcDefinition.ledger_go.ZemuCommand.service, { | ||
Exchange: function (call, callback, ctx) { | ||
if (ctx === void 0) { ctx = self; } | ||
ctx.httpTransport.exchange(call.request.command).then(function (response) { | ||
callback(null, { reply: response }); | ||
}); | ||
} | ||
}); | ||
this.server.bindAsync(this.serverAddress, grpc.ServerCredentials.createInsecure(), | ||
// eslint-disable-next-line no-unused-vars | ||
function (err, port) { | ||
if (err != null) { | ||
return console.error(err); | ||
} | ||
process.stdout.write("gRPC listening on ".concat(port)); | ||
_this.server.start(); | ||
}); | ||
process.stdout.write("grpc server started on ".concat(this.serverAddress)); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
startServer() { | ||
const packageDefinition = (0, proto_loader_1.loadSync)(PROTO_PATH, { | ||
keepCase: true, | ||
longs: String, | ||
enums: String, | ||
defaults: true, | ||
oneofs: true, | ||
}); | ||
}; | ||
GRPCRouter.prototype.stopServer = function () { | ||
const rpcDefinition = (0, grpc_js_1.loadPackageDefinition)(packageDefinition); | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
const self = this; | ||
// @ts-expect-error types are missing | ||
this.server.addService(rpcDefinition.ledger_go.ZemuCommand.service, { | ||
Exchange(call, callback, ctx = self) { | ||
void ctx.httpTransport.exchange(call.request.command).then((response) => { | ||
callback(null, { reply: response }); | ||
}); | ||
}, | ||
}); | ||
this.server.bindAsync(this.serverAddress, grpc_js_1.ServerCredentials.createInsecure(), (err, port) => { | ||
if (err != null) { | ||
console.error(err); | ||
return; | ||
} | ||
process.stdout.write(`gRPC listening on ${port}`); | ||
this.server.start(); | ||
}); | ||
process.stdout.write(`grpc server started on ${this.serverAddress}`); | ||
} | ||
stopServer() { | ||
this.server.forceShutdown(); | ||
}; | ||
return GRPCRouter; | ||
}()); | ||
exports["default"] = GRPCRouter; | ||
} | ||
} | ||
exports.default = GRPCRouter; |
/** ****************************************************************************** | ||
* (c) 2020 Zondax GmbH | ||
* (c) 2018 - 2023 Zondax AG | ||
* | ||
@@ -16,92 +16,7 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
******************************************************************************* */ | ||
/// <reference types="node" /> | ||
import PNG from 'pngjs'; | ||
import Transport from '@ledgerhq/hw-transport'; | ||
export declare const DEFAULT_START_OPTIONS: StartOptions; | ||
export declare class StartOptions { | ||
logging: boolean; | ||
startDelay: number; | ||
custom: string; | ||
model: string; | ||
sdk: string; | ||
startText: string; | ||
caseSensitive: boolean; | ||
startTimeout: number; | ||
} | ||
export interface Snapshot { | ||
width: number; | ||
height: number; | ||
} | ||
export declare class DeviceModel { | ||
name: string; | ||
prefix: string; | ||
path: string; | ||
} | ||
export default class Zemu { | ||
private startOptions; | ||
private host; | ||
private transportPort?; | ||
protected speculosApiPort?: number; | ||
private desiredTransportPort?; | ||
private desiredSpeculosApiPort?; | ||
private transportProtocol; | ||
private elfPath; | ||
private grpcManager; | ||
private mainMenuSnapshot; | ||
private libElfs; | ||
private emuContainer; | ||
private transport; | ||
private containerName; | ||
constructor(elfPath: string, libElfs?: { | ||
[key: string]: string; | ||
}, host?: string, desiredTransportPort?: number, desiredSpeculosApiPort?: number); | ||
static LoadPng2RGB(filename: string): PNG.PNGWithMetadata; | ||
static delay(v?: number): void; | ||
static sleep(ms: number): Promise<void>; | ||
static delayedPromise(p: Promise<any>, delay: number): Promise<void>; | ||
static stopAllEmuContainers(): Promise<void>; | ||
static checkAndPullImage(): Promise<void>; | ||
static checkElf(model: string, elfPath: string): void; | ||
start(options: StartOptions): Promise<void>; | ||
connect(): Promise<void>; | ||
log(message: string): void; | ||
startGRPCServer(ip: string, port: number, options?: {}): void; | ||
stopGRPCServer(): void; | ||
close(): Promise<void>; | ||
getTransport(): Transport; | ||
getWindowRect(): { | ||
x: number; | ||
y: number; | ||
width: number; | ||
height: number; | ||
}; | ||
fetchSnapshot(url: string): Promise<import("axios").AxiosResponse<any, any>>; | ||
saveSnapshot(arrayBuffer: Buffer, filePath: string): void; | ||
convertBufferToPNG(arrayBuffer: Buffer): PNG.PNGWithMetadata; | ||
snapshot(filename?: string): Promise<any>; | ||
getMainMenuSnapshot(): Promise<null>; | ||
waitUntilScreenIsNot(screen: any, timeout?: number): Promise<void>; | ||
formatIndexString(i: number): string; | ||
getSnapshotPath(snapshotPrefix: string, index: number, takeSnapshots: boolean): string | undefined; | ||
navigate(path: string, testcaseName: string, clickSchedule: number[], waitForScreenUpdate?: boolean, takeSnapshots?: boolean, startImgIndex?: number): Promise<number>; | ||
takeSnapshotAndOverwrite(path: string, testcaseName: string, imageIndex: number): Promise<void>; | ||
navigateAndCompareSnapshots(path: string, testcaseName: string, clickSchedule: number[], waitForScreenUpdate?: boolean, startImgIndex?: number): Promise<boolean>; | ||
compareSnapshots(path: string, testcaseName: string, snapshotCount: number): boolean; | ||
/** | ||
* @deprecated The method will be deprecated soon. Try to use navigateAndCompareSnapshots instead | ||
*/ | ||
compareSnapshotsAndAccept(path: string, testcaseName: string, snapshotCount: number, backClickCount?: number): Promise<boolean>; | ||
compareSnapshotsAndApprove(path: string, testcaseName: string, waitForScreenUpdate?: boolean, startImgIndex?: number, timeout?: number): Promise<boolean>; | ||
navigateUntilText(path: string, testcaseName: string, text: string, waitForScreenUpdate?: boolean, takeSnapshots?: boolean, startImgIndex?: number, timeout?: number): Promise<number>; | ||
navigateAndCompareUntilText(path: string, testcaseName: string, text: string, waitForScreenUpdate?: boolean, startImgIndex?: number, timeout?: number): Promise<boolean>; | ||
getEvents(): Promise<any>; | ||
deleteEvents(): Promise<void>; | ||
dumpEvents(): Promise<void>; | ||
waitScreenChange(timeout?: number): Promise<void>; | ||
waitForText(text: string | RegExp, timeout?: number, caseSensitive?: boolean): Promise<void>; | ||
click(endpoint: string, filename?: string, waitForScreenUpdate?: boolean): Promise<any>; | ||
clickLeft(filename?: string, waitForScreenUpdate?: boolean): Promise<any>; | ||
clickRight(filename?: string, waitForScreenUpdate?: boolean): Promise<any>; | ||
clickBoth(filename?: string, waitForScreenUpdate?: boolean): Promise<any>; | ||
private assignPortsToListen; | ||
} | ||
import Zemu from "./Zemu"; | ||
export default Zemu; | ||
export { ClickNavigation, TouchNavigation } from "./actions"; | ||
export { DEFAULT_START_OPTIONS } from "./constants"; | ||
export { ButtonKind, type IDeviceModel, type INavElement, type IStartOptions } from "./types"; | ||
export { zondaxMainmenuNavigation } from "./zondax"; |
"use strict"; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.zondaxMainmenuNavigation = exports.DEFAULT_START_OPTIONS = exports.TouchNavigation = exports.ClickNavigation = void 0; | ||
/** ****************************************************************************** | ||
* (c) 2020 Zondax GmbH | ||
* (c) 2018 - 2023 Zondax AG | ||
* | ||
@@ -17,867 +22,10 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
******************************************************************************* */ | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
exports.__esModule = true; | ||
exports.DeviceModel = exports.StartOptions = exports.DEFAULT_START_OPTIONS = void 0; | ||
var axios_1 = __importDefault(require("axios")); | ||
var axios_retry_1 = __importDefault(require("axios-retry")); | ||
var fs_extra_1 = __importDefault(require("fs-extra")); | ||
var get_port_1 = __importDefault(require("get-port")); | ||
var pngjs_1 = __importDefault(require("pngjs")); | ||
var hw_transport_http_1 = __importDefault(require("@ledgerhq/hw-transport-http")); | ||
// @ts-expect-error | ||
var elfy_1 = __importDefault(require("elfy")); | ||
var path_1 = require("path"); | ||
var randomstring_1 = __importDefault(require("randomstring")); | ||
const Zemu_1 = __importDefault(require("./Zemu")); | ||
exports.default = Zemu_1.default; | ||
var actions_1 = require("./actions"); | ||
Object.defineProperty(exports, "ClickNavigation", { enumerable: true, get: function () { return actions_1.ClickNavigation; } }); | ||
Object.defineProperty(exports, "TouchNavigation", { enumerable: true, get: function () { return actions_1.TouchNavigation; } }); | ||
var constants_1 = require("./constants"); | ||
var emulator_1 = __importDefault(require("./emulator")); | ||
var grpc_1 = __importDefault(require("./grpc")); | ||
exports.DEFAULT_START_OPTIONS = { | ||
model: constants_1.DEFAULT_MODEL, | ||
sdk: '', | ||
logging: false, | ||
custom: '', | ||
startDelay: constants_1.DEFAULT_START_DELAY, | ||
startText: constants_1.DEFAULT_START_TEXT, | ||
caseSensitive: false, | ||
startTimeout: constants_1.DEFAULT_START_TIMEOUT | ||
}; | ||
var StartOptions = /** @class */ (function () { | ||
function StartOptions() { | ||
this.logging = false; | ||
this.startDelay = constants_1.DEFAULT_START_DELAY; | ||
this.custom = ''; | ||
this.model = constants_1.DEFAULT_MODEL; | ||
this.sdk = ''; | ||
this.startText = constants_1.DEFAULT_START_TEXT; | ||
this.caseSensitive = false; | ||
this.startTimeout = constants_1.DEFAULT_START_TIMEOUT; | ||
} | ||
return StartOptions; | ||
}()); | ||
exports.StartOptions = StartOptions; | ||
var DeviceModel = /** @class */ (function () { | ||
function DeviceModel() { | ||
} | ||
return DeviceModel; | ||
}()); | ||
exports.DeviceModel = DeviceModel; | ||
var Zemu = /** @class */ (function () { | ||
function Zemu(elfPath, libElfs, host, desiredTransportPort, desiredSpeculosApiPort) { | ||
if (libElfs === void 0) { libElfs = {}; } | ||
if (host === void 0) { host = constants_1.DEFAULT_HOST; } | ||
this.transportProtocol = 'http'; | ||
this.host = host; | ||
this.desiredTransportPort = desiredTransportPort; | ||
this.desiredSpeculosApiPort = desiredSpeculosApiPort; | ||
this.elfPath = elfPath; | ||
this.libElfs = libElfs; | ||
this.mainMenuSnapshot = null; | ||
if (this.elfPath == null) { | ||
throw new Error('elfPath cannot be null!'); | ||
} | ||
if (!fs_extra_1["default"].existsSync(this.elfPath)) { | ||
throw new Error('elf file was not found! Did you compile?'); | ||
} | ||
Object.keys(libElfs).forEach(function (libName) { | ||
if (!fs_extra_1["default"].existsSync(libElfs[libName])) { | ||
throw new Error('lib elf file was not found! Did you compile?'); | ||
} | ||
}); | ||
this.containerName = constants_1.BASE_NAME + randomstring_1["default"].generate(12); // generate 12 chars long string | ||
this.emuContainer = new emulator_1["default"](this.elfPath, this.libElfs, constants_1.DEFAULT_EMU_IMG, this.containerName); | ||
} | ||
Zemu.LoadPng2RGB = function (filename) { | ||
var tmpBuffer = fs_extra_1["default"].readFileSync(filename); | ||
return pngjs_1["default"].PNG.sync.read(tmpBuffer); | ||
}; | ||
Zemu.delay = function (v) { | ||
if (v === void 0) { v = constants_1.DEFAULT_KEY_DELAY; } | ||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, v); | ||
}; | ||
Zemu.sleep = function (ms) { | ||
return new Promise(function (resolve) { return setTimeout(resolve, ms); }); | ||
}; | ||
Zemu.delayedPromise = function (p, delay) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, Promise.race([ | ||
p, | ||
new Promise(function (resolve) { | ||
setTimeout(resolve, delay); | ||
}), | ||
])]; | ||
case 1: | ||
_a.sent(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.stopAllEmuContainers = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var timer; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
timer = setTimeout(function () { | ||
console.log('Could not kill all containers before timeout!'); | ||
process.exit(1); | ||
}, constants_1.KILL_TIMEOUT); | ||
return [4 /*yield*/, emulator_1["default"].killContainerByName(constants_1.BASE_NAME)]; | ||
case 1: | ||
_a.sent(); | ||
clearTimeout(timer); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.checkAndPullImage = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, emulator_1["default"].checkAndPullImage(constants_1.DEFAULT_EMU_IMG)]; | ||
case 1: | ||
_a.sent(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.checkElf = function (model, elfPath) { | ||
var elfCodeNanoS = 0xc0d00001; | ||
var elfCodeNanoX = 0xc0de0001; | ||
var elfCodeNanoSP = 0xc0de0001; | ||
var elfApp = fs_extra_1["default"].readFileSync(elfPath); | ||
var elfInfo = elfy_1["default"].parse(elfApp); | ||
if (elfInfo.entry !== elfCodeNanoS && elfInfo.entry !== elfCodeNanoX && elfInfo.entry !== elfCodeNanoSP) { | ||
throw new Error('Are you sure is a Nano S/S+/X app ?'); | ||
} | ||
}; | ||
Zemu.prototype.start = function (options) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var e, _b, e_1; | ||
var _this = this; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
this.startOptions = options; | ||
this.log("Checking ELF"); | ||
Zemu.checkElf((_a = this.startOptions.model) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_MODEL, this.elfPath); | ||
_c.label = 1; | ||
case 1: | ||
_c.trys.push([1, 7, , 8]); | ||
return [4 /*yield*/, this.assignPortsToListen()]; | ||
case 2: | ||
_c.sent(); | ||
if (!this.transportPort || !this.speculosApiPort) { | ||
e = new Error("The Speculos API port or/and transport port couldn't be reserved"); | ||
this.log("[ZEMU] ".concat(e)); | ||
// noinspection ExceptionCaughtLocallyJS | ||
throw e; | ||
} | ||
this.log("Starting Container"); | ||
return [4 /*yield*/, this.emuContainer.runContainer(__assign(__assign({}, this.startOptions), { transportPort: this.transportPort.toString(), speculosApiPort: this.speculosApiPort.toString() }))]; | ||
case 3: | ||
_c.sent(); | ||
this.log("Connecting to container"); | ||
// eslint-disable-next-liwaine func-names | ||
return [4 /*yield*/, this.connect()["catch"](function (error) { | ||
_this.log("".concat(error)); | ||
_this.close(); | ||
throw error; | ||
}) | ||
// Captures main screen | ||
]; | ||
case 4: | ||
// eslint-disable-next-liwaine func-names | ||
_c.sent(); | ||
// Captures main screen | ||
this.log("Wait for start text"); | ||
return [4 /*yield*/, this.waitForText(this.startOptions.startText, this.startOptions.startTimeout, this.startOptions.caseSensitive)]; | ||
case 5: | ||
_c.sent(); | ||
this.log("Get initial snapshot"); | ||
_b = this; | ||
return [4 /*yield*/, this.snapshot()]; | ||
case 6: | ||
_b.mainMenuSnapshot = _c.sent(); | ||
return [3 /*break*/, 8]; | ||
case 7: | ||
e_1 = _c.sent(); | ||
this.log("[ZEMU] ".concat(e_1)); | ||
throw e_1; | ||
case 8: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.connect = function () { | ||
var _a, _b; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var transportUrl, start, connected, maxWait, currentTime, elapsed, _c, e_2; | ||
return __generator(this, function (_d) { | ||
switch (_d.label) { | ||
case 0: | ||
transportUrl = "".concat(this.transportProtocol, "://").concat(this.host, ":").concat(this.transportPort); | ||
start = new Date(); | ||
connected = false; | ||
maxWait = (_b = (_a = this.startOptions) === null || _a === void 0 ? void 0 : _a.startDelay) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_START_DELAY; | ||
_d.label = 1; | ||
case 1: | ||
if (!!connected) return [3 /*break*/, 6]; | ||
currentTime = new Date(); | ||
elapsed = currentTime.getTime() - start.getTime(); | ||
if (elapsed > maxWait) { | ||
throw "Timeout waiting to connect"; | ||
} | ||
Zemu.delay(); | ||
_d.label = 2; | ||
case 2: | ||
_d.trys.push([2, 4, , 5]); | ||
// here we should be able to import directly HttpTransport, instead of that Ledger | ||
// offers a wrapper that returns a `StaticTransport` instance | ||
// we need to expect the error to avoid typing errors | ||
// @ts-expect-error | ||
_c = this; | ||
return [4 /*yield*/, (0, hw_transport_http_1["default"])(transportUrl).open(transportUrl)]; | ||
case 3: | ||
// here we should be able to import directly HttpTransport, instead of that Ledger | ||
// offers a wrapper that returns a `StaticTransport` instance | ||
// we need to expect the error to avoid typing errors | ||
// @ts-expect-error | ||
_c.transport = _d.sent(); | ||
connected = true; | ||
return [3 /*break*/, 5]; | ||
case 4: | ||
e_2 = _d.sent(); | ||
this.log("WAIT ".concat(this.containerName, " ").concat(elapsed, " - ").concat(e_2, " ").concat(transportUrl)); | ||
connected = false; | ||
return [3 /*break*/, 5]; | ||
case 5: return [3 /*break*/, 1]; | ||
case 6: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.log = function (message) { | ||
var _a, _b; | ||
if ((_b = (_a = this.startOptions) === null || _a === void 0 ? void 0 : _a.logging) !== null && _b !== void 0 ? _b : false) { | ||
var currentTimestamp = new Date().toISOString().slice(11, 23); | ||
process.stdout.write("[ZEMU] ".concat(currentTimestamp, ": ").concat(message, "\n")); | ||
} | ||
}; | ||
Zemu.prototype.startGRPCServer = function (ip, port, options) { | ||
if (options === void 0) { options = {}; } | ||
this.grpcManager = new grpc_1["default"](ip, port, options, this.transport); | ||
this.grpcManager.startServer(); | ||
}; | ||
Zemu.prototype.stopGRPCServer = function () { | ||
if (this.grpcManager) { | ||
this.grpcManager.stopServer(); | ||
} | ||
}; | ||
Zemu.prototype.close = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
this.log('Close'); | ||
return [4 /*yield*/, this.emuContainer.stop()]; | ||
case 1: | ||
_a.sent(); | ||
this.stopGRPCServer(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.getTransport = function () { | ||
if (!this.transport) | ||
throw new Error('Transport is not loaded.'); | ||
return this.transport; | ||
}; | ||
Zemu.prototype.getWindowRect = function () { | ||
var _a, _b, _c, _d; | ||
switch ((_b = (_a = this.startOptions) === null || _a === void 0 ? void 0 : _a.model) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_MODEL) { | ||
case 'nanos': | ||
return constants_1.WINDOW_S; | ||
case 'nanox': | ||
case 'nanosp': | ||
return constants_1.WINDOW_X; | ||
} | ||
throw "model ".concat((_d = (_c = this.startOptions) === null || _c === void 0 ? void 0 : _c.model) !== null && _d !== void 0 ? _d : constants_1.DEFAULT_MODEL, " not recognized"); | ||
}; | ||
Zemu.prototype.fetchSnapshot = function (url) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
// Exponential back-off retry delay between requests | ||
(0, axios_retry_1["default"])(axios_1["default"], { retryDelay: axios_retry_1["default"].exponentialDelay }); | ||
return [2 /*return*/, (0, axios_1["default"])({ | ||
method: 'GET', | ||
url: url, | ||
responseType: 'arraybuffer' | ||
})]; | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.saveSnapshot = function (arrayBuffer, filePath) { | ||
fs_extra_1["default"].writeFileSync(filePath, Buffer.from(arrayBuffer), 'binary'); | ||
}; | ||
Zemu.prototype.convertBufferToPNG = function (arrayBuffer) { | ||
return pngjs_1["default"].PNG.sync.read(Buffer.from(arrayBuffer)); | ||
}; | ||
Zemu.prototype.snapshot = function (filename) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var snapshotUrl, data, modelWindow, rect; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
snapshotUrl = 'http://localhost:' + ((_a = this.speculosApiPort) === null || _a === void 0 ? void 0 : _a.toString()) + '/screenshot'; | ||
return [4 /*yield*/, this.fetchSnapshot(snapshotUrl)]; | ||
case 1: | ||
data = (_b.sent()).data; | ||
modelWindow = this.getWindowRect(); | ||
if (filename) | ||
this.saveSnapshot(data, filename); | ||
rect = { | ||
height: modelWindow.height, | ||
width: modelWindow.width, | ||
data: data | ||
}; | ||
return [2 /*return*/, rect]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.getMainMenuSnapshot = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
return [2 /*return*/, this.mainMenuSnapshot]; | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.waitUntilScreenIsNot = function (screen, timeout) { | ||
if (timeout === void 0) { timeout = 60000; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var start, inputSnapshotBufferHex, currentSnapshotBufferHex, currentTime, elapsed; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
start = new Date(); | ||
return [4 /*yield*/, screen]; | ||
case 1: | ||
inputSnapshotBufferHex = (_a.sent()).data; | ||
currentSnapshotBufferHex = inputSnapshotBufferHex; | ||
this.log("Wait for screen change"); | ||
_a.label = 2; | ||
case 2: | ||
if (!inputSnapshotBufferHex.equals(currentSnapshotBufferHex)) return [3 /*break*/, 4]; | ||
currentTime = new Date(); | ||
elapsed = currentTime.getTime() - start.getTime(); | ||
if (elapsed > timeout) { | ||
throw "Timeout waiting for screen to change (".concat(timeout, " ms)"); | ||
} | ||
Zemu.delay(); | ||
this.log("Check [".concat(elapsed, "ms]")); | ||
return [4 /*yield*/, this.snapshot()]; | ||
case 3: | ||
currentSnapshotBufferHex = (_a.sent()).data; | ||
return [3 /*break*/, 2]; | ||
case 4: | ||
this.log("Screen changed"); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.formatIndexString = function (i) { | ||
return "".concat(i).padStart(5, '0'); | ||
}; | ||
Zemu.prototype.getSnapshotPath = function (snapshotPrefix, index, takeSnapshots) { | ||
return takeSnapshots ? "".concat(snapshotPrefix, "/").concat(this.formatIndexString(index), ".png") : undefined; | ||
}; | ||
Zemu.prototype.navigate = function (path, testcaseName, clickSchedule, waitForScreenUpdate, takeSnapshots, startImgIndex) { | ||
if (waitForScreenUpdate === void 0) { waitForScreenUpdate = true; } | ||
if (takeSnapshots === void 0) { takeSnapshots = true; } | ||
if (startImgIndex === void 0) { startImgIndex = 0; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var snapshotPrefixGolden, snapshotPrefixTmp, imageIndex, filename, _i, clickSchedule_1, value, j; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
snapshotPrefixGolden = (0, path_1.resolve)("".concat(path, "/snapshots/").concat(testcaseName)); | ||
snapshotPrefixTmp = (0, path_1.resolve)("".concat(path, "/snapshots-tmp/").concat(testcaseName)); | ||
if (takeSnapshots) { | ||
fs_extra_1["default"].ensureDirSync(snapshotPrefixGolden); | ||
fs_extra_1["default"].ensureDirSync(snapshotPrefixTmp); | ||
} | ||
imageIndex = startImgIndex; | ||
filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots); | ||
this.log("---------------------------"); | ||
this.log("Start ".concat(filename)); | ||
return [4 /*yield*/, this.snapshot(filename)]; | ||
case 1: | ||
_a.sent(); | ||
this.log("Instructions ".concat(clickSchedule)); | ||
_i = 0, clickSchedule_1 = clickSchedule; | ||
_a.label = 2; | ||
case 2: | ||
if (!(_i < clickSchedule_1.length)) return [3 /*break*/, 11]; | ||
value = clickSchedule_1[_i]; | ||
if (!(value == 0)) return [3 /*break*/, 4]; | ||
imageIndex += 1; | ||
filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots); | ||
return [4 /*yield*/, this.clickBoth(filename, waitForScreenUpdate)]; | ||
case 3: | ||
_a.sent(); | ||
return [3 /*break*/, 10]; | ||
case 4: | ||
j = 0; | ||
_a.label = 5; | ||
case 5: | ||
if (!(j < Math.abs(value))) return [3 /*break*/, 10]; | ||
imageIndex += 1; | ||
filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots); | ||
if (!(value < 0)) return [3 /*break*/, 7]; | ||
return [4 /*yield*/, this.clickLeft(filename, waitForScreenUpdate)]; | ||
case 6: | ||
_a.sent(); | ||
return [3 /*break*/, 9]; | ||
case 7: return [4 /*yield*/, this.clickRight(filename, waitForScreenUpdate)]; | ||
case 8: | ||
_a.sent(); | ||
_a.label = 9; | ||
case 9: | ||
j += 1; | ||
return [3 /*break*/, 5]; | ||
case 10: | ||
_i++; | ||
return [3 /*break*/, 2]; | ||
case 11: return [4 /*yield*/, this.dumpEvents()]; | ||
case 12: | ||
_a.sent(); | ||
return [2 /*return*/, imageIndex]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.takeSnapshotAndOverwrite = function (path, testcaseName, imageIndex) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var snapshotPrefixTmp, filename; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
snapshotPrefixTmp = (0, path_1.resolve)("".concat(path, "/snapshots-tmp/").concat(testcaseName)); | ||
fs_extra_1["default"].ensureDirSync(snapshotPrefixTmp); | ||
filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, true); | ||
try { | ||
if (typeof filename === 'undefined') | ||
throw Error; | ||
fs_extra_1["default"].unlinkSync(filename); | ||
} | ||
catch (err) { | ||
console.log(err); | ||
throw new Error('Snapshot does not exist'); | ||
} | ||
return [4 /*yield*/, this.snapshot(filename)]; | ||
case 1: | ||
_a.sent(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.navigateAndCompareSnapshots = function (path, testcaseName, clickSchedule, waitForScreenUpdate, startImgIndex) { | ||
if (waitForScreenUpdate === void 0) { waitForScreenUpdate = true; } | ||
if (startImgIndex === void 0) { startImgIndex = 0; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var takeSnapshots, lastImgIndex; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
takeSnapshots = true; | ||
return [4 /*yield*/, this.navigate(path, testcaseName, clickSchedule, waitForScreenUpdate, takeSnapshots, startImgIndex)]; | ||
case 1: | ||
lastImgIndex = _a.sent(); | ||
return [2 /*return*/, this.compareSnapshots(path, testcaseName, lastImgIndex)]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.compareSnapshots = function (path, testcaseName, snapshotCount) { | ||
var snapshotPrefixGolden = (0, path_1.resolve)("".concat(path, "/snapshots/").concat(testcaseName)); | ||
var snapshotPrefixTmp = (0, path_1.resolve)("".concat(path, "/snapshots-tmp/").concat(testcaseName)); | ||
this.log("golden ".concat(snapshotPrefixGolden)); | ||
this.log("tmp ".concat(snapshotPrefixTmp)); | ||
//////////////////// | ||
this.log("Start comparison"); | ||
for (var j = 0; j < snapshotCount + 1; j += 1) { | ||
this.log("Checked ".concat(snapshotPrefixTmp, "/").concat(this.formatIndexString(j), ".png")); | ||
var img1 = Zemu.LoadPng2RGB("".concat(snapshotPrefixTmp, "/").concat(this.formatIndexString(j), ".png")); | ||
var img2 = Zemu.LoadPng2RGB("".concat(snapshotPrefixGolden, "/").concat(this.formatIndexString(j), ".png")); | ||
if (!img1.data.equals(img2.data)) { | ||
throw new Error("Image [".concat(this.formatIndexString(j), "] do not match!")); | ||
} | ||
} | ||
return true; | ||
}; | ||
/** | ||
* @deprecated The method will be deprecated soon. Try to use navigateAndCompareSnapshots instead | ||
*/ | ||
Zemu.prototype.compareSnapshotsAndAccept = function (path, testcaseName, snapshotCount, backClickCount) { | ||
if (backClickCount === void 0) { backClickCount = 0; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var instructions; | ||
return __generator(this, function (_a) { | ||
instructions = []; | ||
if (snapshotCount > 0) | ||
instructions.push(snapshotCount); | ||
if (backClickCount > 0) { | ||
instructions.push(-backClickCount); | ||
instructions.push(backClickCount); | ||
} | ||
instructions.push(0); | ||
return [2 /*return*/, this.navigateAndCompareSnapshots(path, testcaseName, instructions)]; | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.compareSnapshotsAndApprove = function (path, testcaseName, waitForScreenUpdate, startImgIndex, timeout) { | ||
if (waitForScreenUpdate === void 0) { waitForScreenUpdate = true; } | ||
if (startImgIndex === void 0) { startImgIndex = 0; } | ||
if (timeout === void 0) { timeout = 30000; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
return [2 /*return*/, this.navigateAndCompareUntilText(path, testcaseName, 'APPROVE', waitForScreenUpdate, startImgIndex, timeout)]; | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.navigateUntilText = function (path, testcaseName, text, waitForScreenUpdate, takeSnapshots, startImgIndex, timeout) { | ||
if (waitForScreenUpdate === void 0) { waitForScreenUpdate = true; } | ||
if (takeSnapshots === void 0) { takeSnapshots = true; } | ||
if (startImgIndex === void 0) { startImgIndex = 0; } | ||
if (timeout === void 0) { timeout = 30000; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var snapshotPrefixGolden, snapshotPrefixTmp, imageIndex, filename, start, found, currentTime, elapsed, events; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
snapshotPrefixGolden = (0, path_1.resolve)("".concat(path, "/snapshots/").concat(testcaseName)); | ||
snapshotPrefixTmp = (0, path_1.resolve)("".concat(path, "/snapshots-tmp/").concat(testcaseName)); | ||
if (takeSnapshots) { | ||
fs_extra_1["default"].ensureDirSync(snapshotPrefixGolden); | ||
fs_extra_1["default"].ensureDirSync(snapshotPrefixTmp); | ||
} | ||
imageIndex = startImgIndex; | ||
filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots); | ||
return [4 /*yield*/, this.snapshot(filename)]; | ||
case 1: | ||
_a.sent(); | ||
start = new Date(); | ||
found = false; | ||
_a.label = 2; | ||
case 2: | ||
if (!!found) return [3 /*break*/, 8]; | ||
currentTime = new Date(); | ||
elapsed = currentTime.getTime() - start.getTime(); | ||
if (elapsed > timeout) { | ||
throw "Timeout waiting for screen containing ".concat(text); | ||
} | ||
return [4 /*yield*/, this.getEvents()]; | ||
case 3: | ||
events = _a.sent(); | ||
imageIndex += 1; | ||
filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots); | ||
found = events.some(function (event) { return event.text.includes(text); }); | ||
if (!found) return [3 /*break*/, 5]; | ||
return [4 /*yield*/, this.clickBoth(filename, waitForScreenUpdate)]; | ||
case 4: | ||
_a.sent(); | ||
return [3 /*break*/, 7]; | ||
case 5: | ||
// navigate to next screen | ||
return [4 /*yield*/, this.clickRight(filename, waitForScreenUpdate)]; | ||
case 6: | ||
// navigate to next screen | ||
_a.sent(); | ||
start = new Date(); | ||
_a.label = 7; | ||
case 7: return [3 /*break*/, 2]; | ||
case 8: return [2 /*return*/, imageIndex]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.navigateAndCompareUntilText = function (path, testcaseName, text, waitForScreenUpdate, startImgIndex, timeout) { | ||
if (waitForScreenUpdate === void 0) { waitForScreenUpdate = true; } | ||
if (startImgIndex === void 0) { startImgIndex = 0; } | ||
if (timeout === void 0) { timeout = 30000; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var takeSnapshots, lastImgIndex; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
takeSnapshots = true; | ||
return [4 /*yield*/, this.navigateUntilText(path, testcaseName, text, waitForScreenUpdate, takeSnapshots, startImgIndex, timeout)]; | ||
case 1: | ||
lastImgIndex = _a.sent(); | ||
return [2 /*return*/, this.compareSnapshots(path, testcaseName, lastImgIndex)]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.getEvents = function () { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var eventsUrl, data, error_1; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
(0, axios_retry_1["default"])(axios_1["default"], { retryDelay: axios_retry_1["default"].exponentialDelay }); | ||
eventsUrl = 'http://localhost:' + ((_a = this.speculosApiPort) === null || _a === void 0 ? void 0 : _a.toString()) + '/events'; | ||
_b.label = 1; | ||
case 1: | ||
_b.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, axios_1["default"].get(eventsUrl)]; | ||
case 2: | ||
data = (_b.sent()).data; | ||
return [2 /*return*/, data['events']]; | ||
case 3: | ||
error_1 = _b.sent(); | ||
return [2 /*return*/, []]; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.deleteEvents = function () { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: return [4 /*yield*/, (0, axios_1["default"])({ | ||
method: 'DELETE', | ||
url: 'http://localhost:' + ((_a = this.speculosApiPort) === null || _a === void 0 ? void 0 : _a.toString()) + '/events' | ||
})]; | ||
case 1: | ||
_b.sent(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.dumpEvents = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var events; | ||
var _this = this; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, this.getEvents()]; | ||
case 1: | ||
events = _a.sent(); | ||
if (events) { | ||
events.forEach(function (x) { return _this.log("[ZEMU] ".concat(JSON.stringify(x))); }); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.waitScreenChange = function (timeout) { | ||
if (timeout === void 0) { timeout = 30000; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var start, prev_events_qty, current_events_qty, currentTime, elapsed; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
start = new Date(); | ||
return [4 /*yield*/, this.getEvents()]; | ||
case 1: | ||
prev_events_qty = (_a.sent()).length; | ||
current_events_qty = prev_events_qty; | ||
this.log("Wait for screen change"); | ||
_a.label = 2; | ||
case 2: | ||
if (!(prev_events_qty === current_events_qty)) return [3 /*break*/, 4]; | ||
currentTime = new Date(); | ||
elapsed = currentTime.getTime() - start.getTime(); | ||
if (elapsed > timeout) { | ||
throw "Timeout waiting for screen to change (".concat(timeout, " ms)"); | ||
} | ||
Zemu.delay(); | ||
this.log("Check [".concat(elapsed, "ms]")); | ||
return [4 /*yield*/, this.getEvents()]; | ||
case 3: | ||
current_events_qty = (_a.sent()).length; | ||
return [3 /*break*/, 2]; | ||
case 4: | ||
this.log("Screen changed"); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.waitForText = function (text, timeout, caseSensitive) { | ||
if (timeout === void 0) { timeout = 60000; } | ||
if (caseSensitive === void 0) { caseSensitive = false; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var start, found, flags, startRegex, currentTime, elapsed, events; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
start = new Date(); | ||
found = false; | ||
flags = !caseSensitive ? 'i' : ''; | ||
startRegex = new RegExp(text, flags); | ||
_a.label = 1; | ||
case 1: | ||
if (!!found) return [3 /*break*/, 3]; | ||
currentTime = new Date(); | ||
elapsed = currentTime.getTime() - start.getTime(); | ||
if (elapsed > timeout) { | ||
throw "Timeout (".concat(timeout, ") waiting for text (").concat(text, ")"); | ||
} | ||
return [4 /*yield*/, this.getEvents()]; | ||
case 2: | ||
events = _a.sent(); | ||
found = events.some(function (event) { return startRegex.test(event.text); }); | ||
Zemu.delay(); | ||
return [3 /*break*/, 1]; | ||
case 3: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.click = function (endpoint, filename, waitForScreenUpdate) { | ||
var _a; | ||
return __awaiter(this, void 0, void 0, function () { | ||
var previousScreen, bothClickUrl, payload; | ||
return __generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
if (!waitForScreenUpdate) return [3 /*break*/, 2]; | ||
return [4 /*yield*/, this.snapshot()]; | ||
case 1: | ||
previousScreen = _b.sent(); | ||
_b.label = 2; | ||
case 2: | ||
bothClickUrl = 'http://localhost:' + ((_a = this.speculosApiPort) === null || _a === void 0 ? void 0 : _a.toString()) + endpoint; | ||
payload = { action: 'press-and-release' }; | ||
return [4 /*yield*/, axios_1["default"].post(bothClickUrl, payload)]; | ||
case 3: | ||
_b.sent(); | ||
this.log("Click ".concat(endpoint, " -> ").concat(filename)); | ||
if (!waitForScreenUpdate) return [3 /*break*/, 5]; | ||
return [4 /*yield*/, this.waitUntilScreenIsNot(previousScreen)]; | ||
case 4: | ||
_b.sent(); | ||
return [3 /*break*/, 6]; | ||
case 5: | ||
Zemu.delay(); // A minimum delay is required | ||
_b.label = 6; | ||
case 6: // A minimum delay is required | ||
return [2 /*return*/, this.snapshot(filename)]; | ||
} | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.clickLeft = function (filename, waitForScreenUpdate) { | ||
if (waitForScreenUpdate === void 0) { waitForScreenUpdate = true; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
return [2 /*return*/, this.click('/button/left', filename, waitForScreenUpdate)]; | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.clickRight = function (filename, waitForScreenUpdate) { | ||
if (waitForScreenUpdate === void 0) { waitForScreenUpdate = true; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
return [2 /*return*/, this.click('/button/right', filename, waitForScreenUpdate)]; | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.clickBoth = function (filename, waitForScreenUpdate) { | ||
if (waitForScreenUpdate === void 0) { waitForScreenUpdate = true; } | ||
return __awaiter(this, void 0, void 0, function () { | ||
return __generator(this, function (_a) { | ||
return [2 /*return*/, this.click('/button/both', filename, waitForScreenUpdate)]; | ||
}); | ||
}); | ||
}; | ||
Zemu.prototype.assignPortsToListen = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var transportPort, speculosApiPort; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!(!this.transportPort || !this.speculosApiPort)) return [3 /*break*/, 3]; | ||
return [4 /*yield*/, (0, get_port_1["default"])({ port: this.desiredTransportPort })]; | ||
case 1: | ||
transportPort = _a.sent(); | ||
return [4 /*yield*/, (0, get_port_1["default"])({ port: this.desiredSpeculosApiPort })]; | ||
case 2: | ||
speculosApiPort = _a.sent(); | ||
this.transportPort = transportPort; | ||
this.speculosApiPort = speculosApiPort; | ||
_a.label = 3; | ||
case 3: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
return Zemu; | ||
}()); | ||
exports["default"] = Zemu; | ||
Object.defineProperty(exports, "DEFAULT_START_OPTIONS", { enumerable: true, get: function () { return constants_1.DEFAULT_START_OPTIONS; } }); | ||
var zondax_1 = require("./zondax"); | ||
Object.defineProperty(exports, "zondaxMainmenuNavigation", { enumerable: true, get: function () { return zondax_1.zondaxMainmenuNavigation; } }); |
@@ -8,19 +8,19 @@ # Quickstart | ||
```typescript | ||
import Zemu from '@zondax/zemu' | ||
import Zemu from "@zondax/zemu"; | ||
``` | ||
Zemu comes with reasonable defaults. Even though, you might be interested on changing a bit the options of Zemu. We'll explain later how to | ||
apply this options. These are the defaults: | ||
Zemu comes with reasonable defaults. Even though, you might be interested on changing a bit the options of Zemu. We'll | ||
explain later how to apply this options. These are the defaults: | ||
```typescript | ||
const DEFAULT_START_OPTIONS = { | ||
model: 'nanos', // this can be nanos, nanox and nanosp | ||
sdk: '', // version of the ledger-secure-sdk to use | ||
model: "nanos", // this can be nanos, nanox and nanosp | ||
sdk: "", // version of the ledger-secure-sdk to use | ||
logging: false, // some nice logs | ||
custom: '', // other options passed directly to speculos, the emulator | ||
custom: "", // other options passed directly to speculos, the emulator | ||
startDelay: 20000, // wait time before timeout before connection | ||
startText: 'Ready', // text to search at the first screen | ||
startText: "Ready", // text to search at the first screen | ||
caseSensitive: false, // for every text search in the emulator | ||
startTimeout: 20000, // wait time to have startText after connecting | ||
} | ||
}; | ||
``` | ||
@@ -31,3 +31,3 @@ | ||
```typescript | ||
import Zemu, { DEFAULT_START_OPTIONS } from '@zondax/zemu' | ||
import Zemu, { DEFAULT_START_OPTIONS } from "@zondax/zemu"; | ||
``` | ||
@@ -38,7 +38,7 @@ | ||
```typescript | ||
const APP_SEED = 'equip will roof matter pink blind book anxiety banner elbow sun young' // that's just an example one | ||
const APP_SEED = "equip will roof matter pink blind book anxiety banner elbow sun young"; // that's just an example one | ||
const options = { | ||
...DEFAULT_START_OPTIONS, | ||
custom: `-s "${APP_SEED}"`, | ||
} | ||
}; | ||
``` | ||
@@ -48,5 +48,5 @@ | ||
A transport-based communication interface will be also needed to run Zemu tests, This module is usually developed in JS/TS and it’s custom | ||
made for every application. Additionally you can use this package to integrate your web wallet with Ledger devices. Let’s suppose that it’s | ||
called `DemoApp`. | ||
A transport-based communication interface will be also needed to run Zemu tests, This module is usually developed in | ||
JS/TS and it’s custom made for every application. Additionally you can use this package to integrate your web wallet | ||
with Ledger devices. Let’s suppose that it’s called `DemoApp`. | ||
@@ -58,3 +58,3 @@ After importing and deciding our options for Zemu, we need to start the app. | ||
```typescript | ||
const sim = new Zemu('path/to/your/compiled/app.elf') | ||
const sim = new Zemu("path/to/your/compiled/app.elf"); | ||
``` | ||
@@ -65,9 +65,9 @@ | ||
```typescript | ||
const APP_SEED = 'equip will roof matter pink blind book anxiety banner elbow sun young' // that's just an example one | ||
const APP_SEED = "equip will roof matter pink blind book anxiety banner elbow sun young"; // that's just an example one | ||
const options = { | ||
...DEFAULT_START_OPTIONS, | ||
custom: `-s "${APP_SEED}"`, | ||
} | ||
}; | ||
await sim.start(options) // it returns a promise! | ||
await sim.start(options); // it returns a promise! | ||
``` | ||
@@ -78,3 +78,3 @@ | ||
```typescript | ||
await sim.close() // it also returns a Promise! | ||
await sim.close(); // it also returns a Promise! | ||
``` | ||
@@ -89,6 +89,6 @@ | ||
```typescript | ||
import Zemu, { DEFAULT_START_OPTIONS } from '@zondax/zemu' | ||
import Zemu, { DEFAULT_START_OPTIONS } from "@zondax/zemu"; | ||
const sim = new Zemu('path/to/elf/image') | ||
await sim.start(DEFAULT_START_OPTIONS) | ||
const sim = new Zemu("path/to/elf/image"); | ||
await sim.start(DEFAULT_START_OPTIONS); | ||
``` | ||
@@ -99,5 +99,5 @@ | ||
```typescript | ||
await sim.clickLeft() | ||
await sim.clickRight() | ||
await sim.clickBoth() | ||
await sim.clickLeft(); | ||
await sim.clickRight(); | ||
await sim.clickBoth(); | ||
``` | ||
@@ -126,4 +126,4 @@ | ||
```typescript | ||
import Zemu, { DEFAULT_START_OPTIONS } from '@zondax/zemu' | ||
const APP_SEED = 'equip will roof matter pink blind book anxiety banner elbow sun young' // our recurrent example | ||
import Zemu, { DEFAULT_START_OPTIONS } from "@zondax/zemu"; | ||
const APP_SEED = "equip will roof matter pink blind book anxiety banner elbow sun young"; // our recurrent example | ||
@@ -133,17 +133,17 @@ const customOptions = { | ||
custom: `-s "${APP_SEED}"`, | ||
} | ||
}; | ||
test('example', async () => { | ||
const sim = new Zemu('path/to/your/elf/file.elf') | ||
test("example", async () => { | ||
const sim = new Zemu("path/to/your/elf/file.elf"); | ||
try { | ||
// create an instance of your ledger-js app | ||
const demoApp = new DemoApp(sim.getTransport()) | ||
const demoApp = new DemoApp(sim.getTransport()); | ||
// start your simulator | ||
await sim.start(customOptions) | ||
await sim.start(customOptions); | ||
// your testing goes here, as you would do in your wallet | ||
} finally { | ||
// this will close and remove the container | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); | ||
``` |
@@ -5,11 +5,14 @@ # Advanced moves | ||
Navigate command requires an array of numbers as input. This numbers will represent right click (positive), left click (negative) or both | ||
click (zero). For left and right clicks, you can specify how many times you wish to click changing the value. For example, if I want to | ||
click 3 times right, then two times both clicks, and then 4 times left, my array would be `[3, 0, 0, -4]`. | ||
Navigate command requires an array of numbers as input. This numbers will represent right click (positive), left click | ||
(negative) or both click (zero). For left and right clicks, you can specify how many times you wish to click changing | ||
the value. For example, if I want to click 3 times right, then two times both clicks, and then 4 times left, my array | ||
would be `[3, 0, 0, -4]`. | ||
Then, we have several functions implementing this idea, and also some helpers to automatize it and avoid passing the whole array of clicks | ||
Then, we have several functions implementing this idea, and also some helpers to automatize it and avoid passing the | ||
whole array of clicks | ||
### Navigate | ||
This function navigates using the movement array defined and takes snapshots in the road. We have 5 parameters in this function: | ||
This function navigates using the movement array defined and takes snapshots in the road. We have 5 parameters in this | ||
function: | ||
@@ -32,3 +35,3 @@ - `path: string` | ||
```typescript | ||
await sim.navigate('.', 'my-first-test', [2, 0, -1, 0, 3]) // we are using the defualt for the other params | ||
await sim.navigate(".", "my-first-test", [2, 0, -1, 0, 3]); // we are using the defualt for the other params | ||
``` | ||
@@ -40,4 +43,4 @@ | ||
This method has two parts. First one is just a regular `navigate` and, when finished, it compares the new snapshots (found in | ||
`<path>/snapshots-tmp`) with a reference version that you would have saved in `<path>/snapshots`. | ||
This method has two parts. First one is just a regular `navigate` and, when finished, it compares the new snapshots | ||
(found in `<path>/snapshots-tmp`) with a reference version that you would have saved in `<path>/snapshots`. | ||
@@ -49,3 +52,3 @@ It takes the sames params as the method before: | ||
```typescript | ||
await sim.navigateAndCompareSnapshots('.', 'my-first-test', [2, 0, -1, 0, 3]) // we are using the defualt for the other params | ||
await sim.navigateAndCompareSnapshots(".", "my-first-test", [2, 0, -1, 0, 3]); // we are using the defualt for the other params | ||
``` | ||
@@ -55,4 +58,5 @@ | ||
In this method, Zemu is going to click right in every screen until it reaches the target `<text>`, and then is going to double click and end | ||
there the workflow. It's useful for signing process in which we have a variable number of screens before reaching `APPROVE` or `REJECT`. | ||
In this method, Zemu is going to click right in every screen until it reaches the target `<text>`, and then is going to | ||
double click and end there the workflow. It's useful for signing process in which we have a variable number of screens | ||
before reaching `APPROVE` or `REJECT`. | ||
@@ -79,3 +83,3 @@ We have 7 avaliable params: | ||
```typescript | ||
await sim.navigateUntilText('.', 'my-first-test', 'REJECT') // we are using the defualt for the other params | ||
await sim.navigateUntilText(".", "my-first-test", "REJECT"); // we are using the defualt for the other params | ||
``` | ||
@@ -85,7 +89,7 @@ | ||
For each method of navigating, we have a version to also compare snapshots with a previous version of them. It's useful to check that | ||
nothing changed and the app have the expected result. | ||
For each method of navigating, we have a version to also compare snapshots with a previous version of them. It's useful | ||
to check that nothing changed and the app have the expected result. | ||
After navigating, it's going to compare every snapshot taken. As before, snapshots taken by the method are going to be saved in | ||
`<path>/snapshots-tmp`, and is going to compare that with snapshots found in `<path>/snapshots`. | ||
After navigating, it's going to compare every snapshot taken. As before, snapshots taken by the method are going to be | ||
saved in `<path>/snapshots-tmp`, and is going to compare that with snapshots found in `<path>/snapshots`. | ||
@@ -99,3 +103,3 @@ Every method takes the same params than the analogous in navigating ones. Let's go directly with examples. | ||
```typescript | ||
await sim.navigateAndCompareSnapshots('.', 'my-first-test', [2, 0, -1, 0, 3]) // we are using the defualt for the other params | ||
await sim.navigateAndCompareSnapshots(".", "my-first-test", [2, 0, -1, 0, 3]); // we are using the defualt for the other params | ||
``` | ||
@@ -108,10 +112,10 @@ | ||
```typescript | ||
await sim.navigateAndCompareUntilText('.', 'my-first-test', 'REJECT') // we are using the defualt for the other params | ||
await sim.navigateAndCompareUntilText(".", "my-first-test", "REJECT"); // we are using the defualt for the other params | ||
``` | ||
There's also a helper that works the same as `navigateAndCompareUntilText`, but using always the `APPROVE` text. It's useful to check a | ||
signing workflow of the app: | ||
There's also a helper that works the same as `navigateAndCompareUntilText`, but using always the `APPROVE` text. It's | ||
useful to check a signing workflow of the app: | ||
```typescript | ||
await sim.compareSnapshotsAndApprove('.', 'my-first-test') // we are using the defualt for the other params | ||
await sim.compareSnapshotsAndApprove(".", "my-first-test"); // we are using the defualt for the other params | ||
``` |
@@ -5,7 +5,8 @@ # Overview | ||
Integration and end-to-end testing of Ledger Apps is a manual and time consuming process. We believe that the Ledger apps ecosystem is | ||
lacking an adequate approach with respect to testing. The Zemu Framework is our solution for this problem. We stand on the shoulders of the | ||
giant [greenknot’s](https://github.com/greenknot) speculos. | ||
Integration and end-to-end testing of Ledger Apps is a manual and time consuming process. We believe that the Ledger | ||
apps ecosystem is lacking an adequate approach with respect to testing. The Zemu Framework is our solution for this | ||
problem. We stand on the shoulders of the giant [greenknot’s](https://github.com/greenknot) speculos. | ||
It's currently being used in every Ledger App built by Zondax, among many others (such as Ethereum one built by Ledger team). | ||
It's currently being used in every Ledger App built by Zondax, among many others (such as Ethereum one built by Ledger | ||
team). | ||
@@ -12,0 +13,0 @@ _Zemu is an emulation and testing framework for Ledger Nano S/S+/X devices_ |
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
transformIgnorePatterns: ['^.+\\.js$'], | ||
testPathIgnorePatterns: ['<rootDir>/dist'], | ||
globalSetup: './tests/globalsetup.ts', | ||
globalTeardown: './tests/globalteardown.ts', | ||
} | ||
preset: "ts-jest", | ||
testEnvironment: "node", | ||
transformIgnorePatterns: ["^.+\\.js$"], | ||
testPathIgnorePatterns: ["<rootDir>/dist"], | ||
globalSetup: "./tests/globalsetup.ts", | ||
}; |
@@ -5,3 +5,3 @@ { | ||
"license": "Apache-2.0", | ||
"version": "0.34.0", | ||
"version": "0.35.0-beta.1", | ||
"description": "Zemu Testing Framework", | ||
@@ -23,8 +23,9 @@ "main": "./dist/index.js", | ||
"scripts": { | ||
"build": "tsc && yarn copy-files", | ||
"build": "yarn rimraf dist && tsc && yarn copy-files", | ||
"copy-files": "copyfiles -u 0 src/**/*.proto dist/", | ||
"test": "yarn ts-node tests/pullImageKillOld.ts && yarn build && jest", | ||
"linter": "eslint --ext .ts,.tsx,.js,.jsx --ignore-path .eslintignore . --max-warnings 0", | ||
"test:clean": "yarn ts-node tests/pullImageKillOld.ts", | ||
"test": "yarn test:clean && yarn build && jest", | ||
"linter": "eslint --max-warnings 0 .", | ||
"linter:fix": "yarn linter --fix", | ||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"" | ||
"format": "prettier --write ." | ||
}, | ||
@@ -35,13 +36,12 @@ "bugs": { | ||
"dependencies": { | ||
"@grpc/grpc-js": "^1.7.2", | ||
"@grpc/proto-loader": "^0.7.3", | ||
"@ledgerhq/hw-transport": "^6.27.6", | ||
"@ledgerhq/hw-transport-http": "^6.27.6", | ||
"axios": "^1.1.3", | ||
"axios-retry": "^3.2.0", | ||
"@grpc/grpc-js": "^1.8.8", | ||
"@grpc/proto-loader": "^0.7.5", | ||
"@ledgerhq/hw-transport": "^6.28.0", | ||
"@ledgerhq/hw-transport-http": "^6.27.11", | ||
"axios": "^1.3.3", | ||
"axios-retry": "^3.4.0", | ||
"dockerode": "^3.3.1", | ||
"elfy": "^1.0.0", | ||
"fs-extra": "^10.0.0", | ||
"fs-extra": "^11.0.0", | ||
"get-port": "^5.1.1", | ||
"path": "^0.12.7", | ||
"pngjs": "^6.0.0", | ||
@@ -52,31 +52,24 @@ "randomstring": "^1.2.3" | ||
"@types/dockerode": "^3.3.11", | ||
"@types/fs-extra": "^9.0.12", | ||
"@types/jest": "^29.2.0", | ||
"@types/ledgerhq__hw-transport": "^4.21.4", | ||
"@types/fs-extra": "^11.0.1", | ||
"@types/jest": "^29.4.0", | ||
"@types/node": "^18.13.0", | ||
"@types/pngjs": "^6.0.1", | ||
"@types/randomstring": "^1.1.8", | ||
"@types/sleep": "^0.0.8", | ||
"@typescript-eslint/eslint-plugin": "^5.40.1", | ||
"@typescript-eslint/parser": "^5.40.1", | ||
"@zondax/ledger-substrate": "^0.39.0", | ||
"@typescript-eslint/eslint-plugin": "^5.52.0", | ||
"@typescript-eslint/parser": "^5.52.0", | ||
"@zondax/ledger-substrate": "^0.40.4", | ||
"copyfiles": "^2.4.1", | ||
"eslint": "^8.25.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-import": "^2.23.4", | ||
"eslint-plugin-jest": "^27.1.3", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"jest": "^29.2.1", | ||
"js-sha512": "^0.8.0", | ||
"prettier": "^2.5.1", | ||
"ts-jest": "^29.0.3", | ||
"eslint": "^8.34.0", | ||
"eslint-config-prettier": "^8.6.0", | ||
"eslint-config-standard-with-typescript": "^34.0.0", | ||
"eslint-plugin-import": "^2.27.5", | ||
"eslint-plugin-n": "^15.6.1", | ||
"eslint-plugin-promise": "^6.0.0", | ||
"jest": "^29.4.2", | ||
"prettier": "^2.8.4", | ||
"rimraf": "^4.1.2", | ||
"ts-jest": "^29.0.5", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^4.8.4" | ||
}, | ||
"moduleDirectories": [ | ||
"node_modules", | ||
"dist" | ||
], | ||
"publishConfig": { | ||
"access": "public" | ||
"typescript": "^4.9.5" | ||
} | ||
} |
# Zemu | ||
![zondax](docs/assets/zondax_light.png) | ||
![zondax_light](docs/assets/zondax_light.png#gh-light-mode-only) | ||
![zondax_dark](docs/assets/zondax_dark.png#gh-dark-mode-only) | ||
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) | ||
@@ -17,6 +19,6 @@ [![npm version](https://badge.fury.io/js/%40zondax%2Fzemu.svg)](https://badge.fury.io/js/%40zondax%2Fzemu) | ||
Integration and end-to-end testing of Ledger Apps is a manual and time consuming process. We believe that the Ledger apps ecosystem is | ||
lacking an adequate approach with respect to testing. The Zemu Framework is our solution for this problem. Under the hood, Zemu uses | ||
Ledger's project [speculos](https://github.com/ledgerHQ/speculos). It's currently being used in every Ledger App built by Zondax, among many | ||
others (such as Ethereum one built by Ledger team). | ||
Integration and end-to-end testing of Ledger Apps is a manual and time consuming process. We believe that the Ledger | ||
apps ecosystem is lacking an adequate approach with respect to testing. The Zemu Framework is our solution for this | ||
problem. Under the hood, Zemu uses Ledger's project [speculos](https://github.com/ledgerHQ/speculos). It's currently | ||
being used in every Ledger App built by Zondax, among many others (such as Ethereum one built by Ledger team). | ||
@@ -23,0 +25,0 @@ _Zemu is an emulation and testing framework for Ledger Nano S/S+/X devices._ |
@@ -1,12 +0,51 @@ | ||
export const DEFAULT_EMU_IMG = 'zondax/builder-zemu@sha256:7cae0f781ea6f6a58c39f273763bb61176b377bd0d6c713e59ae38e0531ae4ab' | ||
/** ****************************************************************************** | ||
* (c) 2018 - 2023 Zondax AG | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
******************************************************************************* */ | ||
import { ButtonKind, type IDeviceWindow, type IStartOptions } from "./types"; | ||
export const DEFAULT_MODEL = 'nanos' | ||
export const DEFAULT_START_TEXT = 'Ready' | ||
export const DEFAULT_START_DELAY = 20000 | ||
export const DEFAULT_KEY_DELAY = 100 | ||
export const DEFAULT_HOST = '127.0.0.1' | ||
export const BASE_NAME = 'zemu-test-' | ||
export const DEFAULT_START_TIMEOUT = 20000 | ||
export const KILL_TIMEOUT = 5000 | ||
export const DEFAULT_EMU_IMG = | ||
"zondax/builder-zemu@sha256:8d7b06cedf2d018b9464f4af4b7a8357c3fbb180f3ab153f8cb8f138defb22a4"; | ||
export const DEFAULT_MODEL = "nanos"; | ||
export const DEFAULT_START_TEXT = "Ready"; | ||
export const DEFAULT_START_DELAY = 20000; | ||
export const DEFAULT_KEY_DELAY = 100; | ||
export const DEFAULT_HOST = "127.0.0.1"; | ||
export const BASE_NAME = "zemu-test-"; | ||
export const DEFAULT_START_TIMEOUT = 20000; | ||
export const KILL_TIMEOUT = 5000; | ||
export const DEFAULT_METHOD_TIMEOUT = 10000; | ||
export const DEFAULT_NANO_APPROVE_KEYWORD = "APPROVE"; | ||
export const DEFAULT_NANO_REJECT_KEYWORD = "REJECT"; | ||
export const DEFAULT_STAX_APPROVE_KEYWORD = "APPROVE"; | ||
export const DEFAULT_STAX_REJECT_KEYWORD = "Cancel"; | ||
export const DEFAULT_START_OPTIONS: IStartOptions = { | ||
logging: false, | ||
startDelay: DEFAULT_START_DELAY, | ||
custom: "", | ||
model: DEFAULT_MODEL, | ||
sdk: "", | ||
startText: DEFAULT_START_TEXT, | ||
caseSensitive: false, | ||
startTimeout: DEFAULT_START_TIMEOUT, | ||
approveAction: ButtonKind.ApproveHoldButton, | ||
approveKeyword: "", | ||
rejectKeyword: "", | ||
}; | ||
export const KEYS = { | ||
@@ -17,5 +56,5 @@ NOT_PRESSED: 0, | ||
RIGHT: 0xff53, | ||
} | ||
}; | ||
export const WINDOW_S = { | ||
export const WINDOW_S: IDeviceWindow = { | ||
x: 0, | ||
@@ -25,5 +64,5 @@ y: 0, | ||
height: 32, | ||
} | ||
}; | ||
export const WINDOW_X = { | ||
export const WINDOW_X: IDeviceWindow = { | ||
x: 0, | ||
@@ -33,2 +72,9 @@ y: 0, | ||
height: 64, | ||
} | ||
}; | ||
export const WINDOW_STAX: IDeviceWindow = { | ||
x: 0, | ||
y: 0, | ||
width: 400, | ||
height: 672, | ||
}; |
/** ****************************************************************************** | ||
* (c) 2020 Zondax GmbH | ||
* (c) 2018 - 2023 Zondax AG | ||
* | ||
@@ -16,128 +16,117 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
******************************************************************************* */ | ||
import path from 'path' | ||
import Docker, { Container, ContainerInfo } from 'dockerode' | ||
import Docker, { type Container, type ContainerInfo } from "dockerode"; | ||
import path from "path"; | ||
export const DEV_CERT_PRIVATE_KEY = 'ff701d781f43ce106f72dc26a46b6a83e053b5d07bb3d4ceab79c91ca822a66b' | ||
export const BOLOS_SDK = '/project/deps/nanos-secure-sdk' | ||
export const DEFAULT_APP_PATH = '/project/app/bin' | ||
export const DEV_CERT_PRIVATE_KEY = "ff701d781f43ce106f72dc26a46b6a83e053b5d07bb3d4ceab79c91ca822a66b"; | ||
export const BOLOS_SDK = "/project/deps/nanos-secure-sdk"; | ||
export const DEFAULT_APP_PATH = "/project/app/bin"; | ||
export default class EmuContainer { | ||
private logging: boolean | ||
private readonly elfLocalPath: string | ||
private readonly name: string | ||
private readonly image: string | ||
private libElfs: { [p: string]: string } | ||
private currentContainer: Container | undefined | null | ||
private logging: boolean; | ||
private readonly elfLocalPath: string; | ||
private readonly name: string; | ||
private readonly image: string; | ||
private readonly libElfs: Record<string, string>; | ||
private currentContainer?: Container; | ||
constructor(elfLocalPath: string, libElfs: { [p: string]: string }, image: string, name: string) { | ||
// eslint-disable-next-line global-require | ||
this.image = image | ||
this.elfLocalPath = elfLocalPath | ||
this.libElfs = libElfs | ||
this.name = name | ||
this.logging = false | ||
constructor(elfLocalPath: string, libElfs: Record<string, string>, image: string, name: string) { | ||
this.image = image; | ||
this.elfLocalPath = elfLocalPath; | ||
this.libElfs = libElfs; | ||
this.name = name; | ||
this.logging = false; | ||
} | ||
static async killContainerByName(name: string) { | ||
const docker = new Docker() | ||
return new Promise<void>(resolve => { | ||
docker.listContainers({ all: true, filters: { name: [name] } }, (listError, containers: ContainerInfo[] | undefined) => { | ||
if (listError) throw listError | ||
if (!containers?.length) { | ||
console.log('No containers found') | ||
return | ||
} | ||
containers.forEach(containerInfo => { | ||
docker.getContainer(containerInfo.Id).remove({ force: true }, removeError => { | ||
if (removeError) throw removeError | ||
}) | ||
}) | ||
}) | ||
resolve() | ||
}) | ||
static killContainerByName(name: string): void { | ||
const docker = new Docker(); | ||
docker.listContainers({ all: true, filters: { name: [name] } }, (listError, containers?: ContainerInfo[]) => { | ||
if (listError != null) throw listError; | ||
if (containers == null || containers.length === 0) { | ||
console.log("No containers found"); | ||
return; | ||
} | ||
containers.forEach((containerInfo) => { | ||
docker.getContainer(containerInfo.Id).remove({ force: true }, (removeError) => { | ||
if (removeError != null) throw removeError; | ||
}); | ||
}); | ||
}); | ||
} | ||
static async checkAndPullImage(imageName: string) { | ||
const docker = new Docker() | ||
return docker.pull(imageName, (err: any, stream: any) => { | ||
function onProgress(event: any) { | ||
// eslint-disable-next-line no-prototype-builtins | ||
const progress = event.hasOwnProperty('progress') ? event.progress : '' | ||
// eslint-disable-next-line no-prototype-builtins | ||
const status = event.hasOwnProperty('status') ? event.status : '' | ||
process.stdout.write(`[DOCKER] ${status}: ${progress}\n`) | ||
static checkAndPullImage(imageName: string): void { | ||
const docker = new Docker(); | ||
docker.pull(imageName, {}, (err: any, stream: any) => { | ||
function onProgress(event: any): void { | ||
const progress = event?.progress ?? ""; | ||
const status = event?.status ?? ""; | ||
process.stdout.write(`[DOCKER] ${status}: ${progress}\n`); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
function onFinished(err: any, output: any) { | ||
if (err) { | ||
process.stdout.write(`[DOCKER] ${err}\n`) | ||
throw err | ||
function onFinished(err: any, _output: any): void { | ||
if (err != null) { | ||
process.stdout.write(`[DOCKER] ${err}\n`); | ||
throw err; | ||
} | ||
} | ||
if (err) { | ||
process.stdout.write(`[DOCKER] ${err}\n`) | ||
throw new Error(err) | ||
if (err != null) { | ||
process.stdout.write(`[DOCKER] ${err}\n`); | ||
throw new Error(err); | ||
} | ||
docker.modem.followProgress(stream, onFinished, onProgress) | ||
}) | ||
docker.modem.followProgress(stream, onFinished, onProgress); | ||
}); | ||
} | ||
log(message: string) { | ||
if (this.logging ?? false) { | ||
process.stdout.write(`${message}\n`) | ||
} | ||
log(message: string): void { | ||
if (this.logging ?? false) process.stdout.write(`${message}\n`); | ||
} | ||
async runContainer(options: { | ||
logging: boolean | ||
custom: string | ||
model: string | ||
sdk: string | ||
transportPort: string | ||
speculosApiPort: string | ||
}) { | ||
const docker = new Docker() | ||
logging: boolean; | ||
custom: string; | ||
model: string; | ||
sdk: string; | ||
transportPort: string; | ||
speculosApiPort: string; | ||
}): Promise<void> { | ||
const docker = new Docker(); | ||
this.logging = options.logging | ||
this.logging = options.logging; | ||
const appFilename = path.basename(this.elfLocalPath) | ||
const appDir = path.dirname(this.elfLocalPath) | ||
const appFilename = path.basename(this.elfLocalPath); | ||
const appDir = path.dirname(this.elfLocalPath); | ||
const dirBindings = [`${appDir}:${DEFAULT_APP_PATH}`] | ||
const dirBindings = [`${appDir}:${DEFAULT_APP_PATH}`]; | ||
let libArgs = '' | ||
let libArgs = ""; | ||
Object.entries(this.libElfs).forEach(([libName, libPath]) => { | ||
const libFilename = path.basename(libPath) | ||
libArgs += ` -l ${libName}:${DEFAULT_APP_PATH}/${libFilename}` | ||
}) | ||
const libFilename = path.basename(libPath); | ||
libArgs += ` -l ${libName}:${DEFAULT_APP_PATH}/${libFilename}`; | ||
}); | ||
const modelOptions = options?.model ? options.model : 'nanos' | ||
if (modelOptions === 'nanosp') options.sdk = '1.0.3' | ||
const modelOptions = options.model !== "" ? options.model : "nanos"; | ||
if (modelOptions === "nanosp" && options.sdk === "") options.sdk = "1.0.3"; | ||
const sdkOption = options?.sdk ? ` -k ${options.sdk} ` : '' | ||
if (sdkOption) this.log(`[ZEMU] Using SDK ${modelOptions} with version ${options.sdk}`) | ||
const sdkOption = options.sdk !== "" ? `-k ${options.sdk}` : ""; | ||
if (sdkOption !== "") this.log(`[ZEMU] Using SDK ${modelOptions} with version ${options.sdk}`); | ||
let customOptions = '' | ||
if (options.custom) { | ||
customOptions = options.custom | ||
} | ||
const customOptions = options.custom; | ||
const displaySetting = '--display headless' | ||
const command = `/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN ${displaySetting} ${customOptions} -m ${modelOptions} ${sdkOption} ${DEFAULT_APP_PATH}/${appFilename} ${libArgs}` | ||
const displaySetting = "--display headless"; | ||
const command = `/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN ${displaySetting} ${customOptions} -m ${modelOptions} ${sdkOption} ${DEFAULT_APP_PATH}/${appFilename} ${libArgs}`; | ||
this.log(`[ZEMU] Command: ${command}`) | ||
this.log(`[ZEMU] Command: ${command}`); | ||
const portBindings: { [index: string]: { HostPort: string }[] } = { | ||
const portBindings: Record<string, Array<{ HostPort: string }>> = { | ||
[`9998/tcp`]: [{ HostPort: options.transportPort }], | ||
[`5000/tcp`]: [{ HostPort: options.speculosApiPort }], | ||
} | ||
}; | ||
if (customOptions.indexOf('--debug') > -1) { | ||
portBindings[`1234/tcp`] = [{ HostPort: '1234' }] | ||
if (customOptions.includes("--debug")) { | ||
portBindings[`1234/tcp`] = [{ HostPort: "1234" }]; | ||
} | ||
const displayEnvironment: string = process.platform === 'darwin' ? 'host.docker.internal:0' : process.env.DISPLAY ?? '' | ||
const displayEnvironment: string = | ||
process.platform === "darwin" ? "host.docker.internal:0" : process.env.DISPLAY ?? ""; | ||
const environment = [ | ||
@@ -148,5 +137,5 @@ `SCP_PRIVKEY='${DEV_CERT_PRIVATE_KEY}'`, | ||
`DISPLAY='${displayEnvironment}'`, | ||
] | ||
]; | ||
this.log(`[ZEMU] Creating Container ${this.image} - ${this.name} `) | ||
this.log(`[ZEMU] Creating Container ${this.image} - ${this.name} `); | ||
this.currentContainer = await docker.createContainer({ | ||
@@ -158,3 +147,3 @@ Image: this.image, | ||
AttachStderr: true, | ||
User: '1000', | ||
User: "1000", | ||
Env: environment, | ||
@@ -166,39 +155,40 @@ HostConfig: { | ||
Cmd: [command], | ||
}) | ||
}); | ||
this.log(`[ZEMU] Connected ${this.currentContainer.id}`) | ||
this.log(`[ZEMU] Connected ${this.currentContainer.id}`); | ||
if (this.logging) { | ||
this.currentContainer.attach({ stream: true, stdout: true, stderr: true }, (err: any, stream: any) => { | ||
stream.pipe(process.stdout) | ||
}) | ||
this.log(`[ZEMU] Attached ${this.currentContainer.id}`) | ||
if (err != null) throw err; | ||
stream.pipe(process.stdout); | ||
}); | ||
this.log(`[ZEMU] Attached ${this.currentContainer.id}`); | ||
} | ||
await this.currentContainer.start() | ||
await this.currentContainer.start(); | ||
this.log(`[ZEMU] Started ${this.currentContainer.id}`) | ||
this.log(`[ZEMU] Started ${this.currentContainer.id}`); | ||
} | ||
async stop() { | ||
if (this.currentContainer) { | ||
const container = this.currentContainer | ||
this.currentContainer = null | ||
this.log(`[ZEMU] Stopping container`) | ||
async stop(): Promise<void> { | ||
if (this.currentContainer != null) { | ||
const container = this.currentContainer; | ||
delete this.currentContainer; | ||
this.log(`[ZEMU] Stopping container`); | ||
try { | ||
await container.stop({ t: 0 }) | ||
await container.stop({ t: 0 }); | ||
} catch (e) { | ||
this.log(`[ZEMU] Stopping: ${e}`) | ||
throw e | ||
this.log(`[ZEMU] Stopping: ${e}`); | ||
throw e; | ||
} | ||
this.log(`[ZEMU] Stopped`) | ||
this.log(`[ZEMU] Stopped`); | ||
try { | ||
await container.remove() | ||
await container.remove(); | ||
} catch (err) { | ||
this.log('[ZEMU] Unable to remove container') | ||
throw err | ||
this.log("[ZEMU] Unable to remove container"); | ||
throw err; | ||
} | ||
this.log(`[ZEMU] Removed`) | ||
this.log(`[ZEMU] Removed`); | ||
} | ||
} | ||
} |
@@ -1,20 +0,21 @@ | ||
import { Server } from '@grpc/grpc-js' | ||
import { loadPackageDefinition, Server, ServerCredentials } from "@grpc/grpc-js"; | ||
import { loadSync } from "@grpc/proto-loader"; | ||
import type Transport from "@ledgerhq/hw-transport"; | ||
import { resolve } from "path"; | ||
const PROTO_PATH = `${__dirname}/zemu.proto` | ||
const protoLoader = require('@grpc/proto-loader') | ||
const grpc = require('@grpc/grpc-js') | ||
const PROTO_PATH = resolve(__dirname, "zemu.proto"); | ||
export default class GRPCRouter { | ||
private httpTransport: any | ||
private serverAddress: string | ||
private server: Server | ||
private readonly httpTransport: Transport; | ||
private readonly serverAddress: string; | ||
private readonly server: Server; | ||
constructor(ip: string, port: number, options: { debug?: any }, transport: any) { | ||
this.httpTransport = transport | ||
this.serverAddress = `${ip}:${port}` | ||
this.server = new grpc.Server() | ||
constructor(ip: string, port: number, transport: any) { | ||
this.httpTransport = transport; | ||
this.serverAddress = `${ip}:${port}`; | ||
this.server = new Server(); | ||
} | ||
async startServer() { | ||
const packageDefinition = await protoLoader.load(PROTO_PATH, { | ||
startServer(): void { | ||
const packageDefinition = loadSync(PROTO_PATH, { | ||
keepCase: true, | ||
@@ -25,33 +26,30 @@ longs: String, | ||
oneofs: true, | ||
}) | ||
}); | ||
const rpcDefinition = grpc.loadPackageDefinition(packageDefinition) | ||
const rpcDefinition = loadPackageDefinition(packageDefinition); | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
const self = this | ||
const self = this; | ||
// @ts-expect-error types are missing | ||
this.server.addService(rpcDefinition.ledger_go.ZemuCommand.service, { | ||
Exchange(call: any, callback: any, ctx = self) { | ||
ctx.httpTransport.exchange(call.request.command).then((response: any) => { | ||
callback(null, { reply: response }) | ||
}) | ||
void ctx.httpTransport.exchange(call.request.command).then((response: Buffer) => { | ||
callback(null, { reply: response }); | ||
}); | ||
}, | ||
}) | ||
this.server.bindAsync( | ||
this.serverAddress, | ||
grpc.ServerCredentials.createInsecure(), | ||
// eslint-disable-next-line no-unused-vars | ||
(err, port) => { | ||
if (err != null) { | ||
return console.error(err) | ||
} | ||
process.stdout.write(`gRPC listening on ${port}`) | ||
this.server.start() | ||
}, | ||
) | ||
process.stdout.write(`grpc server started on ${this.serverAddress}`) | ||
}); | ||
this.server.bindAsync(this.serverAddress, ServerCredentials.createInsecure(), (err, port) => { | ||
if (err != null) { | ||
console.error(err); | ||
return; | ||
} | ||
process.stdout.write(`gRPC listening on ${port}`); | ||
this.server.start(); | ||
}); | ||
process.stdout.write(`grpc server started on ${this.serverAddress}`); | ||
} | ||
stopServer() { | ||
this.server.forceShutdown() | ||
stopServer(): void { | ||
this.server.forceShutdown(); | ||
} | ||
} |
636
src/index.ts
/** ****************************************************************************** | ||
* (c) 2020 Zondax GmbH | ||
* (c) 2018 - 2023 Zondax AG | ||
* | ||
@@ -16,630 +16,8 @@ * Licensed under the Apache License, Version 2.0 (the "License"); | ||
******************************************************************************* */ | ||
import Zemu from "./Zemu"; | ||
import axios from 'axios' | ||
import axiosRetry from 'axios-retry' | ||
import fs from 'fs-extra' | ||
import getPort from 'get-port' | ||
import PNG from 'pngjs' | ||
import HttpTransport from '@ledgerhq/hw-transport-http' | ||
import Transport from '@ledgerhq/hw-transport' | ||
// @ts-expect-error | ||
import elfy from 'elfy' | ||
import { resolve } from 'path' | ||
import rndstr from 'randomstring' | ||
import { | ||
BASE_NAME, | ||
DEFAULT_EMU_IMG, | ||
DEFAULT_HOST, | ||
DEFAULT_KEY_DELAY, | ||
DEFAULT_MODEL, | ||
DEFAULT_START_DELAY, | ||
DEFAULT_START_TEXT, | ||
DEFAULT_START_TIMEOUT, | ||
KILL_TIMEOUT, | ||
WINDOW_S, | ||
WINDOW_X, | ||
} from './constants' | ||
import EmuContainer from './emulator' | ||
import GRPCRouter from './grpc' | ||
export const DEFAULT_START_OPTIONS: StartOptions = { | ||
model: DEFAULT_MODEL, | ||
sdk: '', | ||
logging: false, | ||
custom: '', | ||
startDelay: DEFAULT_START_DELAY, | ||
startText: DEFAULT_START_TEXT, | ||
caseSensitive: false, | ||
startTimeout: DEFAULT_START_TIMEOUT, | ||
} | ||
export class StartOptions { | ||
logging = false | ||
startDelay = DEFAULT_START_DELAY | ||
custom = '' | ||
model = DEFAULT_MODEL | ||
sdk = '' | ||
startText = DEFAULT_START_TEXT | ||
caseSensitive = false | ||
startTimeout = DEFAULT_START_TIMEOUT | ||
} | ||
export interface Snapshot { | ||
width: number | ||
height: number | ||
} | ||
export class DeviceModel { | ||
name!: string | ||
prefix!: string | ||
path!: string | ||
} | ||
export default class Zemu { | ||
private startOptions: StartOptions | undefined | ||
private host: string | ||
private transportPort?: number | ||
protected speculosApiPort?: number | ||
private desiredTransportPort?: number | ||
private desiredSpeculosApiPort?: number | ||
private transportProtocol = 'http' | ||
private elfPath: string | ||
private grpcManager: GRPCRouter | null | undefined | ||
private mainMenuSnapshot: null | ||
private libElfs: { [p: string]: string } | ||
private emuContainer: EmuContainer | ||
private transport: Transport | undefined | ||
private containerName: string | ||
constructor( | ||
elfPath: string, | ||
libElfs: { [key: string]: string } = {}, | ||
host: string = DEFAULT_HOST, | ||
desiredTransportPort?: number, | ||
desiredSpeculosApiPort?: number, | ||
) { | ||
this.host = host | ||
this.desiredTransportPort = desiredTransportPort | ||
this.desiredSpeculosApiPort = desiredSpeculosApiPort | ||
this.elfPath = elfPath | ||
this.libElfs = libElfs | ||
this.mainMenuSnapshot = null | ||
if (this.elfPath == null) { | ||
throw new Error('elfPath cannot be null!') | ||
} | ||
if (!fs.existsSync(this.elfPath)) { | ||
throw new Error('elf file was not found! Did you compile?') | ||
} | ||
Object.keys(libElfs).forEach(libName => { | ||
if (!fs.existsSync(libElfs[libName])) { | ||
throw new Error('lib elf file was not found! Did you compile?') | ||
} | ||
}) | ||
this.containerName = BASE_NAME + rndstr.generate(12) // generate 12 chars long string | ||
this.emuContainer = new EmuContainer(this.elfPath, this.libElfs, DEFAULT_EMU_IMG, this.containerName) | ||
} | ||
static LoadPng2RGB(filename: string) { | ||
const tmpBuffer = fs.readFileSync(filename) | ||
return PNG.PNG.sync.read(tmpBuffer) | ||
} | ||
static delay(v: number = DEFAULT_KEY_DELAY) { | ||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, v) | ||
} | ||
static sleep(ms: number) { | ||
return new Promise<void>(resolve => setTimeout(resolve, ms)) | ||
} | ||
static async delayedPromise(p: Promise<any>, delay: number) { | ||
await Promise.race([ | ||
p, | ||
new Promise(resolve => { | ||
setTimeout(resolve, delay) | ||
}), | ||
]) | ||
} | ||
static async stopAllEmuContainers() { | ||
const timer = setTimeout(function () { | ||
console.log('Could not kill all containers before timeout!') | ||
process.exit(1) | ||
}, KILL_TIMEOUT) | ||
await EmuContainer.killContainerByName(BASE_NAME) | ||
clearTimeout(timer) | ||
} | ||
static async checkAndPullImage() { | ||
await EmuContainer.checkAndPullImage(DEFAULT_EMU_IMG) | ||
} | ||
static checkElf(model: string, elfPath: string) { | ||
const elfCodeNanoS = 0xc0d00001 | ||
const elfCodeNanoX = 0xc0de0001 | ||
const elfCodeNanoSP = 0xc0de0001 | ||
const elfApp = fs.readFileSync(elfPath) | ||
const elfInfo = elfy.parse(elfApp) | ||
if (elfInfo.entry !== elfCodeNanoS && elfInfo.entry !== elfCodeNanoX && elfInfo.entry !== elfCodeNanoSP) { | ||
throw new Error('Are you sure is a Nano S/S+/X app ?') | ||
} | ||
} | ||
async start(options: StartOptions) { | ||
this.startOptions = options | ||
this.log(`Checking ELF`) | ||
Zemu.checkElf(this.startOptions.model ?? DEFAULT_MODEL, this.elfPath) | ||
try { | ||
await this.assignPortsToListen() | ||
if (!this.transportPort || !this.speculosApiPort) { | ||
const e = new Error("The Speculos API port or/and transport port couldn't be reserved") | ||
this.log(`[ZEMU] ${e}`) | ||
// noinspection ExceptionCaughtLocallyJS | ||
throw e | ||
} | ||
this.log(`Starting Container`) | ||
await this.emuContainer.runContainer({ | ||
...this.startOptions, | ||
transportPort: this.transportPort.toString(), | ||
speculosApiPort: this.speculosApiPort.toString(), | ||
}) | ||
this.log(`Connecting to container`) | ||
// eslint-disable-next-liwaine func-names | ||
await this.connect().catch(error => { | ||
this.log(`${error}`) | ||
this.close() | ||
throw error | ||
}) | ||
// Captures main screen | ||
this.log(`Wait for start text`) | ||
await this.waitForText(this.startOptions.startText, this.startOptions.startTimeout, this.startOptions.caseSensitive) | ||
this.log(`Get initial snapshot`) | ||
this.mainMenuSnapshot = await this.snapshot() | ||
} catch (e) { | ||
this.log(`[ZEMU] ${e}`) | ||
throw e | ||
} | ||
} | ||
async connect() { | ||
const transportUrl = `${this.transportProtocol}://${this.host}:${this.transportPort}` | ||
const start = new Date() | ||
let connected = false | ||
const maxWait = this.startOptions?.startDelay ?? DEFAULT_START_DELAY | ||
while (!connected) { | ||
const currentTime = new Date() | ||
const elapsed = currentTime.getTime() - start.getTime() | ||
if (elapsed > maxWait) { | ||
throw `Timeout waiting to connect` | ||
} | ||
Zemu.delay() | ||
try { | ||
// here we should be able to import directly HttpTransport, instead of that Ledger | ||
// offers a wrapper that returns a `StaticTransport` instance | ||
// we need to expect the error to avoid typing errors | ||
// @ts-expect-error | ||
this.transport = await HttpTransport(transportUrl).open(transportUrl) | ||
connected = true | ||
} catch (e) { | ||
this.log(`WAIT ${this.containerName} ${elapsed} - ${e} ${transportUrl}`) | ||
connected = false | ||
} | ||
} | ||
} | ||
log(message: string) { | ||
if (this.startOptions?.logging ?? false) { | ||
const currentTimestamp = new Date().toISOString().slice(11, 23) | ||
process.stdout.write(`[ZEMU] ${currentTimestamp}: ${message}\n`) | ||
} | ||
} | ||
startGRPCServer(ip: string, port: number, options = {}) { | ||
this.grpcManager = new GRPCRouter(ip, port, options, this.transport) | ||
this.grpcManager.startServer() | ||
} | ||
stopGRPCServer() { | ||
if (this.grpcManager) { | ||
this.grpcManager.stopServer() | ||
} | ||
} | ||
async close() { | ||
this.log('Close') | ||
await this.emuContainer.stop() | ||
this.stopGRPCServer() | ||
} | ||
getTransport(): Transport { | ||
if (!this.transport) throw new Error('Transport is not loaded.') | ||
return this.transport | ||
} | ||
getWindowRect() { | ||
switch (this.startOptions?.model ?? DEFAULT_MODEL) { | ||
case 'nanos': | ||
return WINDOW_S | ||
case 'nanox': | ||
case 'nanosp': | ||
return WINDOW_X | ||
} | ||
throw `model ${this.startOptions?.model ?? DEFAULT_MODEL} not recognized` | ||
} | ||
async fetchSnapshot(url: string) { | ||
// Exponential back-off retry delay between requests | ||
axiosRetry(axios, { retryDelay: axiosRetry.exponentialDelay }) | ||
return axios({ | ||
method: 'GET', | ||
url: url, | ||
responseType: 'arraybuffer', | ||
}) | ||
} | ||
saveSnapshot(arrayBuffer: Buffer, filePath: string) { | ||
fs.writeFileSync(filePath, Buffer.from(arrayBuffer), 'binary') | ||
} | ||
convertBufferToPNG(arrayBuffer: Buffer) { | ||
return PNG.PNG.sync.read(Buffer.from(arrayBuffer)) | ||
} | ||
async snapshot(filename?: string): Promise<any> { | ||
const snapshotUrl = 'http://localhost:' + this.speculosApiPort?.toString() + '/screenshot' | ||
const { data } = await this.fetchSnapshot(snapshotUrl) | ||
const modelWindow = this.getWindowRect() | ||
if (filename) this.saveSnapshot(data, filename) | ||
const rect = { | ||
height: modelWindow.height, | ||
width: modelWindow.width, | ||
data, | ||
} | ||
return rect | ||
} | ||
async getMainMenuSnapshot() { | ||
return this.mainMenuSnapshot | ||
} | ||
async waitUntilScreenIsNot(screen: any, timeout = 60000) { | ||
const start = new Date() | ||
const inputSnapshotBufferHex = (await screen).data | ||
let currentSnapshotBufferHex = inputSnapshotBufferHex | ||
this.log(`Wait for screen change`) | ||
while (inputSnapshotBufferHex.equals(currentSnapshotBufferHex)) { | ||
const currentTime = new Date() | ||
const elapsed = currentTime.getTime() - start.getTime() | ||
if (elapsed > timeout) { | ||
throw `Timeout waiting for screen to change (${timeout} ms)` | ||
} | ||
Zemu.delay() | ||
this.log(`Check [${elapsed}ms]`) | ||
currentSnapshotBufferHex = (await this.snapshot()).data | ||
} | ||
this.log(`Screen changed`) | ||
} | ||
formatIndexString(i: number) { | ||
return `${i}`.padStart(5, '0') | ||
} | ||
getSnapshotPath(snapshotPrefix: string, index: number, takeSnapshots: boolean) { | ||
return takeSnapshots ? `${snapshotPrefix}/${this.formatIndexString(index)}.png` : undefined | ||
} | ||
async navigate( | ||
path: string, | ||
testcaseName: string, | ||
clickSchedule: number[], | ||
waitForScreenUpdate = true, | ||
takeSnapshots = true, | ||
startImgIndex = 0, | ||
) { | ||
const snapshotPrefixGolden = resolve(`${path}/snapshots/${testcaseName}`) | ||
const snapshotPrefixTmp = resolve(`${path}/snapshots-tmp/${testcaseName}`) | ||
if (takeSnapshots) { | ||
fs.ensureDirSync(snapshotPrefixGolden) | ||
fs.ensureDirSync(snapshotPrefixTmp) | ||
} | ||
let imageIndex = startImgIndex | ||
let filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots) | ||
this.log(`---------------------------`) | ||
this.log(`Start ${filename}`) | ||
await this.snapshot(filename) | ||
this.log(`Instructions ${clickSchedule}`) | ||
for (const value of clickSchedule) { | ||
// Both click action | ||
if (value == 0) { | ||
imageIndex += 1 | ||
filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots) | ||
await this.clickBoth(filename, waitForScreenUpdate) | ||
continue | ||
} | ||
// Move forward/backwards | ||
for (let j = 0; j < Math.abs(value); j += 1) { | ||
imageIndex += 1 | ||
filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots) | ||
if (value < 0) { | ||
await this.clickLeft(filename, waitForScreenUpdate) | ||
} else { | ||
await this.clickRight(filename, waitForScreenUpdate) | ||
} | ||
} | ||
} | ||
await this.dumpEvents() | ||
return imageIndex | ||
} | ||
async takeSnapshotAndOverwrite(path: string, testcaseName: string, imageIndex: number) { | ||
const snapshotPrefixTmp = resolve(`${path}/snapshots-tmp/${testcaseName}`) | ||
fs.ensureDirSync(snapshotPrefixTmp) | ||
const filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, true) | ||
try { | ||
if (typeof filename === 'undefined') throw Error | ||
fs.unlinkSync(filename) | ||
} catch (err) { | ||
console.log(err) | ||
throw new Error('Snapshot does not exist') | ||
} | ||
await this.snapshot(filename) | ||
} | ||
async navigateAndCompareSnapshots( | ||
path: string, | ||
testcaseName: string, | ||
clickSchedule: number[], | ||
waitForScreenUpdate = true, | ||
startImgIndex = 0, | ||
) { | ||
const takeSnapshots = true | ||
const lastImgIndex = await this.navigate(path, testcaseName, clickSchedule, waitForScreenUpdate, takeSnapshots, startImgIndex) | ||
return this.compareSnapshots(path, testcaseName, lastImgIndex) | ||
} | ||
compareSnapshots(path: string, testcaseName: string, snapshotCount: number): boolean { | ||
const snapshotPrefixGolden = resolve(`${path}/snapshots/${testcaseName}`) | ||
const snapshotPrefixTmp = resolve(`${path}/snapshots-tmp/${testcaseName}`) | ||
this.log(`golden ${snapshotPrefixGolden}`) | ||
this.log(`tmp ${snapshotPrefixTmp}`) | ||
//////////////////// | ||
this.log(`Start comparison`) | ||
for (let j = 0; j < snapshotCount + 1; j += 1) { | ||
this.log(`Checked ${snapshotPrefixTmp}/${this.formatIndexString(j)}.png`) | ||
const img1 = Zemu.LoadPng2RGB(`${snapshotPrefixTmp}/${this.formatIndexString(j)}.png`) | ||
const img2 = Zemu.LoadPng2RGB(`${snapshotPrefixGolden}/${this.formatIndexString(j)}.png`) | ||
if (!img1.data.equals(img2.data)) { | ||
throw new Error(`Image [${this.formatIndexString(j)}] do not match!`) | ||
} | ||
} | ||
return true | ||
} | ||
/** | ||
* @deprecated The method will be deprecated soon. Try to use navigateAndCompareSnapshots instead | ||
*/ | ||
async compareSnapshotsAndAccept(path: string, testcaseName: string, snapshotCount: number, backClickCount = 0) { | ||
const instructions = [] | ||
if (snapshotCount > 0) instructions.push(snapshotCount) | ||
if (backClickCount > 0) { | ||
instructions.push(-backClickCount) | ||
instructions.push(backClickCount) | ||
} | ||
instructions.push(0) | ||
return this.navigateAndCompareSnapshots(path, testcaseName, instructions) | ||
} | ||
async compareSnapshotsAndApprove( | ||
path: string, | ||
testcaseName: string, | ||
waitForScreenUpdate = true, | ||
startImgIndex = 0, | ||
timeout = 30000, | ||
): Promise<boolean> { | ||
return this.navigateAndCompareUntilText(path, testcaseName, 'APPROVE', waitForScreenUpdate, startImgIndex, timeout) | ||
} | ||
async navigateUntilText( | ||
path: string, | ||
testcaseName: string, | ||
text: string, | ||
waitForScreenUpdate = true, | ||
takeSnapshots = true, | ||
startImgIndex = 0, | ||
timeout = 30000, | ||
): Promise<number> { | ||
const snapshotPrefixGolden = resolve(`${path}/snapshots/${testcaseName}`) | ||
const snapshotPrefixTmp = resolve(`${path}/snapshots-tmp/${testcaseName}`) | ||
if (takeSnapshots) { | ||
fs.ensureDirSync(snapshotPrefixGolden) | ||
fs.ensureDirSync(snapshotPrefixTmp) | ||
} | ||
let imageIndex = startImgIndex | ||
let filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots) | ||
await this.snapshot(filename) | ||
let start = new Date() | ||
let found = false | ||
while (!found) { | ||
const currentTime = new Date() | ||
const elapsed = currentTime.getTime() - start.getTime() | ||
if (elapsed > timeout) { | ||
throw `Timeout waiting for screen containing ${text}` | ||
} | ||
const events = await this.getEvents() | ||
imageIndex += 1 | ||
filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, takeSnapshots) | ||
found = events.some((event: any) => event.text.includes(text)) | ||
if (found) { | ||
await this.clickBoth(filename, waitForScreenUpdate) | ||
} else { | ||
// navigate to next screen | ||
await this.clickRight(filename, waitForScreenUpdate) | ||
start = new Date() | ||
} | ||
} | ||
return imageIndex | ||
} | ||
async navigateAndCompareUntilText( | ||
path: string, | ||
testcaseName: string, | ||
text: string, | ||
waitForScreenUpdate = true, | ||
startImgIndex = 0, | ||
timeout = 30000, | ||
): Promise<boolean> { | ||
const takeSnapshots = true | ||
const lastImgIndex = await this.navigateUntilText(path, testcaseName, text, waitForScreenUpdate, takeSnapshots, startImgIndex, timeout) | ||
return this.compareSnapshots(path, testcaseName, lastImgIndex) | ||
} | ||
async getEvents() { | ||
axiosRetry(axios, { retryDelay: axiosRetry.exponentialDelay }) | ||
const eventsUrl = 'http://localhost:' + this.speculosApiPort?.toString() + '/events' | ||
try { | ||
const { data } = await axios.get(eventsUrl) | ||
return data['events'] | ||
} catch (error) { | ||
return [] | ||
} | ||
} | ||
async deleteEvents() { | ||
await axios({ | ||
method: 'DELETE', | ||
url: 'http://localhost:' + this.speculosApiPort?.toString() + '/events', | ||
}) | ||
} | ||
async dumpEvents() { | ||
const events = await this.getEvents() | ||
if (events) { | ||
events.forEach((x: any) => this.log(`[ZEMU] ${JSON.stringify(x)}`)) | ||
} | ||
} | ||
async waitScreenChange(timeout = 30000) { | ||
const start = new Date() | ||
const prev_events_qty = (await this.getEvents()).length | ||
let current_events_qty = prev_events_qty | ||
this.log(`Wait for screen change`) | ||
while (prev_events_qty === current_events_qty) { | ||
const currentTime = new Date() | ||
const elapsed = currentTime.getTime() - start.getTime() | ||
if (elapsed > timeout) { | ||
throw `Timeout waiting for screen to change (${timeout} ms)` | ||
} | ||
Zemu.delay() | ||
this.log(`Check [${elapsed}ms]`) | ||
current_events_qty = (await this.getEvents()).length | ||
} | ||
this.log(`Screen changed`) | ||
} | ||
async waitForText(text: string | RegExp, timeout = 60000, caseSensitive = false) { | ||
const start = new Date() | ||
let found = false | ||
const flags = !caseSensitive ? 'i' : '' | ||
const startRegex = new RegExp(text, flags) | ||
while (!found) { | ||
const currentTime = new Date() | ||
const elapsed = currentTime.getTime() - start.getTime() | ||
if (elapsed > timeout) { | ||
throw `Timeout (${timeout}) waiting for text (${text})` | ||
} | ||
const events = await this.getEvents() | ||
found = events.some((event: any) => startRegex.test(event.text)) | ||
Zemu.delay() | ||
} | ||
} | ||
async click(endpoint: string, filename?: string, waitForScreenUpdate?: boolean) { | ||
let previousScreen | ||
if (waitForScreenUpdate) previousScreen = await this.snapshot() | ||
const bothClickUrl = 'http://localhost:' + this.speculosApiPort?.toString() + endpoint | ||
const payload = { action: 'press-and-release' } | ||
await axios.post(bothClickUrl, payload) | ||
this.log(`Click ${endpoint} -> ${filename}`) | ||
// Wait and poll Speculos until the application screen gets updated | ||
if (waitForScreenUpdate) await this.waitUntilScreenIsNot(previousScreen) | ||
else Zemu.delay() // A minimum delay is required | ||
return this.snapshot(filename) | ||
} | ||
async clickLeft(filename?: string, waitForScreenUpdate = true) { | ||
return this.click('/button/left', filename, waitForScreenUpdate) | ||
} | ||
async clickRight(filename?: string, waitForScreenUpdate = true) { | ||
return this.click('/button/right', filename, waitForScreenUpdate) | ||
} | ||
async clickBoth(filename?: string, waitForScreenUpdate = true) { | ||
return this.click('/button/both', filename, waitForScreenUpdate) | ||
} | ||
private async assignPortsToListen(): Promise<void> { | ||
if (!this.transportPort || !this.speculosApiPort) { | ||
const transportPort = await getPort({ port: this.desiredTransportPort }) | ||
const speculosApiPort = await getPort({ port: this.desiredSpeculosApiPort }) | ||
this.transportPort = transportPort | ||
this.speculosApiPort = speculosApiPort | ||
} | ||
} | ||
} | ||
export default Zemu; | ||
export { ClickNavigation, TouchNavigation } from "./actions"; | ||
export { DEFAULT_START_OPTIONS } from "./constants"; | ||
export { ButtonKind, type IDeviceModel, type INavElement, type IStartOptions } from "./types"; | ||
export { zondaxMainmenuNavigation } from "./zondax"; |
@@ -18,124 +18,123 @@ // noinspection SpellCheckingInspection | ||
******************************************************************************* */ | ||
import Zemu, { DEFAULT_START_OPTIONS, StartOptions } from '../src' | ||
import MinimalApp from './minapp' | ||
import { newPolymeshApp } from '@zondax/ledger-substrate' | ||
import { newPolymeshApp } from "@zondax/ledger-substrate"; | ||
import Zemu, { DEFAULT_START_OPTIONS, IStartOptions } from "../src"; | ||
import MinimalApp from "./minapp"; | ||
const Resolve = require('path').resolve | ||
import { resolve } from "path"; | ||
jest.setTimeout(60000) | ||
const DEMO_APP_PATH_S = Resolve('bin/demoAppS.elf') | ||
const DEMO_APP2_PATH_S = Resolve('bin/app_s.elf') | ||
jest.setTimeout(60000); | ||
const DEMO_APP_PATH_S = resolve("bin/demoAppS.elf"); | ||
const DEMO_APP2_PATH_S = resolve("bin/app_s.elf"); | ||
const APP_SEED = 'equip will roof matter pink blind book anxiety banner elbow sun young' | ||
const APP_SEED = "equip will roof matter pink blind book anxiety banner elbow sun young"; | ||
const ZEMU_OPTIONS_S: StartOptions = { | ||
const ZEMU_OPTIONS_S: IStartOptions = { | ||
...DEFAULT_START_OPTIONS, | ||
logging: true, | ||
custom: `-s "${APP_SEED}" `, | ||
} | ||
}; | ||
test.concurrent('File-Missing', () => { | ||
test.concurrent("File-Missing", () => { | ||
expect(() => { | ||
new Zemu('it_does_not_exist') | ||
}).toThrow(/Did you compile/) | ||
}) | ||
new Zemu("it_does_not_exist"); | ||
}).toThrow(/Did you compile/); | ||
}); | ||
test.concurrent('Start&Close-NanoS', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S) | ||
expect(sim).not.toBeNull() | ||
test.concurrent("Start&Close-NanoS", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S); | ||
expect(sim).not.toBeNull(); | ||
try { | ||
await sim.start(ZEMU_OPTIONS_S) | ||
await sim.start(ZEMU_OPTIONS_S); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); | ||
test.concurrent('Basic Control - S', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S) | ||
test.concurrent("Basic Control - S", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S); | ||
try { | ||
await sim.start(ZEMU_OPTIONS_S) | ||
await sim.start(ZEMU_OPTIONS_S); | ||
await sim.clickLeft(undefined, false) | ||
await sim.clickLeft(undefined, false) | ||
await sim.clickLeft(undefined, false) | ||
await sim.clickLeft(undefined, false); | ||
await sim.clickLeft(undefined, false); | ||
await sim.clickLeft(undefined, false); | ||
// Move up and down and check screens | ||
const view0 = await sim.snapshot('tests/tmp/00000.png') | ||
const view1 = await sim.clickRight('tests/tmp/00001.png') | ||
const view2 = await sim.clickLeft('tests/tmp/00002.png') | ||
const view0 = await sim.snapshot("tests/tmp/00000.png"); | ||
const view1 = await sim.clickRight("tests/tmp/00001.png"); | ||
const view2 = await sim.clickLeft("tests/tmp/00002.png"); | ||
// compare to check that it went back to the same view | ||
expect(view2).toEqual(view0) | ||
expect(view1).not.toEqual(view0) | ||
expect(view2).toEqual(view0); | ||
expect(view1).not.toEqual(view0); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); | ||
test.concurrent('Load/Compare Snapshots', async () => { | ||
const image1A = Zemu.LoadPng2RGB('tests/snapshots/image1A.png') | ||
const image1B = Zemu.LoadPng2RGB('tests/snapshots/image1B.png') | ||
const image2A = Zemu.LoadPng2RGB('tests/snapshots/image2A.png') | ||
test.concurrent("Load/Compare Snapshots", async () => { | ||
const image1A = Zemu.LoadPng2RGB("tests/snapshots/image1A.png"); | ||
const image1B = Zemu.LoadPng2RGB("tests/snapshots/image1B.png"); | ||
const image2A = Zemu.LoadPng2RGB("tests/snapshots/image2A.png"); | ||
expect(image1A).toEqual(image1B) | ||
expect(image1A).not.toEqual(image2A) | ||
}) | ||
expect(image1A).toEqual(image1B); | ||
expect(image1A).not.toEqual(image2A); | ||
}); | ||
test.concurrent('Wait for change / timeout', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S) | ||
test.concurrent("Wait for change / timeout", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S); | ||
try { | ||
await sim.start(ZEMU_OPTIONS_S) | ||
const result = sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot(), 5000) | ||
await expect(result).rejects.toEqual('Timeout waiting for screen to change (5000 ms)') | ||
await sim.start(ZEMU_OPTIONS_S); | ||
const result = sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot(), 5000); | ||
await expect(result).rejects.toThrowError("Timeout waiting for screen to change (5000 ms)"); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); | ||
test.concurrent('Snapshot and compare', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S) | ||
test.concurrent("Snapshot and compare", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S); | ||
try { | ||
await sim.start(ZEMU_OPTIONS_S) | ||
expect(await sim.compareSnapshotsAndAccept('tests', 'compare_test', 1)).toBeTruthy() | ||
await sim.start(ZEMU_OPTIONS_S); | ||
expect(await sim.navigateAndCompareUntilText("tests", "compare_test", "Expert")).toBeTruthy(); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); | ||
test.concurrent('Snapshot and compare 2', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S) | ||
test.concurrent("Snapshot and compare 2", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S); | ||
try { | ||
await sim.start(ZEMU_OPTIONS_S) | ||
await sim.start(ZEMU_OPTIONS_S); | ||
expect(await sim.compareSnapshotsAndAccept('tests', 'compare_test2', 1, 1)).toBeTruthy() | ||
expect(await sim.navigateAndCompareSnapshots("tests", "compare_test2", [1, -1, 1, 0])).toBeTruthy(); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); | ||
// eslint-disable-next-line jest/expect-expect | ||
test.concurrent('GRPC Server start-stop', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S) | ||
await sim.start(ZEMU_OPTIONS_S) | ||
sim.startGRPCServer('localhost', 3002) | ||
await Zemu.sleep(3000) | ||
await sim.close() | ||
}) | ||
test.concurrent("GRPC Server start-stop", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S); | ||
await sim.start(ZEMU_OPTIONS_S); | ||
sim.startGRPCServer("localhost", 3002); | ||
await Zemu.sleep(3000); | ||
await sim.close(); | ||
}); | ||
test.concurrent('Get app info', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S) | ||
expect(sim).not.toBeNull() | ||
test.concurrent("Get app info", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_S); | ||
expect(sim).not.toBeNull(); | ||
try { | ||
await sim.start(ZEMU_OPTIONS_S) | ||
const app = new MinimalApp(sim.getTransport()) | ||
const resp = await app.appInfo() | ||
await sim.start(ZEMU_OPTIONS_S); | ||
const app = new MinimalApp(sim.getTransport()); | ||
const resp = await app.appInfo(); | ||
console.log(resp) | ||
console.log(resp); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); | ||
test.concurrent('sign real app', async function () { | ||
const sim = new Zemu(DEMO_APP2_PATH_S) | ||
test.concurrent("sign real app", async function () { | ||
const sim = new Zemu(DEMO_APP2_PATH_S); | ||
try { | ||
@@ -147,26 +146,26 @@ const defaultOptions = { | ||
X11: false, | ||
} | ||
}; | ||
await sim.start({ ...defaultOptions, model: 'nanos' }) | ||
const app = newPolymeshApp(sim.getTransport()) | ||
const pathAccount = 0x80000000 | ||
const pathChange = 0x80000000 | ||
const pathIndex = 0x80000000 | ||
await sim.start({ ...defaultOptions, model: "nanos" }); | ||
const app = newPolymeshApp(sim.getTransport()); | ||
const pathAccount = 0x80000000; | ||
const pathChange = 0x80000000; | ||
const pathIndex = 0x80000000; | ||
const txBasic = | ||
'050000ca1ef1d326bd379143d6e743f6c3b51b7058d07e02e4614dc027e05bdb226c6503d2029649d503ae1103008ed73e0db80b0000010000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f40636fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063' | ||
const txBlob = Buffer.from(txBasic, 'hex') | ||
"050000ca1ef1d326bd379143d6e743f6c3b51b7058d07e02e4614dc027e05bdb226c6503d2029649d503ae1103008ed73e0db80b0000010000006fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f40636fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063"; | ||
const txBlob = Buffer.from(txBasic, "hex"); | ||
const signatureRequest = app.sign(pathAccount, pathChange, pathIndex, txBlob) | ||
await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot()) | ||
await sim.compareSnapshotsAndApprove('.', `s-sign_basic_normal`) | ||
const signatureRequest = app.sign(pathAccount, pathChange, pathIndex, txBlob); | ||
await sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot()); | ||
await sim.compareSnapshotsAndApprove("tests", `s-sign_basic_normal`); | ||
const signatureResponse = await signatureRequest | ||
console.log(signatureResponse) | ||
const signatureResponse = await signatureRequest; | ||
console.log(signatureResponse); | ||
expect(signatureResponse.return_code).toEqual(0x9000) | ||
expect(signatureResponse.error_message).toEqual('No errors') | ||
expect(signatureResponse.return_code).toEqual(0x9000); | ||
expect(signatureResponse.error_message).toEqual("No errors"); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); |
@@ -18,78 +18,74 @@ // noinspection SpellCheckingInspection | ||
******************************************************************************* */ | ||
import Zemu, { DEFAULT_START_OPTIONS, StartOptions } from '../src' | ||
import Zemu, { DEFAULT_START_OPTIONS, IStartOptions } from "../src"; | ||
const Resolve = require('path').resolve | ||
import { resolve } from "path"; | ||
jest.setTimeout(60000) | ||
const DEMO_APP_PATH_X = Resolve('bin/demoAppX.elf') | ||
jest.setTimeout(60000); | ||
const DEMO_APP_PATH_X = resolve("bin/demoAppX.elf"); | ||
const APP_SEED = 'equip will roof matter pink blind book anxiety banner elbow sun young' | ||
const APP_SEED = "equip will roof matter pink blind book anxiety banner elbow sun young"; | ||
beforeAll(async () => { | ||
await Zemu.checkAndPullImage() | ||
}) | ||
const ZEMU_OPTIONS_X: StartOptions = { | ||
const ZEMU_OPTIONS_X: IStartOptions = { | ||
...DEFAULT_START_OPTIONS, | ||
logging: true, | ||
custom: `-s "${APP_SEED}" `, | ||
model: 'nanox', | ||
} | ||
model: "nanox", | ||
}; | ||
test.concurrent('File-Missing', () => { | ||
test.concurrent("File-Missing", () => { | ||
expect(() => { | ||
new Zemu('it_does_not_exist') | ||
}).toThrow(/Did you compile/) | ||
}) | ||
new Zemu("it_does_not_exist"); | ||
}).toThrow(/Did you compile/); | ||
}); | ||
test.concurrent('Start&Close-NanoX', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_X) | ||
expect(sim).not.toBeNull() | ||
test.concurrent("Start&Close-NanoX", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_X); | ||
expect(sim).not.toBeNull(); | ||
try { | ||
await sim.start(ZEMU_OPTIONS_X) | ||
await sim.start(ZEMU_OPTIONS_X); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); | ||
test.concurrent('Basic Control - X', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_X) | ||
test.concurrent("Basic Control - X", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_X); | ||
try { | ||
await sim.start(ZEMU_OPTIONS_X) | ||
await sim.start(ZEMU_OPTIONS_X); | ||
await sim.clickLeft(undefined, false) | ||
await sim.clickLeft(undefined, false) | ||
await sim.clickLeft(undefined, false) | ||
await sim.clickLeft(undefined, false); | ||
await sim.clickLeft(undefined, false); | ||
await sim.clickLeft(undefined, false); | ||
// Move up and down and check screens | ||
const view0 = await sim.snapshot('tests/tmpX/00000.png') | ||
const view1 = await sim.clickRight('tests/tmpX/00001.png') | ||
const view2 = await sim.clickLeft('tests/tmpX/00002.png') | ||
const view0 = await sim.snapshot("tests/tmpX/00000.png"); | ||
const view1 = await sim.clickRight("tests/tmpX/00001.png"); | ||
const view2 = await sim.clickLeft("tests/tmpX/00002.png"); | ||
// compare to check that it went back to the same view | ||
expect(view2).toEqual(view0) | ||
expect(view1).not.toEqual(view0) | ||
expect(view2).toEqual(view0); | ||
expect(view1).not.toEqual(view0); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); | ||
test.concurrent('Load/Compare Snapshots', async () => { | ||
const image1A = Zemu.LoadPng2RGB('tests/snapshots/image1A.png') | ||
const image1B = Zemu.LoadPng2RGB('tests/snapshots/image1B.png') | ||
const image2A = Zemu.LoadPng2RGB('tests/snapshots/image2A.png') | ||
test.concurrent("Load/Compare Snapshots", async () => { | ||
const image1A = Zemu.LoadPng2RGB("tests/snapshots/image1A.png"); | ||
const image1B = Zemu.LoadPng2RGB("tests/snapshots/image1B.png"); | ||
const image2A = Zemu.LoadPng2RGB("tests/snapshots/image2A.png"); | ||
expect(image1A).toEqual(image1B) | ||
expect(image1A).not.toEqual(image2A) | ||
}) | ||
expect(image1A).toEqual(image1B); | ||
expect(image1A).not.toEqual(image2A); | ||
}); | ||
test.concurrent('Wait for change / timeout', async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_X) | ||
test.concurrent("Wait for change / timeout", async () => { | ||
const sim = new Zemu(DEMO_APP_PATH_X); | ||
try { | ||
await sim.start(ZEMU_OPTIONS_X) | ||
const result = sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot(), 2000) | ||
await expect(result).rejects.toEqual('Timeout waiting for screen to change (2000 ms)') | ||
await sim.start(ZEMU_OPTIONS_X); | ||
const result = sim.waitUntilScreenIsNot(sim.getMainMenuSnapshot(), 2000); | ||
await expect(result).rejects.toThrowError("Timeout waiting for screen to change (2000 ms)"); | ||
} finally { | ||
await sim.close() | ||
await sim.close(); | ||
} | ||
}) | ||
}); |
@@ -1,16 +0,12 @@ | ||
import Zemu from '../src' | ||
import Zemu from "../src"; | ||
const catchExit = async () => { | ||
process.on('SIGINT', () => { | ||
console.log('Stopping dangling containers') | ||
Zemu.stopAllEmuContainers() | ||
}) | ||
} | ||
process.on("SIGINT", () => { | ||
console.log("Stopping dangling containers"); | ||
Zemu.stopAllEmuContainers(); | ||
}); | ||
}; | ||
module.exports = async () => { | ||
console.log('Executing tasks before starting the test suites') | ||
await catchExit() | ||
await Zemu.checkAndPullImage() | ||
await Zemu.stopAllEmuContainers() | ||
} | ||
await catchExit(); | ||
}; |
@@ -19,3 +19,3 @@ /** ****************************************************************************** | ||
function isDict(v: any) { | ||
return typeof v === 'object' && v !== null && !(v instanceof Array) && !(v instanceof Date) | ||
return typeof v === "object" && v !== null && !(v instanceof Array) && !(v instanceof Date); | ||
} | ||
@@ -26,14 +26,14 @@ | ||
if (isDict(response)) { | ||
if (Object.prototype.hasOwnProperty.call(response, 'statusCode')) { | ||
if (Object.prototype.hasOwnProperty.call(response, "statusCode")) { | ||
return { | ||
return_code: response.statusCode, | ||
error_message: response.statusCode.toString, | ||
} | ||
}; | ||
} | ||
if ( | ||
Object.prototype.hasOwnProperty.call(response, 'return_code') && | ||
Object.prototype.hasOwnProperty.call(response, 'error_message') | ||
Object.prototype.hasOwnProperty.call(response, "return_code") && | ||
Object.prototype.hasOwnProperty.call(response, "error_message") | ||
) { | ||
return response | ||
return response; | ||
} | ||
@@ -44,3 +44,3 @@ } | ||
error_message: response.toString(), | ||
} | ||
}; | ||
} | ||
@@ -51,15 +51,15 @@ | ||
error_message: response.toString(), | ||
} | ||
}; | ||
} | ||
export default class MinimalApp { | ||
private transport: any | ||
private transport: any; | ||
constructor(transport: any) { | ||
if (!transport) { | ||
throw new Error('Transport has not been defined') | ||
throw new Error("Transport has not been defined"); | ||
} | ||
this.transport = transport | ||
transport.decorateAppAPIMethods(this, ['appInfo']) | ||
this.transport = transport; | ||
transport.decorateAppAPIMethods(this, ["appInfo"]); | ||
} | ||
@@ -69,28 +69,28 @@ | ||
return this.transport.send(0xb0, 0x01, 0, 0).then((response: any) => { | ||
const errorCodeData = response.slice(-2) | ||
const returnCode = errorCodeData[0] * 256 + errorCodeData[1] | ||
const errorCodeData = response.slice(-2); | ||
const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; | ||
const result: any = {} | ||
const result: any = {}; | ||
let appName = 'err' | ||
let appVersion = 'err' | ||
let flagLen = 0 | ||
let flagsValue = 0 | ||
let appName = "err"; | ||
let appVersion = "err"; | ||
let flagLen = 0; | ||
let flagsValue = 0; | ||
if (response[0] !== 1) { | ||
// Ledger responds with format ID 1. There is no spec for any format != 1 | ||
result.error_message = 'response format ID not recognized' | ||
result.return_code = 0x9001 | ||
result.error_message = "response format ID not recognized"; | ||
result.return_code = 0x9001; | ||
} else { | ||
const appNameLen = response[1] | ||
appName = response.slice(2, 2 + appNameLen).toString('ascii') | ||
let idx = 2 + appNameLen | ||
const appVersionLen = response[idx] | ||
idx += 1 | ||
appVersion = response.slice(idx, idx + appVersionLen).toString('ascii') | ||
idx += appVersionLen | ||
const appFlagsLen = response[idx] | ||
idx += 1 | ||
flagLen = appFlagsLen | ||
flagsValue = response[idx] | ||
const appNameLen = response[1]; | ||
appName = response.slice(2, 2 + appNameLen).toString("ascii"); | ||
let idx = 2 + appNameLen; | ||
const appVersionLen = response[idx]; | ||
idx += 1; | ||
appVersion = response.slice(idx, idx + appVersionLen).toString("ascii"); | ||
idx += appVersionLen; | ||
const appFlagsLen = response[idx]; | ||
idx += 1; | ||
flagLen = appFlagsLen; | ||
flagsValue = response[idx]; | ||
} | ||
@@ -101,3 +101,2 @@ | ||
error_message: returnCode.toString(), | ||
// // | ||
appName, | ||
@@ -107,13 +106,9 @@ appVersion, | ||
flagsValue, | ||
// eslint-disable-next-line no-bitwise | ||
flag_recovery: (flagsValue & 1) !== 0, | ||
// eslint-disable-next-line no-bitwise | ||
flag_signed_mcu_code: (flagsValue & 2) !== 0, | ||
// eslint-disable-next-line no-bitwise | ||
flag_onboarded: (flagsValue & 4) !== 0, | ||
// eslint-disable-next-line no-bitwise | ||
flag_pin_validated: (flagsValue & 128) !== 0, | ||
} | ||
}, processErrorResponse) | ||
}; | ||
}, processErrorResponse); | ||
} | ||
} |
@@ -1,4 +0,4 @@ | ||
import Zemu from '../src/index' | ||
import Zemu from "../src/index"; | ||
Zemu.checkAndPullImage() | ||
Zemu.stopAllEmuContainers() | ||
Zemu.checkAndPullImage(); | ||
Zemu.stopAllEmuContainers(); |
{ | ||
"compilerOptions": { | ||
"target": "es2020", | ||
"module": "commonjs", | ||
"moduleResolution": "node", | ||
"strict": true, | ||
@@ -11,3 +13,3 @@ "esModuleInterop": true, | ||
}, | ||
"exclude": ["node_modules", "tests", "./dist/**"] | ||
"exclude": ["node_modules", "tests", "dist"] | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3028738
12
77
3028
45
1
+ Addedfs-extra@11.2.0(transitive)
- Removedpath@^0.12.7
- Removedfs-extra@10.1.0(transitive)
- Removedinherits@2.0.3(transitive)
- Removedpath@0.12.7(transitive)
- Removedprocess@0.11.10(transitive)
- Removedutil@0.10.4(transitive)
Updated@grpc/grpc-js@^1.8.8
Updated@grpc/proto-loader@^0.7.5
Updatedaxios@^1.3.3
Updatedaxios-retry@^3.4.0
Updatedfs-extra@^11.0.0