Socket
Socket
Sign inDemoInstall

@zondax/zemu

Package Overview
Dependencies
Maintainers
3
Versions
166
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@zondax/zemu - npm Package Compare versions

Comparing version 0.34.0 to 0.35.0-beta.1

.eslintrc.json

39

dist/constants.d.ts

@@ -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();
}
}
/** ******************************************************************************
* (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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc