From 5327de4eb5f51f7faca79e75c58073e78f472039 Mon Sep 17 00:00:00 2001 From: Baha Date: Thu, 17 Mar 2022 19:20:10 +0300 Subject: [PATCH] feat trezor connect v8 upgrade (#670) * feat: trezor-connect upgrade to v8, WIP * fix: voting trezor integration version upgrade & refactor * fix: trezorAccount serialize account parameter hdKeypath fix * fix: voting transactions fee flooring issue fixed * fix: Trezor T network -104 value should be 152, fixed * fix: remove nem-trezor dependency since not needed anymore * fix: trezor result error field fixed * fix: removed unused parameter receiverPublicKey and the encryption logic * fix: importance transfer tx trezor compatibility issue fixed * fix: failing unit tests fixed, trezor unit tests added, pr feedbacks * fix: trezor account network param passed, trezor spec refactored Co-authored-by: Baha --- package-lock.json | 85 +- package.json | 6 +- src/app/modules/trezor/trezor.service.js | 94 +- src/app/modules/trezor/trezorAccount.js | 41 + src/app/services/voting.service.js | 13 +- src/start.html | 3 +- src/vendors/connect.js | 1022 ----------------- tests/specs/ImportanceTransferModule.spec.js | 4 +- tests/specs/ledger.spec.js | 16 +- tests/specs/transferTransactionModule.spec.js | 3 +- tests/specs/trezor.spec.js | 66 ++ 11 files changed, 256 insertions(+), 1097 deletions(-) create mode 100644 src/app/modules/trezor/trezorAccount.js delete mode 100644 src/vendors/connect.js create mode 100644 tests/specs/trezor.spec.js diff --git a/package-lock.json b/package-lock.json index 1f2525d9..61702ede 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "NEM-Wallet", - "version": "2.6.0", + "version": "2.6.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4732,6 +4732,24 @@ "sha.js": "^2.4.8" } }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "requires": { + "node-fetch": "2.6.7" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + } + } + }, "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", @@ -12467,17 +12485,6 @@ } } }, - "nem-trezor": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/nem-trezor/-/nem-trezor-1.1.0.tgz", - "integrity": "sha512-l4L2XtJlQudTW0Ov+YiRB50w7eYUY+d1G1LfoHnpb663wvoKgurvtNW4ue30mVv5aFwVjlaF/h9qlwnWBRHURw==", - "dev": true, - "requires": { - "crypto-js": "^3.1.9-1", - "nem-library": "1.0.5", - "trezor-connect": "^6.0.2" - } - }, "nem-voting": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/nem-voting/-/nem-voting-2.2.9.tgz", @@ -14925,8 +14932,7 @@ "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" }, "regenerator-transform": { "version": "0.10.1", @@ -17821,6 +17827,11 @@ "punycode": "^1.4.1" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, "treeify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", @@ -17828,14 +17839,28 @@ "dev": true }, "trezor-connect": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/trezor-connect/-/trezor-connect-6.0.5.tgz", - "integrity": "sha512-U5D8bEkGCBFCwT3zg4WAQUUBfkG9sDhumTY2Mgj2VN+TrRwkrP7nFyK4d0XRzt4ZsLJxBAT/Qg4/ZKqKh0a+wg==", - "dev": true, + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/trezor-connect/-/trezor-connect-8.2.7.tgz", + "integrity": "sha512-SoRDqZoTLb7W0nk7B9OimRoUCGRUc6htEJrHcB0nbC1Fs6Uw5lxCGn/agYCbqgX3oiWs2MIu0UMt+1Ky634Enw==", "requires": { - "babel-runtime": "^6.26.0", - "events": "^1.1.1", - "whatwg-fetch": "^2.0.4" + "@babel/runtime": "^7.15.4", + "cross-fetch": "^3.1.5", + "events": "^3.3.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", + "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + } } }, "trim-newlines": { @@ -18688,6 +18713,11 @@ } } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, "websocket": { "version": "1.0.33", "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.33.tgz", @@ -18719,11 +18749,14 @@ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, - "whatwg-fetch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", - "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==", - "dev": true + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } }, "which": { "version": "1.3.1", diff --git a/package.json b/package.json index d99113f8..a8396d51 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "author": "https://github.com/QuantumMechanics ", "license": "MIT", "dependencies": { - "@ledgerhq/hw-transport-node-hid-noevents": "^5.43.0" + "@ledgerhq/hw-transport-node-hid-noevents": "^5.43.0", + "trezor-connect": "^8.2.7" }, "devDependencies": { "@babel/runtime": "^7.4.2", @@ -28,7 +29,6 @@ "browserify": "^13.0.0", "browserify-ngannotate": "^2.0.0", "bufferutil": "^4.0.1", - "symbol-post-launch-optin-module": "1.0.1", "chart.js": "2.1.6", "crypto-js": "^3.1.9-1", "electron": "11.1.0", @@ -52,7 +52,6 @@ "merge-stream": "^1.0.0", "nem-library": "1.0.5", "nem-sdk": "^1.6.8", - "nem-trezor": "1.1.0", "nem-voting": "^2.2.9", "ng-toast": "2.0.0", "ngstorage": "0.3.10", @@ -62,6 +61,7 @@ "rxjs": "5.5.12", "symbol-hd-wallets": "0.12.0", "symbol-paper-wallets": "1.0.2-optin", + "symbol-post-launch-optin-module": "1.0.1", "symbol-qr-library": "^0.9.0", "symbol-sdk": "1.0.0", "url-parse": "1.1.9", diff --git a/src/app/modules/trezor/trezor.service.js b/src/app/modules/trezor/trezor.service.js index 2fddc494..e78bc0e1 100644 --- a/src/app/modules/trezor/trezor.service.js +++ b/src/app/modules/trezor/trezor.service.js @@ -1,4 +1,6 @@ +import { TransactionTypes } from 'nem-library'; import nem from 'nem-sdk'; +import TrezorConnect from 'trezor-connect'; /** Service storing Trezor utility functions. */ class Trezor { @@ -18,6 +20,7 @@ class Trezor { // Service properties region // // End properties region // + TrezorConnect.manifest({ email: 'maintainers@nem.io', appUrl: 'https://www.nem.io' }); } // Service methods region // @@ -37,27 +40,34 @@ class Trezor { return `m/44'/${coinType}'/${index}'/0'/0'`; } + adjustNetwork(network) { + return network < 0 ? 256 + network : network; + } + createAccount(network, index, label) { return new Promise((resolve, reject) => { const hdKeypath = this.bip44(network, index); - - TrezorConnect.nemGetAddress(hdKeypath, network, (result) => { + TrezorConnect.nemGetAddress({ + path: hdKeypath, + network: this.adjustNetwork(network), + showOnTrezor: true + }).then(function(result) { if (result.success) { resolve({ "brain": false, "algo": "trezor", "encrypted": "", "iv": "", - "address": result.address, + "address": result.payload.address, "label": label, "network": network, "child": "", "hdKeypath": hdKeypath }); } else { - reject(result.error); + reject(result.payload.error); } - }); + }) }); } @@ -66,9 +76,16 @@ class Trezor { const value = "0000000000000000000000000000000000000000000000000000000000000000"; return new Promise((resolve, reject) => { - TrezorConnect.cipherKeyValue(account.hdKeypath, key, value, true, true, true, (result) => { + TrezorConnect.cipherKeyValue({ + path: account.hdKeypath, + key: key, + value: value, + encrypt: true, + askOnEncrypt: true, + askOnDecrypt: true + }).then(function(result) { if (result.success) { - const privateKey = nem.utils.helpers.fixPrivateKey(result.value); + const privateKey = nem.utils.helpers.fixPrivateKey(result.payload.value); const keyPair = nem.crypto.keyPair.create(privateKey); const publicKey = keyPair.publicKey.toString(); const address = nem.model.address.toAddress(publicKey, network); @@ -79,38 +96,65 @@ class Trezor { publicKey }); } else { - reject(result.error); + reject(result.payload.error); } - }); + }) }); } + /** + * Adjusts importance transfer transaction to be compatible with Trezor + * + * @param {Object} transaction - Transaction to adjust + * @returns the adjusted transaction or the same transaction (if not an importance transfer) + */ + adjustImportanceTransferTransaction(transaction) { + if (transaction.type === TransactionTypes.IMPORTANCE_TRANSFER) { + Object.assign(transaction, { + importanceTransfer: { + mode: transaction.mode, + publicKey: transaction.remoteAccount, + }, + }); + } + return transaction; + } + serialize(transaction, account) { + const tx = this.adjustImportanceTransferTransaction(transaction); + return new Promise((resolve, reject) => { - TrezorConnect.nemSignTx(account.hdKeypath, transaction, (result) => { - if (result.success) { - resolve(result.message); - } else { - reject({ - "code": 0, - "data": { - "message": result.error - } - }); - } - }); + TrezorConnect.nemSignTransaction({ + path: account.hdKeypath, + transaction: tx, + }).then(function (result) { + if (result.success) { + resolve(result.payload); + } else { + reject({ + code: 0, + data: { + message: result.payload.error, + }, + }); + } + }); }); } showAccount(account) { return new Promise((resolve, reject) => { - TrezorConnect.nemGetAddress(account.hdKeypath, account.network, (result) => { + TrezorConnect.nemGetAddress({ + path: account.hdKeypath, + network: this.adjustNetwork(account.network), + showOnTrezor: true + }).then(function(result) { if (result.success) { - resolve(result.address); + resolve(result.payload.address); } else { - reject(result.error); + reject(result.payload.error); } - }); + }) }); } diff --git a/src/app/modules/trezor/trezorAccount.js b/src/app/modules/trezor/trezorAccount.js new file mode 100644 index 00000000..e18f3e6a --- /dev/null +++ b/src/app/modules/trezor/trezorAccount.js @@ -0,0 +1,41 @@ +import { Observable } from "rxjs"; + +/** + * TrezorAccount model + */ +export class TrezorAccount { + + /** + * Constructor + * @param address + * @param hdKeyPath + * @param network + * @param Trezor trezor service + */ + constructor(address, hdKeyPath, network, Trezor) { + this.address = address; + this.hdKeyPath = hdKeyPath; + this.network = network; + this._Trezor = Trezor; + } + + /** + * Sign a transaction + * @param transaction + * @returns {Observable<{data: any, signature: string}>} + */ + signTransaction(transaction) { + transaction.setNetworkType(this.network); + return Observable.fromPromise(this._Trezor.serialize(transaction.toDTO(), { hdKeypath: this.hdKeyPath })); + } + + /** + * Sign multiple transactions + * + * @param transactions + * @returns {Observable} Observable of signed transactions + */ + signTransactions(transactions) { + return Observable.merge(...(transactions.map(t => this.signTransaction(t)))).toArray(); + } +} diff --git a/src/app/services/voting.service.js b/src/app/services/voting.service.js index 90910f58..1dc5a2d5 100644 --- a/src/app/services/voting.service.js +++ b/src/app/services/voting.service.js @@ -1,5 +1,5 @@ import {Observable} from 'rxjs'; -import {TrezorAccount} from 'nem-trezor'; +import {TrezorAccount} from '../modules/trezor/trezorAccount'; import nemsdk from 'nem-sdk'; const nem = require('nem-library'); const voting = require('nem-voting'); @@ -127,7 +127,7 @@ class Voting { let account = {}; if (common.isHW) { if (this._Wallet.algo == "trezor") { - account = new TrezorAccount(this._Wallet.currentAccount.address, this._Wallet.currentAccount.hdKeypath); + account = new TrezorAccount(this._Wallet.currentAccount.address, this._Wallet.currentAccount.hdKeypath, nem.NEMLibrary.getNetworkType(), this._Trezor); } } else { account = nem.Account.createWithPrivateKey(common.privateKey); @@ -138,6 +138,7 @@ class Voting { const broadcastData = poll.broadcast(account.publicKey, index); broadcastData.transactions = broadcastData.transactions.map((t) => { t.timeWindow = nem.TimeWindow.createFromDTOInfo(timeStamp, deadline); + t.fee = Math.floor(t.fee); return t; }); const nodeSplit = this._Wallet.node.host.split("://"); @@ -156,7 +157,6 @@ class Voting { if (this._Wallet.algo == "trezor") { if (window['isElectronEnvironment']) { const transaction = broadcastData.transactions[i]; - transaction.signer = nem.PublicAccount.createWithPublicKey("462ee976890916e54fa825d26bdd0235f5eb5b6a143c199ab0ae5ee9328e08ce"); transaction.setNetworkType(nem.NEMLibrary.getNetworkType()); const dto = transaction.toDTO(); p = this._Trezor.serialize(dto, this._Wallet.currentAccount); @@ -165,7 +165,6 @@ class Voting { } } else if (this._Wallet.algo == "ledger") { const transaction = broadcastData.transactions[i]; - transaction.signer = nem.PublicAccount.createWithPublicKey("462ee976890916e54fa825d26bdd0235f5eb5b6a143c199ab0ae5ee9328e08ce"); transaction.setNetworkType(nem.NEMLibrary.getNetworkType()); const dto = transaction.toDTO(); p = this._Ledger.serialize(dto, this._Wallet.currentAccount); @@ -349,7 +348,7 @@ class Voting { let account; if (common.isHW) { if (this._Wallet.algo == "trezor") { - account = new TrezorAccount(this._Wallet.currentAccount.address, this._Wallet.currentAccount.hdKeypath); + account = new TrezorAccount(this._Wallet.currentAccount.address, this._Wallet.currentAccount.hdKeypath, nem.NEMLibrary.getNetworkType(), this._Trezor); } } else { account = nem.Account.createWithPrivateKey(common.privateKey); @@ -361,7 +360,6 @@ class Voting { if (window['isElectronEnvironment']) { const signTransaction = (i) => { const transaction = votes[i]; - transaction.signer = nem.PublicAccount.createWithPublicKey("462ee976890916e54fa825d26bdd0235f5eb5b6a143c199ab0ae5ee9328e08ce"); transaction.setNetworkType(nem.NEMLibrary.getNetworkType()); const dto = transaction.toDTO(); const p = this._Trezor.serialize(dto, this._Wallet.currentAccount); @@ -378,12 +376,11 @@ class Voting { } signedTransactionsPromise = signTransaction(0); } else { - signedTransactionsPromise = account.signSerialTransactions(votes).first().toPromise(); + signedTransactionsPromise = account.signTransactions(votes).first().toPromise(); } } else if (this._Wallet.algo == "ledger") { const signTransaction = (i) => { const transaction = votes[i]; - transaction.signer = nem.PublicAccount.createWithPublicKey("462ee976890916e54fa825d26bdd0235f5eb5b6a143c199ab0ae5ee9328e08ce"); transaction.setNetworkType(nem.NEMLibrary.getNetworkType()); const dto = transaction.toDTO(); const p = this._Ledger.serialize(dto, this._Wallet.currentAccount); diff --git a/src/start.html b/src/start.html index f0bae2a7..4766fff9 100644 --- a/src/start.html +++ b/src/start.html @@ -21,8 +21,7 @@ - - + diff --git a/src/vendors/connect.js b/src/vendors/connect.js deleted file mode 100644 index 16b34073..00000000 --- a/src/vendors/connect.js +++ /dev/null @@ -1,1022 +0,0 @@ -/** - * (C) 2017 SatoshiLabs - * - * GPLv3 - */ -var VERSION = 4; - -if (!Array.isArray) { - Array.isArray = function(arg) { - return Object.prototype.toString.call(arg) === '[object Array]'; - }; -} - -var HD_HARDENED = 0x80000000; - -// react sometimes adds some other parameters that should not be there -function _fwStrFix(obj, fw) { - if (typeof fw === 'string') { - obj.requiredFirmware = fw; - } - return obj; -} - -this.TrezorConnect = (function () { - 'use strict'; - - var chrome = window.chrome; - var IS_CHROME_APP = chrome && chrome.app && chrome.app.window; - - var ERR_TIMED_OUT = 'Loading timed out'; - var ERR_WINDOW_CLOSED = 'Window closed'; - var ERR_WINDOW_BLOCKED = 'Window blocked'; - var ERR_ALREADY_WAITING = 'Already waiting for a response'; - var ERR_CHROME_NOT_CONNECTED = 'Internal Chrome popup is not responding.'; - - var DISABLE_LOGIN_BUTTONS = window.TREZOR_DISABLE_LOGIN_BUTTONS || false; - var CHROME_URL = window.TREZOR_CHROME_URL || './chrome/wrapper.html'; - var POPUP_ORIGIN = window.TREZOR_POPUP_ORIGIN || 'https://connect.trezor.io'; - var POPUP_PATH = window.TREZOR_POPUP_PATH || POPUP_ORIGIN + '/' + VERSION; - if (window.location.hostname === 'localhost' && !window.TREZOR_POPUP_ORIGIN) { - // development settings - POPUP_ORIGIN = window.location.origin; - POPUP_PATH = POPUP_ORIGIN; - } - var POPUP_URL = window.TREZOR_POPUP_URL || POPUP_PATH + '/popup/popup.html'; - - var POPUP_INIT_TIMEOUT = 15000; - - /** - * Public API. - */ - function TrezorConnect() { - - var manager = new PopupManager(); - - /** - * Popup errors. - */ - this.ERR_TIMED_OUT = ERR_TIMED_OUT; - this.ERR_WINDOW_CLOSED = ERR_WINDOW_CLOSED; - this.ERR_WINDOW_BLOCKED = ERR_WINDOW_BLOCKED; - this.ERR_ALREADY_WAITING = ERR_ALREADY_WAITING; - this.ERR_CHROME_NOT_CONNECTED = ERR_CHROME_NOT_CONNECTED; - - /** - * Open the popup for further communication. All API functions open the - * popup automatically, but if you need to generate some parameters - * asynchronously, use `open` first to avoid popup blockers. - * @param {function(?Error)} callback - */ - this.open = function (callback) { - var onchannel = function (result) { - if (result instanceof Error) { - callback(result); - } else { - callback(); - } - }; - manager.waitForChannel(onchannel); - }; - - /** - * Close the opened popup, if any. - */ - this.close = function () { manager.close(); }; - - /** - * Enable or disable closing the opened popup after a successful call. - * @param {boolean} value - */ - this.closeAfterSuccess = function (value) { manager.closeAfterSuccess = value; }; - - /** - * Enable or disable closing the opened popup after a failed call. - * @param {boolean} value - */ - this.closeAfterFailure = function (value) { manager.closeAfterFailure = value; }; - - /** - * Set bitcore server - * @param {string|Array} value - */ - this.setBitcoreURLS = function(value) { - if (typeof value === 'string') { - manager.bitcoreURLS = [ value ]; - }else if (value instanceof Array) { - manager.bitcoreURLS = value; - } - } - - /** - * Set currency. Human readable coin name - * @param {string|Array} value - */ - this.setCurrency = function(value) { - if (typeof value === 'string') { - manager.currency = value; - } - } - - /** - * Set currency units (mBTC, BTC) - * @param {string|Array} value - */ - this.setCurrencyUnits = function(value) { - if (typeof value === 'string') { - manager.currencyUnits = value; - } - } - - /** - * Set coin info json url - * @param {string|Array} value - */ - this.setCoinInfoURL = function(value) { - if (typeof value === 'string') { - manager.coinInfoURL = value; - } - } - - /** - * Set max. limit for account discovery - * @param {number} value - */ - this.setAccountDiscoveryLimit = function(value) { - if(!isNaN(value)) - manager.accountDiscoveryLimit = value; - } - - /** - * Set max. gap for account discovery - * @param {number} value - */ - this.setAccountDiscoveryGapLength = function(value) { - if(!isNaN(value)) - manager.accountDiscoveryGapLength = value; - } - - /** - * Set discovery BIP44 coin type - * @param {number} value - */ - this.setAccountDiscoveryBip44CoinType = function(value) { - if(!isNaN(value)) - manager.accountDiscoveryBip44CoinType = value; - } - - /** - * @typedef XPubKeyResult - * @param {boolean} success - * @param {?string} error - * @param {?string} xpubkey serialized extended public key - * @param {?string} path BIP32 serializd path of the key - */ - - /** - * Load BIP32 extended public key by path. - * - * Path can be specified either in the string form ("m/44'/1/0") or as - * raw integer array. In case you omit the path, user is asked to select - * a BIP32 account to export, and the result contains m/44'/0'/x' node - * of the account. - * - * @param {?(string|array)} path - * @param {function(XPubKeyResult)} callback - * @param {?(string|array)} requiredFirmware - */ - this.getXPubKey = function (path, callback, requiredFirmware) { - if (typeof path === 'string') { - path = parseHDPath(path); - } - manager.sendWithChannel(_fwStrFix({ - type: 'xpubkey', - path: path - }, requiredFirmware), callback); - }; - - this.getFreshAddress = function (callback, requiredFirmware) { - var wrapperCallback = function (result) { - if (result.success) { - callback({success: true, address: result.freshAddress}); - } else { - callback(result); - } - } - - manager.sendWithChannel(_fwStrFix({ - type: 'accountinfo' - }, requiredFirmware), wrapperCallback); - } - - this.getAccountInfo = function (input, callback, requiredFirmware) { - try { - manager.sendWithChannel(_fwStrFix({ - type: 'accountinfo', - description: input - }, requiredFirmware), callback); - } catch(e) { - callback({success: false, error: e}); - } - } - - this.getAllAccountsInfo = function(callback, requiredFirmware){ - try { - manager.sendWithChannel(_fwStrFix({ - type: 'allaccountsinfo', - description: 'all' - }, requiredFirmware), callback); - } catch(e) { - callback({success: false, error: e}); - } - } - - this.getBalance = function (callback, requiredFirmware) { - manager.sendWithChannel(_fwStrFix({ - type: 'accountinfo' - }, requiredFirmware), callback) - } - - /** - * @typedef SignTxResult - * @param {boolean} success - * @param {?string} error - * @param {?string} serialized_tx serialized tx, in hex, including signatures - * @param {?array} signatures array of input signatures, in hex - */ - - /** - * Sign a transaction in the device and return both serialized - * transaction and the signatures. - * - * @param {array} inputs - * @param {array} outputs - * @param {function(SignTxResult)} callback - * @param {?(string|array)} requiredFirmware - * - * @see https://github.com/trezor/trezor-common/blob/master/protob/types.proto - */ - this.signTx = function (inputs, outputs, callback, requiredFirmware, coin) { - manager.sendWithChannel(_fwStrFix({ - type: 'signtx', - inputs: inputs, - outputs: outputs, - coin: coin - }, requiredFirmware), callback); - }; - - // new implementation with ethereum at beginnig - this.ethereumSignTx = function() { - this.signEthereumTx.apply(this, arguments); - } - - // old fallback - this.signEthereumTx = function ( - address_n, - nonce, - gas_price, - gas_limit, - to, - value, - data, - chain_id, - callback, - requiredFirmware - ) { - if (requiredFirmware == null) { - requiredFirmware = '1.4.0'; // first firmware that supports ethereum - } - if (typeof address_n === 'string') { - address_n = parseHDPath(address_n); - } - manager.sendWithChannel(_fwStrFix({ - type: 'signethtx', - address_n: address_n, - nonce: nonce, - gas_price: gas_price, - gas_limit: gas_limit, - to: to, - value: value, - data: data, - chain_id: chain_id, - }, requiredFirmware), callback); - }; - - /** - * @typedef TxRecipient - * @param {number} amount the amount to send, in satoshis - * @param {string} address the address of the recipient - */ - - /** - * Compose a transaction by doing BIP-0044 discovery, letting the user - * select an account, and picking UTXO by internal preferences. - * Transaction is then signed and returned in the same format as - * `signTx`. Only supports BIP-0044 accounts (single-signature). - * - * @param {array} recipients - * @param {function(SignTxResult)} callback - * @param {?(string|array)} requiredFirmware - */ - this.composeAndSignTx = function (recipients, callback, requiredFirmware) { - manager.sendWithChannel(_fwStrFix({ - type: 'composetx', - recipients: recipients - }, requiredFirmware), callback); - }; - - /** - * @typedef RequestLoginResult - * @param {boolean} success - * @param {?string} error - * @param {?string} public_key public key used for signing, in hex - * @param {?string} signature signature, in hex - */ - - /** - * Sign a login challenge for active origin. - * - * @param {?string} hosticon - * @param {string} challenge_hidden - * @param {string} challenge_visual - * @param {string|function(RequestLoginResult)} callback - * @param {?(string|array)} requiredFirmware - * - * @see https://github.com/trezor/trezor-common/blob/master/protob/messages.proto - */ - this.requestLogin = function ( - hosticon, - challenge_hidden, - challenge_visual, - callback, - requiredFirmware - ) { - if (typeof callback === 'string') { - // special case for a login through button. - // `callback` is name of global var - callback = window[callback]; - } - if (!callback) { - throw new TypeError('TrezorConnect: login callback not found'); - } - manager.sendWithChannel(_fwStrFix({ - type: 'login', - icon: hosticon, - challenge_hidden: challenge_hidden, - challenge_visual: challenge_visual - }, requiredFirmware), callback); - }; - - /** - * @typedef SignMessageResult - * @param {boolean} success - * @param {?string} error - * @param {?string} address address (in base58check) - * @param {?string} signature signature, in base64 - */ - - /** - * Sign a message - * - * @param {string|array} path - * @param {string} message to sign (ascii) - * @param {string|function(SignMessageResult)} callback - * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) - * @param {?(string|array)} requiredFirmware - * - */ - this.signMessage = function ( - path, - message, - callback, - opt_coin, - requiredFirmware - ) { - if (typeof path === 'string') { - path = parseHDPath(path); - } - if (!opt_coin) { - opt_coin = 'Bitcoin'; - } - if (!callback) { - throw new TypeError('TrezorConnect: callback not found'); - } - manager.sendWithChannel(_fwStrFix({ - type: 'signmsg', - path: path, - message: message, - coin: opt_coin, - }, requiredFirmware), callback); - }; - - /** - * Sign an Ethereum message - * - * @param {string|array} path - * @param {string} message to sign (ascii) - * @param {string|function(SignMessageResult)} callback - * @param {?(string|array)} requiredFirmware - * - */ - this.ethereumSignMessage = function ( - path, - message, - callback, - requiredFirmware - ) { - if (typeof path === 'string') { - path = parseHDPath(path); - } - if (!callback) { - throw new TypeError('TrezorConnect: callback not found'); - } - manager.sendWithChannel(_fwStrFix({ - type: 'signethmsg', - path: path, - message: message, - }, requiredFirmware), callback); - }; - - /** - * Verify message - * - * @param {string} address - * @param {string} signature (base64) - * @param {string} message (string) - * @param {string|function()} callback - * @param {?string} opt_coin - (optional) name of coin (default Bitcoin) - * @param {?(string|array)} requiredFirmware - * - */ - this.verifyMessage = function ( - address, - signature, - message, - callback, - opt_coin, - requiredFirmware - ) { - if (!opt_coin) { - opt_coin = 'Bitcoin'; - } - if (!callback) { - throw new TypeError('TrezorConnect: callback not found'); - } - manager.sendWithChannel(_fwStrFix({ - type: 'verifymsg', - address: address, - signature: signature, - message: message, - coin: {coin_name: opt_coin}, - }, requiredFirmware), callback); - }; - - /** - * Verify ethereum message - * - * @param {string} address - * @param {string} signature (base64) - * @param {string} message (string) - * @param {string|function()} callback - * @param {?(string|array)} requiredFirmware - * - */ - this.ethereumVerifyMessage = function ( - address, - signature, - message, - callback, - requiredFirmware - ) { - if (!callback) { - throw new TypeError('TrezorConnect: callback not found'); - } - manager.sendWithChannel(_fwStrFix({ - type: 'verifyethmsg', - address: address, - signature: signature, - message: message, - }, requiredFirmware), callback); - }; - - /** - * Symmetric key-value encryption - * - * @param {string|array} path - * @param {string} key to show on device display - * @param {string} value hexadecimal value, length a multiple of 16 bytes - * @param {boolean} encrypt / decrypt direction - * @param {boolean} ask_on_encrypt (should user confirm on encrypt?) - * @param {boolean} ask_on_decrypt (should user confirm on decrypt?) - * @param {string|function()} callback - * @param {?(string|array)} requiredFirmware - * - */ - this.cipherKeyValue = function ( - path, - key, - value, - encrypt, - ask_on_encrypt, - ask_on_decrypt, - callback, - requiredFirmware - ) { - if (typeof path === 'string') { - path = parseHDPath(path); - } - if (typeof value !== 'string') { - throw new TypeError('TrezorConnect: Value must be a string'); - } - if (!(/^[0-9A-Fa-f]*$/.test(value))) { - throw new TypeError('TrezorConnect: Value must be hexadecimal'); - } - if (value.length % 32 !== 0) { - // 1 byte == 2 hex strings - throw new TypeError('TrezorConnect: Value length must be multiple of 16 bytes'); - } - if (!callback) { - throw new TypeError('TrezorConnect: callback not found'); - } - manager.sendWithChannel(_fwStrFix({ - type: 'cipherkeyvalue', - path: path, - key: key, - value: value, - encrypt: !!encrypt, - ask_on_encrypt: !!ask_on_encrypt, - ask_on_decrypt: !!ask_on_decrypt - }, requiredFirmware), callback); - }; - - this.nemGetAddress = function ( - address_n, - network, - callback, - requiredFirmware - ) { - if (requiredFirmware == null) { - requiredFirmware = '1.6.0'; // first firmware that supports NEM - } - if (typeof address_n === 'string') { - address_n = parseHDPath(address_n); - } - manager.sendWithChannel(_fwStrFix({ - type: 'nemGetAddress', - address_n: address_n, - network: network, - }, requiredFirmware), callback); - } - - this.nemSignTx = function ( - address_n, - transaction, - callback, - requiredFirmware - ) { - if (requiredFirmware == null) { - requiredFirmware = '1.6.0'; // first firmware that supports NEM - } - if (typeof address_n === 'string') { - address_n = parseHDPath(address_n); - } - manager.sendWithChannel(_fwStrFix({ - type: 'nemSignTx', - address_n: address_n, - transaction: transaction - }, requiredFirmware), callback); - } - - this.pushTransaction = function ( - rawTx, - callback - ) { - if (!(/^[0-9A-Fa-f]*$/.test(rawTx))) { - throw new TypeError('TrezorConnect: Transaction must be hexadecimal'); - } - if (!callback) { - throw new TypeError('TrezorConnect: callback not found'); - } - - manager.sendWithChannel({ - type: 'pushtx', - rawTx: rawTx, - }, callback); - } - - /** - * Display address on device - * - * @param {array} address - * @param {string} coin - * @param {boolean} segwit - * @param {?(string|array)} requiredFirmware - * - */ - this.getAddress = function (address, coin, segwit, callback, requiredFirmware) { - - if (typeof address === 'string') { - address = parseHDPath(address); - } - - manager.sendWithChannel(_fwStrFix({ - type: 'getaddress', - address_n: address, - coin: coin, - segwit: segwit - }, requiredFirmware), callback); - } - - /** - * Display ethereum address on device - * - * @param {array} address - * @param {?(string|array)} requiredFirmware - * - */ - this.ethereumGetAddress = function (address, callback, requiredFirmware) { - - if (typeof address === 'string') { - address = parseHDPath(address); - } - - manager.sendWithChannel(_fwStrFix({ - type: 'ethgetaddress', - address_n: address, - }, requiredFirmware), callback); - } - - var LOGIN_CSS = - ''; - - var LOGIN_ONCLICK = - 'TrezorConnect.requestLogin(' - + "'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'" - + ')'; - - var LOGIN_HTML = - '
' - + ' ' - + ' ' - + ' @text@' - + ' ' - + ' ' - + ' What is TREZOR?' - + ' ' - + '
'; - - /** - * Find elements and replace them with login buttons. - * It's not required to use these special elements, feel free to call - * `TrezorConnect.requestLogin` directly. - */ - this.renderLoginButtons = function () { - var elements = document.getElementsByTagName('trezor:login'); - - for (var i = 0; i < elements.length; i++) { - var e = elements[i]; - var text = e.getAttribute('text') || 'Sign in with TREZOR'; - var callback = e.getAttribute('callback') || ''; - var hosticon = e.getAttribute('icon') || ''; - var challenge_hidden = e.getAttribute('challenge_hidden') || ''; - var challenge_visual = e.getAttribute('challenge_visual') || ''; - - // it's not valid to put markup into attributes, so let users - // supply a raw text and make TREZOR bold - text = text.replace('TREZOR', 'TREZOR'); - e.outerHTML = - (LOGIN_CSS + LOGIN_HTML) - .replace('@text@', text) - .replace('@callback@', callback) - .replace('@hosticon@', hosticon) - .replace('@challenge_hidden@', challenge_hidden) - .replace('@challenge_visual@', challenge_visual) - .replace('@connect_path@', POPUP_PATH); - } - }; - } - - /* - * `getXPubKey()` - */ - - function parseHDPath(string) { - return string - .toLowerCase() - .split('/') - .filter(function (p) { return p !== 'm'; }) - .map(function (p) { - var hardened = false; - if (p[p.length - 1] === "'") { - hardened = true; - p = p.substr(0, p.length - 1); - } - if (isNaN(p)) { - throw new Error('Not a valid path.'); - } - var n = parseInt(p); - if (hardened) { // hardened index - n = (n | 0x80000000) >>> 0; - } - return n; - }); - } - - /* - * Popup management - */ - - function ChromePopup(url, name, width, height) { - var left = (screen.width - width) / 2; - var top = (screen.height - height) / 2; - var opts = { - id: name, - innerBounds: { - width: width, - height: height, - left: left, - top: top - } - }; - - var closed = function () { - if (this.onclose) { - this.onclose(false); // never report as blocked - } - }.bind(this); - - var opened = function (w) { - this.window = w; - this.window.onClosed.addListener(closed); - }.bind(this); - - chrome.app.window.create(url, opts, opened); - - this.name = name; - this.window = null; - this.onclose = null; - } - - function ChromeChannel(popup, waiting) { - var port = null; - - var respond = function (data) { - if (waiting) { - var w = waiting; - waiting = null; - w(data); - } - }; - - var setup = function (p) { - if (p.name === popup.name) { - port = p; - port.onMessage.addListener(respond); - chrome.runtime.onConnect.removeListener(setup); - } - }; - - chrome.runtime.onConnect.addListener(setup); - - this.respond = respond; - - this.close = function () { - chrome.runtime.onConnect.removeListener(setup); - port.onMessage.removeListener(respond); - port.disconnect(); - port = null; - }; - - this.send = function (value, callback) { - if (waiting === null) { - waiting = callback; - - if (port) { - port.postMessage(value); - } else { - throw new Error(ERR_CHROME_NOT_CONNECTED); - } - } else { - throw new Error(ERR_ALREADY_WAITING); - } - }; - } - - function Popup(url, origin, name, width, height) { - var left = (screen.width - width) / 2; - var top = (screen.height - height) / 2; - var opts = - 'width=' + width + - ',height=' + height + - ',left=' + left + - ',top=' + top + - ',menubar=no' + - ',toolbar=no' + - ',location=no' + - ',personalbar=no' + - ',status=no'; - var w = window.open(url, name, opts); - - var interval; - var blocked = w.closed; - var iterate = function () { - if (w.closed) { - clearInterval(interval); - if (this.onclose) { - this.onclose(blocked); - } - } - }.bind(this); - interval = setInterval(iterate, 100); - - this.window = w; - this.origin = origin; - this.onclose = null; - } - - function Channel(popup, waiting) { - - var respond = function (data) { - if (waiting) { - var w = waiting; - waiting = null; - w(data); - } - }; - - var receive = function (event) { - var org1 = event.origin.match(/^.+\:\/\/[^\‌​/]+/)[0]; - var org2 = popup.origin.match(/^.+\:\/\/[^\‌​/]+/)[0]; - //if (event.source === popup.window && event.origin === popup.origin) { - if (event.source === popup.window && org1 === org2) { - respond(event.data); - } - }; - - window.addEventListener('message', receive); - - this.respond = respond; - - this.close = function () { - window.removeEventListener('message', receive); - }; - - this.send = function (value, callback) { - if (waiting === null) { - waiting = callback; - popup.window.postMessage(value, popup.origin); - } else { - throw new Error(ERR_ALREADY_WAITING); - } - }; - } - - function ConnectedChannel(p) { - - var ready = function () { - clearTimeout(this.timeout); - this.popup.onclose = null; - this.ready = true; - this.onready(); - }.bind(this); - - var closed = function (blocked) { - clearTimeout(this.timeout); - this.channel.close(); - if (blocked) { - this.onerror(new Error(ERR_WINDOW_BLOCKED)); - } else { - this.onerror(new Error(ERR_WINDOW_CLOSED)); - } - }.bind(this); - - var timedout = function () { - this.popup.onclose = null; - if (this.popup.window) { - this.popup.window.close(); - } - this.channel.close(); - this.onerror(new Error(ERR_TIMED_OUT)); - }.bind(this); - - if (IS_CHROME_APP) { - this.popup = new ChromePopup(p.chromeUrl, p.name, p.width, p.height); - this.channel = new ChromeChannel(this.popup, ready); - } else { - this.popup = new Popup(p.url, p.origin, p.name, p.width, p.height); - this.channel = new Channel(this.popup, ready); - } - - this.timeout = setTimeout(timedout, POPUP_INIT_TIMEOUT); - - this.popup.onclose = closed; - - this.ready = false; - this.onready = null; - this.onerror = null; - } - - function PopupManager() { - var cc = null; - - var closed = function () { - cc.channel.respond(new Error(ERR_WINDOW_CLOSED)); - cc.channel.close(); - cc = null; - }; - - var open = function (callback) { - cc = new ConnectedChannel({ - name: 'trezor-connect', - width: 600, - height: 500, - origin: POPUP_ORIGIN, - path: POPUP_PATH, - url: POPUP_URL, - chromeUrl: CHROME_URL - }); - cc.onready = function () { - cc.popup.onclose = closed; - callback(cc.channel); - }; - cc.onerror = function (error) { - cc = null; - callback(error); - }; - }.bind(this); - - this.closeAfterSuccess = true; - this.closeAfterFailure = true; - - this.close = function () { - if (cc && cc.popup.window) { - cc.popup.window.close(); - } - }; - - this.waitForChannel = function (callback) { - if (cc) { - if (cc.ready) { - callback(cc.channel); - } else { - callback(new Error(ERR_ALREADY_WAITING)); - } - } else { - try { - open(callback); - } catch (e) { - callback(new Error(ERR_WINDOW_BLOCKED)); - } - } - }; - - this.sendWithChannel = function (message, callback) { - message.bitcoreURLS = this.bitcoreURLS || null; - message.currency = this.currency || null; - message.currencyUnits = this.currencyUnits || null; - message.coinInfoURL = this.coinInfoURL || null; - message.accountDiscoveryLimit = this.accountDiscoveryLimit || null; - message.accountDiscoveryGapLength = this.accountDiscoveryGapLength || null; - message.accountDiscoveryBip44CoinType = this.accountDiscoveryBip44CoinType || null; - - var respond = function (response) { - var succ = response.success && this.closeAfterSuccess; - var fail = !response.success && this.closeAfterFailure; - if (succ || fail) { - this.close(); - } - callback(response); - }.bind(this); - - var onresponse = function (response) { - if (response instanceof Error) { - var error = response; - respond({ success: false, error: error.message }); - } else { - respond(response); - } - }; - - var onchannel = function (channel) { - if (channel instanceof Error) { - var error = channel; - respond({ success: false, error: error.message }); - } else { - channel.send(message, onresponse); - } - }; - - this.waitForChannel(onchannel); - }; - } - - var exports = new TrezorConnect(); - - if (!IS_CHROME_APP && !DISABLE_LOGIN_BUTTONS) { - exports.renderLoginButtons(); - } - - return exports; - -}()); diff --git a/tests/specs/ImportanceTransferModule.spec.js b/tests/specs/ImportanceTransferModule.spec.js index 1b28966c..7cc1d993 100644 --- a/tests/specs/ImportanceTransferModule.spec.js +++ b/tests/specs/ImportanceTransferModule.spec.js @@ -51,8 +51,8 @@ describe('Importance transfer module tests', function() { expect(ctrl.isCustomNode).toBe(false); expect(ctrl.customHarvestingNode).toEqual(""); expect(ctrl.harvestingNode).toEqual(Wallet.node); - expect(ctrl.noFreeSlots).toBe(true); - expect(ctrl.nodes[0]).toEqual(nem.model.objects.create("endpoint")("http://104.128.226.60", 7890)); + expect(ctrl.hasFreeSlots).toBe(false); + expect(ctrl.nodes[0]).toEqual(nem.model.objects.create("endpoint")("http://hugetestalice.nem.ninja", 7890)); expect(ctrl.showSupernodes).toBe(false); }); diff --git a/tests/specs/ledger.spec.js b/tests/specs/ledger.spec.js index daad84d4..5992ddce 100644 --- a/tests/specs/ledger.spec.js +++ b/tests/specs/ledger.spec.js @@ -14,7 +14,7 @@ describe("Ledger service", function() { it ("Has right bip44 path for testnet NetWork", function() { // GIVEN - let expectedResult = "44'/43'/152'/0'/0'"; + let expectedResult = "m/44'/1'/0'/0'/0'"; // WHEN let result = LedgerService.bip44(-104, 0); @@ -25,7 +25,7 @@ describe("Ledger service", function() { it ("Has right bip44 path for main NetWork", function() { // GIVEN - let expectedResult = "44'/43'/104'/0'/0'"; + let expectedResult = "m/44'/43'/0'/0'/0'"; // WHEN let result = LedgerService.bip44(104, 0); @@ -46,7 +46,7 @@ describe("Ledger service", function() { let result = await LedgerService.createAccount(network, index, label) // THEN - expect(LedgerService.getAccount).toHaveBeenCalledWith("44'/43'/152'/0'/0'", network, label); + expect(LedgerService.getAccount).toHaveBeenCalledWith("m/44'/1'/0'/0'/0'", network, label); expect(result).toEqual(expetedResult); done(); }); @@ -99,17 +99,19 @@ describe("Ledger service", function() { publicKey:"3e6e6cbac488b8a44bdf5abf27b9e1cc2a6f20d09d550a66b9b36f525ca222ee", $$hashKey:"object:141" }; - spyOn(LedgerService, 'signTransaction').and.returnValue(Promise.resolve('payload')); - let expetedResult = 'payload'; + spyOn(window, 'alert'); + spyOn(LedgerService, 'signTransaction').and.returnValue(Promise.resolve({ signature: 'signature' })); + spyOn(LedgerService, 'getAppVersion').and.returnValue(Promise.resolve(1)); + let expectedResult = { signature: 'signature' }; // WHEN let result = await LedgerService.serialize(transaction, account).catch(err => { - fail('Error while serializig transaction: ' + err); + fail('Error while serializing transaction: ' + err); }); // THEN expect(LedgerService.signTransaction).toHaveBeenCalled(); - expect(result).toEqual(expetedResult); + expect(result).toEqual(expectedResult); done(); }); }); diff --git a/tests/specs/transferTransactionModule.spec.js b/tests/specs/transferTransactionModule.spec.js index fbca153e..32b1a1b6 100644 --- a/tests/specs/transferTransactionModule.spec.js +++ b/tests/specs/transferTransactionModule.spec.js @@ -430,11 +430,10 @@ describe('Transfer transaction module tests', function() { scope.$digest(); // Act - ctrl.resetData(); + ctrl.init(); scope.$digest(); // Assert - expect(ctrl.formData).toEqual(nem.model.objects.get("transferTransaction")); expect(ctrl.common).toEqual(nem.model.objects.get("common")); }); diff --git a/tests/specs/trezor.spec.js b/tests/specs/trezor.spec.js new file mode 100644 index 00000000..97ccf9cf --- /dev/null +++ b/tests/specs/trezor.spec.js @@ -0,0 +1,66 @@ +import { fail } from 'assert'; +import { TrezorAccount } from '../../src/app/modules/trezor/trezorAccount'; + +describe('Trezor integration', function () { + const transaction = { + type: 257, + version: -1744830463, + timeStamp: 130922553, + deadline: 130926153, + recipient: 'TA545ICAVNEUDFUBIHO3CEJBSVIZ7YYHFFX5LQPT', + amount: 1000000, + fee: 100000, + message: { + type: 1, + payload: '616263', + }, + mosaics: null, + setNetworkType: (network) => { + this.network = network; + }, + toDTO: () => {}, + }; + const address = 'TA545ICAVNEUDFUBIHO3CEJBSVIZ7YYHFFX5LQPT'; + const hdKeyPath = "44'/43'/152'/0'/0'"; + const network = -104; + const Trezor = { + serialize: () => {}, + }; + + const testTrezorAccountSignTxs = async (transactions, expectedResults, done) => { + // Arrange + const trezorAccount = new TrezorAccount(address, hdKeyPath, network, Trezor); + spyOn(Trezor, 'serialize').and.returnValues(...expectedResults.map((r) => Promise.resolve(r))); + + let result; + + try { + if (transactions.length > 1) { + // Act + result = await trezorAccount.signTransactions(transactions).toPromise(); + + // Assert + expect(result).toEqual(expectedResults); + } else { + // Act + result = await trezorAccount.signTransaction(transactions[0]).toPromise(); + + // Assert + expect(result).toEqual(expectedResults[0]); + } + } catch (err) { + fail('Error while serializing transaction for trezor: ' + err); + } + + // Assert more + expect(Trezor.serialize).toHaveBeenCalled(); + done(); + }; + it('Can serialize single transaction for trezor account', async function (done) { + testTrezorAccountSignTxs([transaction], ['payload'], done); + }); + + it('Can serialize multiple transactions for trezor account', async function (done) { + testTrezorAccountSignTxs([transaction, transaction], ['payload 1', 'payload 2'], done); + }); +});