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.33.2 to 0.34.0-beta.1

docs/01-quickstart.md

2

dist/constants.d.ts

@@ -6,3 +6,3 @@ export declare const DEFAULT_EMU_IMG = "zondax/builder-zemu@sha256:7cae0f781ea6f6a58c39f273763bb61176b377bd0d6c713e59ae38e0531ae4ab";

export declare const DEFAULT_HOST = "127.0.0.1";
export declare const BASE_NAME = "zemu-656d75-";
export declare const BASE_NAME = "zemu-test-";
export declare const DEFAULT_START_TIMEOUT = 20000;

@@ -9,0 +9,0 @@ export declare const KILL_TIMEOUT = 5000;

@@ -9,3 +9,3 @@ "use strict";

exports.DEFAULT_HOST = '127.0.0.1';
exports.BASE_NAME = 'zemu-656d75-';
exports.BASE_NAME = 'zemu-test-';
exports.DEFAULT_START_TIMEOUT = 20000;

@@ -12,0 +12,0 @@ exports.KILL_TIMEOUT = 5000;

@@ -8,14 +8,13 @@ export declare const DEV_CERT_PRIVATE_KEY = "ff701d781f43ce106f72dc26a46b6a83e053b5d07bb3d4ceab79c91ca822a66b";

private readonly name;
private startDelay;
private readonly image;
private libElfs;
private currentContainer;
constructor(elfLocalPath: string, libElfs: any, image: any, name: string);
constructor(elfLocalPath: string, libElfs: {
[p: string]: string;
}, image: string, name: string);
static killContainerByName(name: string): Promise<void>;
static checkAndPullImage(imageName: string): Promise<void>;
static checkAndPullImage(imageName: string): Promise<any>;
log(message: string): void;
runContainer(options: {
logging: any;
startDelay: any;
X11: boolean;
logging: boolean;
custom: string;

@@ -22,0 +21,0 @@ model: string;

@@ -38,2 +38,5 @@ "use strict";

};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
exports.__esModule = true;

@@ -56,4 +59,4 @@ exports.DEFAULT_APP_PATH = exports.BOLOS_SDK = exports.DEV_CERT_PRIVATE_KEY = void 0;

******************************************************************************* */
var path = require('path');
var Docker = require('dockerode');
var path_1 = __importDefault(require("path"));
var dockerode_1 = __importDefault(require("dockerode"));
exports.DEV_CERT_PRIVATE_KEY = 'ff701d781f43ce106f72dc26a46b6a83e053b5d07bb3d4ceab79c91ca822a66b';

@@ -70,3 +73,2 @@ exports.BOLOS_SDK = '/project/deps/nanos-secure-sdk';

this.logging = false;
this.startDelay = 200;
}

@@ -77,19 +79,17 @@ EmuContainer.killContainerByName = function (name) {

return __generator(this, function (_a) {
switch (_a.label) {
case 0:
docker = new Docker();
return [4 /*yield*/, new Promise(function (resolve) {
docker.listContainers({ all: true, filters: { name: [name] } }, function (err, containers) {
containers.forEach(function (containerInfo) {
docker.getContainer(containerInfo.Id).remove({ force: true }, function () {
// console.log("container removed");
});
});
return resolve(true);
docker = new dockerode_1["default"]();
return [2 /*return*/, new Promise(function (resolve) {
docker.listContainers({ all: true, filters: { name: [name] } }, function (containers) {
if (!(containers === null || containers === void 0 ? void 0 : containers.length))
throw 'Container not found, cannot be removed';
containers.forEach(function (containerInfo) {
docker.getContainer(containerInfo.Id).remove({ force: true }, function (err, res) {
if (err)
throw err;
console.log(res);
});
})];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
resolve();
})];
});

@@ -102,35 +102,24 @@ });

return __generator(this, function (_a) {
switch (_a.label) {
case 0:
docker = new Docker();
return [4 /*yield*/, new Promise(function (resolve) {
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) {
resolve(true);
}
else {
process.stdout.write("[DOCKER] ".concat(err, "\n"));
process.exit(1);
}
}
if (err) {
process.stdout.write("[DOCKER] ".concat(err, "\n"));
throw new Error(err);
}
docker.modem.followProgress(stream, onFinished, onProgress);
});
})];
case 1:
_a.sent();
return [2 /*return*/];
}
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);
})];
});

@@ -147,3 +136,3 @@ });

return __awaiter(this, void 0, void 0, function () {
var docker, appFilename, appDir, dirBindings, libArgs, displaySetting, displayEnvironment, modelOptions, sdkOption, customOptions, command, portBindings, environment, _a;
var docker, appFilename, appDir, dirBindings, libArgs, modelOptions, sdkOption, customOptions, command, portBindings, environment, _a;
var _b;

@@ -153,12 +142,6 @@ return __generator(this, function (_c) {

case 0:
if ('X11' in options && options.X11) {
this.log('[ZEMU] X11 support is deprecated and not supported anymore');
this.log('[ZEMU] automatically disabling');
options.X11 = false;
}
docker = new Docker();
docker = new dockerode_1["default"]();
this.logging = options.logging;
this.startDelay = options.startDelay;
appFilename = path.basename(this.elfLocalPath);
appDir = path.dirname(this.elfLocalPath);
appFilename = path_1["default"].basename(this.elfLocalPath);
appDir = path_1["default"].dirname(this.elfLocalPath);
dirBindings = ["".concat(appDir, ":").concat(exports.DEFAULT_APP_PATH)];

@@ -168,18 +151,5 @@ libArgs = '';

var libName = _a[0], libPath = _a[1];
var libFilename = path.basename(libPath);
var libFilename = path_1["default"].basename(libPath);
libArgs += " -l ".concat(libName, ":").concat(exports.DEFAULT_APP_PATH, "/").concat(libFilename);
});
displaySetting = '--display headless';
displayEnvironment = '';
// Disable X11 in CI
if (!('CI' in process.env) || process.env.CI === 'false') {
if ('X11' in options && options.X11) {
displaySetting = '';
dirBindings.push('/tmp/.X11-unix:/tmp/.X11-unix:ro');
}
displayEnvironment = process.env.DISPLAY ? process.env.DISPLAY : displayEnvironment;
if (process.platform === 'darwin') {
displayEnvironment = 'host.docker.internal:0';
}
}
modelOptions = (options === null || options === void 0 ? void 0 : options.model) ? options.model : 'nanos';

@@ -195,3 +165,3 @@ if (modelOptions === 'nanosp')

}
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);
command = "/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN --display headless ".concat(customOptions, " -m ").concat(modelOptions, " ").concat(sdkOption, " ").concat(exports.DEFAULT_APP_PATH, "/").concat(appFilename, " ").concat(libArgs);
this.log("[ZEMU] Command: ".concat(command));

@@ -205,8 +175,3 @@ portBindings = (_b = {},

}
environment = [
"SCP_PRIVKEY=".concat(exports.DEV_CERT_PRIVATE_KEY),
"BOLOS_SDK=".concat(exports.BOLOS_SDK),
"BOLOS_ENV=/opt/bolos",
"DISPLAY=".concat(displayEnvironment),
];
environment = ["SCP_PRIVKEY=".concat(exports.DEV_CERT_PRIVATE_KEY), "BOLOS_SDK=".concat(exports.BOLOS_SDK), "BOLOS_ENV=/opt/bolos", "DISPLAY=''"];
this.log("[ZEMU] Creating Container ".concat(this.image, " - ").concat(this.name, " "));

@@ -218,3 +183,2 @@ _a = this;

Tty: true,
Privileged: true,
AttachStdout: true,

@@ -239,3 +203,3 @@ AttachStderr: true,

}
return [4 /*yield*/, this.currentContainer.start({})];
return [4 /*yield*/, this.currentContainer.start()];
case 2:

@@ -251,5 +215,5 @@ _c.sent();

return __awaiter(this, void 0, void 0, function () {
var container, e_1, _a;
return __generator(this, function (_b) {
switch (_b.label) {
var container, e_1, err_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:

@@ -260,28 +224,29 @@ if (!this.currentContainer) return [3 /*break*/, 9];

this.log("[ZEMU] Stopping container");
_b.label = 1;
_a.label = 1;
case 1:
_b.trys.push([1, 3, , 4]);
_a.trys.push([1, 3, , 4]);
return [4 /*yield*/, container.stop({ t: 0 })];
case 2:
_b.sent();
_a.sent();
return [3 /*break*/, 4];
case 3:
e_1 = _b.sent();
e_1 = _a.sent();
this.log("[ZEMU] Stopping: ".concat(e_1));
return [3 /*break*/, 4];
throw e_1;
case 4:
this.log("[ZEMU] Stopped");
_b.label = 5;
_a.label = 5;
case 5:
_b.trys.push([5, 7, , 8]);
_a.trys.push([5, 7, , 8]);
return [4 /*yield*/, container.remove()];
case 6:
_b.sent();
_a.sent();
return [3 /*break*/, 8];
case 7:
_a = _b.sent();
return [3 /*break*/, 8];
err_1 = _a.sent();
this.log('[ZEMU] Unable to remove container');
throw err_1;
case 8:
this.log("[ZEMU] Removed");
_b.label = 9;
_a.label = 9;
case 9: return [2 /*return*/];

@@ -288,0 +253,0 @@ }

@@ -1,2 +0,1 @@

/// <reference types="node" />
/** ******************************************************************************

@@ -17,26 +16,12 @@ * (c) 2020 Zondax GmbH

******************************************************************************* */
/// <reference types="node" />
import PNG from 'pngjs';
import Transport from '@ledgerhq/hw-transport';
export declare const DEFAULT_START_OPTIONS: {
model: string;
sdk: string;
export declare const DEFAULT_START_OPTIONS: StartOptions;
export declare class StartOptions {
logging: boolean;
X11: boolean;
startDelay: number;
custom: string;
startDelay: number;
pressDelay: number;
startText: string;
caseSensitive: boolean;
startTimeout: number;
};
export declare class StartOptions {
model: string;
sdk: string;
logging: boolean;
/**
* @deprecated [ZEMU] X11 support is deprecated and not supported anymore
*/
X11: boolean;
custom: string;
startDelay: number;
startText: string;

@@ -75,3 +60,3 @@ caseSensitive: boolean;

static delay(v?: number): void;
static sleep(ms: number): Promise<unknown>;
static sleep(ms: number): Promise<void>;
static delayedPromise(p: any, delay: number): Promise<void>;

@@ -78,0 +63,0 @@ static stopAllEmuContainers(): Promise<void>;

"use strict";
/** ******************************************************************************
* (c) 2020 Zondax GmbH
*
* 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.
******************************************************************************* */
var __assign = (this && this.__assign) || function () {

@@ -54,30 +69,15 @@ __assign = Object.assign || function(t) {

exports.DeviceModel = exports.StartOptions = exports.DEFAULT_START_OPTIONS = void 0;
/** ******************************************************************************
* (c) 2020 Zondax GmbH
*
* 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.
******************************************************************************* */
var pngjs_1 = __importDefault(require("pngjs"));
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 axios_1 = __importDefault(require("axios"));
var axios_retry_1 = __importDefault(require("axios-retry"));
var hw_transport_http_1 = __importDefault(require("@ledgerhq/hw-transport-http"));
// @ts-ignore
var pngjs_1 = __importDefault(require("pngjs"));
var HttpTransport_1 = __importDefault(require("@ledgerhq/hw-transport-http/lib/HttpTransport"));
// @ts-expect-error
var elfy_1 = __importDefault(require("elfy"));
var grpc_1 = __importDefault(require("./grpc"));
var path_1 = require("path");
var randomstring_1 = __importDefault(require("randomstring"));
var constants_1 = require("./constants");
var emulator_1 = __importDefault(require("./emulator"));
var Resolve = require('path').resolve;
var rndstr = require('randomstring');
var grpc_1 = __importDefault(require("./grpc"));
exports.DEFAULT_START_OPTIONS = {

@@ -87,6 +87,4 @@ model: constants_1.DEFAULT_MODEL,

logging: false,
X11: false,
custom: '',
startDelay: constants_1.DEFAULT_START_DELAY,
pressDelay: constants_1.DEFAULT_KEY_DELAY,
startText: 'Ready',

@@ -98,11 +96,7 @@ caseSensitive: false,

function StartOptions() {
this.model = 'nanos';
this.sdk = '';
this.logging = false;
/**
* @deprecated [ZEMU] X11 support is deprecated and not supported anymore
*/
this.X11 = false;
this.startDelay = constants_1.DEFAULT_START_DELAY;
this.custom = '';
this.startDelay = constants_1.DEFAULT_START_DELAY;
this.model = constants_1.DEFAULT_MODEL;
this.sdk = '';
this.startText = 'Ready';

@@ -143,3 +137,3 @@ this.caseSensitive = false;

});
this.containerName = constants_1.BASE_NAME + rndstr.generate();
this.containerName = constants_1.BASE_NAME + randomstring_1["default"].generate(8); // generate 8 chars long string
this.emuContainer = new emulator_1["default"](this.elfPath, this.libElfs, constants_1.DEFAULT_EMU_IMG, this.containerName);

@@ -229,9 +223,6 @@ }

case 1:
_c.trys.push([1, 8, , 9]);
return [4 /*yield*/, Zemu.stopAllEmuContainers()];
_c.trys.push([1, 7, , 8]);
return [4 /*yield*/, this.assignPortsToListen()];
case 2:
_c.sent();
return [4 /*yield*/, this.assignPortsToListen()];
case 3:
_c.sent();
if (!this.transportPort || !this.speculosApiPort) {

@@ -245,3 +236,3 @@ e = new Error("The Speculos API port or/and transport port couldn't be reserved");

return [4 /*yield*/, this.emuContainer.runContainer(__assign(__assign({}, this.startOptions), { transportPort: this.transportPort.toString(), speculosApiPort: this.speculosApiPort.toString() }))];
case 4:
case 3:
_c.sent();

@@ -257,3 +248,3 @@ this.log("Connecting to container");

];
case 5:
case 4:
// eslint-disable-next-liwaine func-names

@@ -264,3 +255,3 @@ _c.sent();

return [4 /*yield*/, this.waitForText(this.startOptions.startText, this.startOptions.startTimeout, this.startOptions.caseSensitive)];
case 6:
case 5:
_c.sent();

@@ -270,10 +261,10 @@ this.log("Get initial snapshot");

return [4 /*yield*/, this.snapshot()];
case 6:
_b.mainMenuSnapshot = _c.sent();
return [3 /*break*/, 8];
case 7:
_b.mainMenuSnapshot = _c.sent();
return [3 /*break*/, 9];
case 8:
e_1 = _c.sent();
this.log("[ZEMU] ".concat(e_1));
throw e_1;
case 9: return [2 /*return*/];
case 8: return [2 /*return*/];
}

@@ -286,40 +277,25 @@ });

return __awaiter(this, void 0, void 0, function () {
var transport_url, start, connected, maxWait, currentTime, elapsed, _c, e_2;
return __generator(this, function (_d) {
switch (_d.label) {
case 0:
transport_url = "".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(100);
_d.label = 2;
case 2:
_d.trys.push([2, 4, , 5]);
// Here it should be "StaticTransport" type, in order to be able to use the static method "open". That method belongs to StaticTransport
// https://github.com/LedgerHQ/ledgerjs/blob/0ec9a60fe57d75dff26a69c213fd824aa321231c/packages/hw-transport-http/src/withStaticURLs.ts#L89
_c = this;
return [4 /*yield*/, (0, hw_transport_http_1["default"])(transport_url).open(transport_url)];
case 3:
// Here it should be "StaticTransport" type, in order to be able to use the static method "open". That method belongs to StaticTransport
// https://github.com/LedgerHQ/ledgerjs/blob/0ec9a60fe57d75dff26a69c213fd824aa321231c/packages/hw-transport-http/src/withStaticURLs.ts#L89
_c.transport = _d.sent();
var transport_url, start, connected, maxWait, currentTime, elapsed;
return __generator(this, function (_c) {
transport_url = "".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;
while (!connected) {
currentTime = new Date();
elapsed = currentTime.getTime() - start.getTime();
if (elapsed > maxWait) {
throw "Timeout waiting to connect";
}
Zemu.delay(100);
try {
this.transport = new HttpTransport_1["default"](transport_url);
connected = true;
return [3 /*break*/, 5];
case 4:
e_2 = _d.sent();
this.log("WAIT ".concat(this.containerName, " ").concat(elapsed, " - ").concat(e_2, " ").concat(transport_url));
}
catch (e) {
this.log("WAIT ".concat(this.containerName, " ").concat(elapsed, " - ").concat(e, " ").concat(transport_url));
connected = false;
return [3 /*break*/, 5];
case 5: return [3 /*break*/, 1];
case 6: return [2 /*return*/];
}
}
return [2 /*return*/];
});

@@ -484,4 +460,4 @@ });

case 0:
snapshotPrefixGolden = Resolve("".concat(path, "/snapshots/").concat(testcaseName));
snapshotPrefixTmp = Resolve("".concat(path, "/snapshots-tmp/").concat(testcaseName));
snapshotPrefixGolden = (0, path_1.resolve)("".concat(path, "/snapshots/").concat(testcaseName));
snapshotPrefixTmp = (0, path_1.resolve)("".concat(path, "/snapshots-tmp/").concat(testcaseName));
if (takeSnapshots) {

@@ -547,3 +523,3 @@ fs_extra_1["default"].ensureDirSync(snapshotPrefixGolden);

case 0:
snapshotPrefixTmp = Resolve("".concat(path, "/snapshots-tmp/").concat(testcaseName));
snapshotPrefixTmp = (0, path_1.resolve)("".concat(path, "/snapshots-tmp/").concat(testcaseName));
fs_extra_1["default"].ensureDirSync(snapshotPrefixTmp);

@@ -589,4 +565,4 @@ filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, true);

return __generator(this, function (_a) {
snapshotPrefixGolden = Resolve("".concat(path, "/snapshots/").concat(testcaseName));
snapshotPrefixTmp = Resolve("".concat(path, "/snapshots-tmp/").concat(testcaseName));
snapshotPrefixGolden = (0, path_1.resolve)("".concat(path, "/snapshots/").concat(testcaseName));
snapshotPrefixTmp = (0, path_1.resolve)("".concat(path, "/snapshots-tmp/").concat(testcaseName));
this.log("golden ".concat(snapshotPrefixGolden));

@@ -648,4 +624,4 @@ this.log("tmp ".concat(snapshotPrefixTmp));

case 0:
snapshotPrefixGolden = Resolve("".concat(path, "/snapshots/").concat(testcaseName));
snapshotPrefixTmp = Resolve("".concat(path, "/snapshots-tmp/").concat(testcaseName));
snapshotPrefixGolden = (0, path_1.resolve)("".concat(path, "/snapshots/").concat(testcaseName));
snapshotPrefixTmp = (0, path_1.resolve)("".concat(path, "/snapshots-tmp/").concat(testcaseName));
if (takeSnapshots) {

@@ -652,0 +628,0 @@ fs_extra_1["default"].ensureDirSync(snapshotPrefixGolden);

# Zemu Testing Framework
:::warning Work in progress This project is under development.
![zondax](./assets/zondax_light.png)
API and usage guidelines are **very** likely to change
:::
## 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.
_Zemu is an emulation and testing framework for Ledger Nano S/X devices._
_Zemu is an emulation and testing framework for Ledger Nano S/S+/X devices_

@@ -29,1 +26,6 @@ ## Features

- Used by Zondax in multiple apps
# Who we are?
We are Zondax, a company pioneering blockchain services. If you want to know more about us, please visit us at
[zondax.ch](https://zondax.ch)

@@ -5,3 +5,3 @@ {

"license": "Apache-2.0",
"version": "0.33.2",
"version": "0.34.0-beta.1",
"description": "Zemu Testing Framework",

@@ -34,7 +34,7 @@ "main": "./dist/index.js",

"dependencies": {
"@grpc/grpc-js": "^1.5.5",
"@grpc/proto-loader": "^0.6.9",
"@ledgerhq/hw-transport": "^6.24.1",
"@ledgerhq/hw-transport-http": "^6.24.1",
"axios": "^0.27.2",
"@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",

@@ -50,21 +50,23 @@ "dockerode": "^3.3.1",

"devDependencies": {
"@types/dockerode": "^3.3.11",
"@types/fs-extra": "^9.0.12",
"@types/jest": "^28.1.1",
"@types/jest": "^29.2.0",
"@types/ledgerhq__hw-transport": "^4.21.4",
"@types/pngjs": "^6.0.1",
"@types/randomstring": "^1.1.8",
"@types/sleep": "^0.0.8",
"@typescript-eslint/eslint-plugin": "^5.11.0",
"@typescript-eslint/parser": "^5.11.0",
"@zondax/ledger-substrate": "^0.34.0",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"@zondax/ledger-substrate": "^0.39.0",
"copyfiles": "^2.4.1",
"eslint": "^8.9.0",
"eslint": "^8.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jest": "^26.1.0",
"eslint-plugin-jest": "^27.1.2",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^28.1.3",
"jest": "^29.2.0",
"js-sha512": "^0.8.0",
"prettier": "^2.5.1",
"ts-jest": "^28.0.8",
"typescript": "^4.5.5"
"ts-jest": "^29.0.3",
"typescript": "^4.8.4"
},

@@ -71,0 +73,0 @@ "moduleDirectories": [

# Zondax Zemu Testing Framework
![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)

@@ -15,7 +19,8 @@ [![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. 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. 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).
_Zemu is an emulation and testing framework for Ledger Nano S/X devices._
_Zemu is an emulation and testing framework for Ledger Nano S/S+/X devices._

@@ -25,52 +30,16 @@ ## Features

- Minimal configuration + Docker based
- Speculous/Qemu based emulation
- Speculos/Qemu based emulation
- Easy JS API
- Mocha / Jest compatible
- Parallelized testing
- Abstracted device control (buttons, reset, etc.)
- Screenshots + comparisons
- Navigate thru screens and take screenshots of them
- Debugging (support for CLion and vscode, even mixed C/Rust)
- Used by Zondax in multiple apps
## QuickStart
## Docs
`Zemu` class provides access and control to your emulated Ledger app running on a docker container.
Check our documentation and quickstart at https://docs.zondax.ch
Basic testing code:
# Who we are?
```javascript
jest.setTimeout(20000);
test("demo", async () => {
//Create Zemu object. Pass the path to your .elf file
const sim = new Zemu("/ledger-demo/app/bin/");
//Create an instance of your Ledger-js app
try {
const demoJSApp = new DemoApp(sim.getTransport());
//Start simulator. A new docker container instance will be created.
await sim.start({});
//Do your tests
...
//Finally, close the simulator. This will stop and remove the container.
} finally {
await sim.close();
}
});
```
## Basic control commands examples:\*\*
- Take a screenshot and save it: \
`await sim.snapshot("tests/snapshots/0.png")`
- Send "click left": \
`await sim.clickLeft()`
- Send "click right": \
`await sim.clickRight()`
- Send "click both": \
`await sim.clickBoth()`
- Wait some time: \
`await Zemu.sleep(500) //Time in [ms]`
We are Zondax, a company pioneering blockchain services. If you want to know more about us, please visit us at
[zondax.ch](https://zondax.ch)

@@ -7,3 +7,3 @@ export const DEFAULT_EMU_IMG = 'zondax/builder-zemu@sha256:7cae0f781ea6f6a58c39f273763bb61176b377bd0d6c713e59ae38e0531ae4ab'

export const DEFAULT_HOST = '127.0.0.1'
export const BASE_NAME = 'zemu-656d75-'
export const BASE_NAME = 'zemu-test-'
export const DEFAULT_START_TIMEOUT = 20000

@@ -10,0 +10,0 @@ export const KILL_TIMEOUT = 5000

@@ -16,4 +16,4 @@ /** ******************************************************************************

******************************************************************************* */
const path = require('path')
const Docker = require('dockerode')
import path from 'path'
import Docker, { Container, ContainerInfo } from 'dockerode'

@@ -28,8 +28,7 @@ export const DEV_CERT_PRIVATE_KEY = 'ff701d781f43ce106f72dc26a46b6a83e053b5d07bb3d4ceab79c91ca822a66b'

private readonly name: string
private startDelay: number
private readonly image: any
private libElfs: any
private currentContainer: any | null
private readonly image: string
private libElfs: { [p: string]: string }
private currentContainer: Container | undefined | null
constructor(elfLocalPath: string, libElfs: any, image: any, name: string) {
constructor(elfLocalPath: string, libElfs: { [p: string]: string }, image: string, name: string) {
// eslint-disable-next-line global-require

@@ -41,3 +40,2 @@ this.image = image

this.logging = false
this.startDelay = 200
}

@@ -47,11 +45,13 @@

const docker = new Docker()
await new Promise(resolve => {
docker.listContainers({ all: true, filters: { name: [name] } }, function (err: any, containers: any[]) {
containers.forEach(function (containerInfo) {
docker.getContainer(containerInfo.Id).remove({ force: true }, function () {
// console.log("container removed");
return new Promise<void>(resolve => {
docker.listContainers({ all: true, filters: { name: [name] } }, (containers: ContainerInfo[] | undefined) => {
if (!containers?.length) throw 'Container not found, cannot be removed'
containers.forEach(containerInfo => {
docker.getContainer(containerInfo.Id).remove({ force: true }, (err, res) => {
if (err) throw err
console.log(res)
})
})
return resolve(true)
})
resolve()
})

@@ -62,29 +62,25 @@ }

const docker = new Docker()
await new Promise(resolve => {
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`)
}
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`)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function onFinished(err: any, output: any) {
if (!err) {
resolve(true)
} else {
process.stdout.write(`[DOCKER] ${err}\n`)
process.exit(1)
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function onFinished(err: any, output: any) {
if (err) {
process.stdout.write(`[DOCKER] ${err}\n`)
throw new Error(err)
throw err
}
}
docker.modem.followProgress(stream, onFinished, onProgress)
})
if (err) {
process.stdout.write(`[DOCKER] ${err}\n`)
throw new Error(err)
}
docker.modem.followProgress(stream, onFinished, onProgress)
})

@@ -100,5 +96,3 @@ }

async runContainer(options: {
logging: any
startDelay: any
X11: boolean
logging: boolean
custom: string

@@ -110,13 +104,5 @@ model: string

}) {
if ('X11' in options && options.X11) {
this.log('[ZEMU] X11 support is deprecated and not supported anymore')
this.log('[ZEMU] automatically disabling')
options.X11 = false
}
// eslint-disable-next-line global-require
const docker = new Docker()
this.logging = options.logging
this.startDelay = options.startDelay

@@ -134,18 +120,2 @@ const appFilename = path.basename(this.elfLocalPath)

let displaySetting = '--display headless'
let displayEnvironment = ''
// Disable X11 in CI
if (!('CI' in process.env) || process.env.CI === 'false') {
if ('X11' in options && options.X11) {
displaySetting = ''
dirBindings.push('/tmp/.X11-unix:/tmp/.X11-unix:ro')
}
displayEnvironment = process.env.DISPLAY ? process.env.DISPLAY : displayEnvironment
if (process.platform === 'darwin') {
displayEnvironment = 'host.docker.internal:0'
}
}
const modelOptions = options?.model ? options.model : 'nanos'

@@ -162,3 +132,3 @@ if (modelOptions === 'nanosp') options.sdk = '1.0.3'

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 command = `/home/zondax/speculos/speculos.py --log-level speculos:DEBUG --color JADE_GREEN --display headless ${customOptions} -m ${modelOptions} ${sdkOption} ${DEFAULT_APP_PATH}/${appFilename} ${libArgs}`

@@ -176,8 +146,3 @@ this.log(`[ZEMU] Command: ${command}`)

const environment = [
`SCP_PRIVKEY=${DEV_CERT_PRIVATE_KEY}`,
`BOLOS_SDK=${BOLOS_SDK}`,
`BOLOS_ENV=/opt/bolos`,
`DISPLAY=${displayEnvironment}`, // needed if X forwarding
]
const environment = [`SCP_PRIVKEY=${DEV_CERT_PRIVATE_KEY}`, `BOLOS_SDK=${BOLOS_SDK}`, `BOLOS_ENV=/opt/bolos`, `DISPLAY=''`]

@@ -189,3 +154,2 @@ this.log(`[ZEMU] Creating Container ${this.image} - ${this.name} `)

Tty: true,
Privileged: true,
AttachStdout: true,

@@ -205,3 +169,3 @@ AttachStderr: true,

if (this.logging) {
this.currentContainer.attach({ stream: true, stdout: true, stderr: true }, function (err: any, stream: any) {
this.currentContainer.attach({ stream: true, stdout: true, stderr: true }, (err: any, stream: any) => {
stream.pipe(process.stdout)

@@ -212,3 +176,3 @@ })

await this.currentContainer.start({})
await this.currentContainer.start()

@@ -227,2 +191,3 @@ this.log(`[ZEMU] Started ${this.currentContainer.id}`)

this.log(`[ZEMU] Stopping: ${e}`)
throw e
}

@@ -232,4 +197,5 @@ this.log(`[ZEMU] Stopped`)

await container.remove()
} catch {
// eslint-disable-next-line no-empty
} catch (err) {
this.log('[ZEMU] Unable to remove container')
throw err
}

@@ -236,0 +202,0 @@ this.log(`[ZEMU] Removed`)

@@ -16,12 +16,17 @@ /** ******************************************************************************

******************************************************************************* */
import PNG from 'pngjs'
import axios from 'axios'
import axiosRetry from 'axios-retry'
import fs from 'fs-extra'
import getPort from 'get-port'
import axios from 'axios'
import axiosRetry from 'axios-retry'
import PNG from 'pngjs'
import TransportHttp from '@ledgerhq/hw-transport-http'
// @ts-ignore
import HttpTransport from '@ledgerhq/hw-transport-http/lib/HttpTransport'
import Transport from '@ledgerhq/hw-transport'
// @ts-expect-error
import elfy from 'elfy'
import GRPCRouter from './grpc'
import { resolve } from 'path'
import rndstr from 'randomstring'
import {

@@ -39,16 +44,12 @@ BASE_NAME,

} from './constants'
import EmuContainer from './emulator'
import Transport from '@ledgerhq/hw-transport'
import GRPCRouter from './grpc'
const Resolve = require('path').resolve
const rndstr = require('randomstring')
export const DEFAULT_START_OPTIONS = {
export const DEFAULT_START_OPTIONS: StartOptions = {
model: DEFAULT_MODEL,
sdk: '',
logging: false,
X11: false,
custom: '',
startDelay: DEFAULT_START_DELAY,
pressDelay: DEFAULT_KEY_DELAY,
startText: 'Ready',

@@ -60,11 +61,7 @@ caseSensitive: false,

export class StartOptions {
model = 'nanos'
sdk = ''
logging = false
/**
* @deprecated [ZEMU] X11 support is deprecated and not supported anymore
*/
X11 = false
startDelay = DEFAULT_START_DELAY
custom = ''
startDelay = DEFAULT_START_DELAY
model = DEFAULT_MODEL
sdk = ''
startText = 'Ready'

@@ -132,3 +129,3 @@ caseSensitive = false

this.containerName = BASE_NAME + rndstr.generate()
this.containerName = BASE_NAME + rndstr.generate(8) // generate 8 chars long string
this.emuContainer = new EmuContainer(this.elfPath, this.libElfs, DEFAULT_EMU_IMG, this.containerName)

@@ -147,3 +144,3 @@ }

static sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
return new Promise<void>(resolve => setTimeout(resolve, ms))
}

@@ -193,3 +190,2 @@

try {
await Zemu.stopAllEmuContainers()
await this.assignPortsToListen()

@@ -246,5 +242,3 @@

try {
// Here it should be "StaticTransport" type, in order to be able to use the static method "open". That method belongs to StaticTransport
// https://github.com/LedgerHQ/ledgerjs/blob/0ec9a60fe57d75dff26a69c213fd824aa321231c/packages/hw-transport-http/src/withStaticURLs.ts#L89
this.transport = await (TransportHttp(transport_url) as any).open(transport_url)
this.transport = new HttpTransport(transport_url)
connected = true

@@ -381,4 +375,4 @@ } catch (e) {

) {
const snapshotPrefixGolden = Resolve(`${path}/snapshots/${testcaseName}`)
const snapshotPrefixTmp = Resolve(`${path}/snapshots-tmp/${testcaseName}`)
const snapshotPrefixGolden = resolve(`${path}/snapshots/${testcaseName}`)
const snapshotPrefixTmp = resolve(`${path}/snapshots-tmp/${testcaseName}`)

@@ -422,8 +416,4 @@ if (takeSnapshots) {

async takeSnapshotAndOverwrite(
path: string,
testcaseName: string,
imageIndex: number,
) {
const snapshotPrefixTmp = Resolve(`${path}/snapshots-tmp/${testcaseName}`)
async takeSnapshotAndOverwrite(path: string, testcaseName: string, imageIndex: number) {
const snapshotPrefixTmp = resolve(`${path}/snapshots-tmp/${testcaseName}`)
fs.ensureDirSync(snapshotPrefixTmp)

@@ -435,5 +425,5 @@ const filename = this.getSnapshotPath(snapshotPrefixTmp, imageIndex, true)

fs.unlinkSync(filename)
} catch(err) {
} catch (err) {
console.log(err)
throw new Error('Snapshot does not exist');
throw new Error('Snapshot does not exist')
}

@@ -456,4 +446,4 @@ await this.snapshot(filename)

async compareSnapshots(path: string, testcaseName: string, snapshotCount: number): Promise<boolean> {
const snapshotPrefixGolden = Resolve(`${path}/snapshots/${testcaseName}`)
const snapshotPrefixTmp = Resolve(`${path}/snapshots-tmp/${testcaseName}`)
const snapshotPrefixGolden = resolve(`${path}/snapshots/${testcaseName}`)
const snapshotPrefixTmp = resolve(`${path}/snapshots-tmp/${testcaseName}`)

@@ -511,4 +501,4 @@ this.log(`golden ${snapshotPrefixGolden}`)

): Promise<number> {
const snapshotPrefixGolden = Resolve(`${path}/snapshots/${testcaseName}`)
const snapshotPrefixTmp = Resolve(`${path}/snapshots-tmp/${testcaseName}`)
const snapshotPrefixGolden = resolve(`${path}/snapshots/${testcaseName}`)
const snapshotPrefixTmp = resolve(`${path}/snapshots-tmp/${testcaseName}`)

@@ -515,0 +505,0 @@ if (takeSnapshots) {

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