diff --git a/karma.conf.coverage.js b/karma.conf.coverage.js deleted file mode 100644 index 73c420c4c..000000000 --- a/karma.conf.coverage.js +++ /dev/null @@ -1,87 +0,0 @@ -// Karma configuration - -module.exports = function (/** @type {any} */ config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', - - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine'], - - - // list of files / patterns to load in the browser - files: [ - {'pattern': 'node_modules/@nimiq/albatross-wasm/**/*', included: false}, - 'instrumented/**/*js', - 'tests/**/*.spec.js' - ], - - - // list of files / patterns to exclude - exclude: [ - 'instrumented/request/**/index.js' - ], - - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress', 'istanbul'], - - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { }, - - - istanbulReporter: { - reporters: [ - { type: 'text' }, - { type: 'html' } - ] - }, - - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: false, - - - // On travis in sudo:false mode (which is much faster to start) we have to run chrome without sandbox - customLaunchers: { - ChromeHeadlessNoSandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox'] - } - }, - - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: process.env.TRAVIS? ['ChromeHeadlessNoSandbox'] : ['ChromeHeadless'], - - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: true, - - // Concurrency level - // how many browser should be started simultaneous - concurrency: 1 - }); -}; diff --git a/karma.conf.js b/karma.conf.js index 06a6eff2d..fa71cf312 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -14,7 +14,9 @@ module.exports = function (/** @type {any} */ config) { // list of files / patterns to load in the browser files: [ - {'pattern': 'node_modules/@nimiq/albatross-wasm/**/*', included: false}, + {pattern: 'src/lib/AlbatrossWasm.mjs', type: 'module'}, + {pattern: 'node_modules/@nimiq/albatross-wasm/**/*', included: false}, + 'src/lib/Observable.js', 'src/lib/*.js', // Force load of lib files before components and common.js 'src/request/TopLevelApi.js', // Force load of TopLevelApi before BitcoinEnabledTopLevelApi 'src/lib/bitcoin/*.js', diff --git a/src/lib/Key.js b/src/lib/Key.js index 3ea03b0b5..bf6bd158c 100644 --- a/src/lib/Key.js +++ b/src/lib/Key.js @@ -144,6 +144,20 @@ class Key { : Nimiq.PublicKey.derive(this._secret).toAddress().serialize(); return Key.deriveHash(input); } + + // eslint-disable-next-line valid-jsdoc + /** + * @param {unknown} other + * @returns {other is Key} + */ + equals(other) { + return other instanceof Key + && this.id === other.id + && this.type === other.type + && this.hasPin === other.hasPin + && this.secret.equals(/** @type {Nimiq.PrivateKey} */ (other.secret)) + && this.defaultAddress.equals(other.defaultAddress); + } } Key.PIN_LENGTH = 6; diff --git a/src/lib/KeyInfo.js b/src/lib/KeyInfo.js index c3027e38a..40ffd59c4 100644 --- a/src/lib/KeyInfo.js +++ b/src/lib/KeyInfo.js @@ -84,4 +84,19 @@ class KeyInfo { static fromObject(obj, encrypted, defaultAddress) { return new KeyInfo(obj.id, obj.type, encrypted, obj.hasPin, defaultAddress); } + + // eslint-disable-next-line valid-jsdoc + /** + * @param {unknown} other + * @returns {other is KeyInfo} + */ + equals(other) { + return other instanceof KeyInfo + && this.id === other.id + && this.type === other.type + && this.hasPin === other.hasPin + && this.encrypted === other.encrypted + && this.useLegacyStore === other.useLegacyStore + && this.defaultAddress.equals(other.defaultAddress); + } } diff --git a/src/lib/swap/HtlcUtils.js b/src/lib/swap/HtlcUtils.js index 78d33fee9..d42422a33 100644 --- a/src/lib/swap/HtlcUtils.js +++ b/src/lib/swap/HtlcUtils.js @@ -12,16 +12,16 @@ class HtlcUtils { // eslint-disable-line no-unused-vars static decodeNimHtlcData(data) { const error = new Errors.InvalidRequestError('Invalid NIM HTLC data'); - if (!data || !(data instanceof Uint8Array) || data.length !== 78) throw error; + if (!data || !(data instanceof Uint8Array) || data.length !== 82) throw error; const buf = new Nimiq.SerialBuffer(data); - const sender = new Nimiq.Address(buf).toUserFriendlyAddress(); - const recipient = new Nimiq.Address(buf).toUserFriendlyAddress(); + const sender = new Nimiq.Address(buf.read(20)).toUserFriendlyAddress(); + const recipient = new Nimiq.Address(buf.read(20)).toUserFriendlyAddress(); const hashAlgorithm = buf.readUint8(); - const hashRoot = Nimiq.BufferUtils.toHex(buf); + const hashRoot = Nimiq.BufferUtils.toHex(buf.read(32)); const hashCount = buf.readUint8(); - const timeout = buf.readUint32(); + const timeout = buf.readUint64(); if (hashAlgorithm !== 3 /* Nimiq.Hash.Algorithm.SHA256 */) throw error; if (hashCount !== 1) throw error; @@ -30,7 +30,7 @@ class HtlcUtils { // eslint-disable-line no-unused-vars refundAddress: sender, redeemAddress: recipient, hash: hashRoot, - timeoutBlockHeight: timeout, + timeoutTimestamp: timeout / 1e3, // Convert to seconds to match Bitcoin, Polygon and OASIS }; } diff --git a/src/request/TopLevelApi.js b/src/request/TopLevelApi.js index b1d637d65..d9d393508 100644 --- a/src/request/TopLevelApi.js +++ b/src/request/TopLevelApi.js @@ -4,7 +4,6 @@ /* global KeyStore */ /* global CookieJar */ /* global I18n */ -/* global Nimiq */ /* global RequestParser */ /* global NoRequestErrorPage */ diff --git a/src/request/swap-iframe/SwapIFrameApi.js b/src/request/swap-iframe/SwapIFrameApi.js index cff186209..a9f4e3871 100644 --- a/src/request/swap-iframe/SwapIFrameApi.js +++ b/src/request/swap-iframe/SwapIFrameApi.js @@ -161,13 +161,6 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint throw new Errors.InvalidRequestError('NIM HTLC refund address must be same as sender'); } - // Check that validityStartHeight is before HTLC timeout - if (storedRequest.fund.transaction.validityStartHeight >= htlcDetails.timeoutBlockHeight) { - throw new Errors.InvalidRequestError( - 'Fund validityStartHeight must be lower than HTLC timeout block height', - ); - } - fund = { type: 'NIM', htlcDetails, @@ -182,13 +175,6 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint throw new Errors.InvalidRequestError('NIM HTLC redeem address must be same as recipient'); } - // Check that validityStartHeight is before HTLC timeout - if (storedRequest.redeem.transaction.validityStartHeight >= htlcDetails.timeoutBlockHeight) { - throw new Errors.InvalidRequestError( - 'Redeem validityStartHeight must be lower than HTLC timeout block height', - ); - } - redeem = { type: 'NIM', htlcDetails, @@ -395,22 +381,16 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint // Validate timeouts of the two contracts // The redeem HTLC must have a later timeout than the funding HTLC. const fundingTimeout = 'htlcDetails' in fund - ? 'timeoutTimestamp' in fund.htlcDetails - ? fund.htlcDetails.timeoutTimestamp - : undefined + ? fund.htlcDetails.timeoutTimestamp : fund.description.args.timeout.toNumber(); - const redeemingTimeout = 'timeoutTimestamp' in redeem.htlcDetails - ? redeem.htlcDetails.timeoutTimestamp - : undefined; - if (fundingTimeout && redeemingTimeout) { - const diff = redeemingTimeout - fundingTimeout; - - // Validate that the difference is at least 15 minutes - if (diff < 15 * 60) { - throw new Errors.InvalidRequestError( - 'HTLC redeem timeout must be 15 min or more after the funding timeout', - ); - } + const redeemingTimeout = redeem.htlcDetails.timeoutTimestamp; + const diff = redeemingTimeout - fundingTimeout; + + // Validate that the difference is at least 15 minutes + if (diff < 15 * 60) { + throw new Errors.InvalidRequestError( + 'HTLC redeem timeout must be 15 min or more after the funding timeout', + ); } /** @type {Parsed} */ @@ -466,7 +446,7 @@ class SwapIFrameApi extends BitcoinRequestParserMixin(RequestParser) { // eslint transaction.sender, Nimiq.AccountType.Basic, new Uint8Array(0), transaction.value - fee, fee, 0 /* Nimiq.Transaction.Flag.NONE */, - parsedRequest.fund.htlcDetails.timeoutBlockHeight, + parsedRequest.fund.htlcDetails.timeoutTimestamp, CONFIG.NIMIQ_NETWORK_ID, ); diff --git a/tests/DummyData.spec.js b/tests/DummyData.spec.js index 715f00832..d51760a55 100644 --- a/tests/DummyData.spec.js +++ b/tests/DummyData.spec.js @@ -7,250 +7,258 @@ // Increase test timeout to reduce Travis failure rate jasmine.DEFAULT_TIMEOUT_INTERVAL = 10 * 1000; // 10 seconds +/** + * @type {ReturnType} + */ +let Dummy; + beforeAll(async () => { await loadNimiq(); + Dummy = DummyData(); }); -/* To generate HEX arrays, you can use [...].map(n => '0x' + n.toString(16)) from a decimal array, and then remove the quotes. */ - -const keys = [ - // eslint-disable-next-line max-len - Uint8Array.from([ 0x50, 0x07, 0xdf, 0x4a, 0xb4, 0x57, 0x2d, 0x5c, 0x3a, 0x30, 0xbf, 0x71, 0x2b, 0xd6, 0x22, 0x37, 0x2e, 0x7a, 0x48, 0x0b, 0x6b, 0x08, 0x41, 0xf1, 0x73, 0x5c, 0x82, 0xaf, 0xf1, 0xb9, 0xa9, 0xb7 ]), - // eslint-disable-next-line max-len - Uint8Array.from([ 0x68, 0x2d, 0xf0, 0x35, 0xc3, 0x0e, 0x8c, 0x55, 0xbd, 0xa4, 0xab, 0x51, 0x72, 0x13, 0x79, 0x2c, 0x2a, 0xf4, 0xb4, 0xe9, 0xc2, 0x3a, 0xc2, 0xcf, 0x11, 0xbd, 0x28, 0xb2, 0x37, 0xa6, 0x54, 0x4e ]), -]; - -const mnemonics = [ - 'exotic discover sport hamster index puppy trip blood illness gadget mass hour tray catch color genre draft merit strong aim yellow system plug year', - 'habit hurdle aspect maid trip client walk nice fabric mountain tattoo flame quantum foil poem elder security joy hungry fabric casino spring eye scene', -]; - -/** @type {[Nimiq.Entropy, Nimiq.PrivateKey]} */ -const secrets = [ - new Nimiq.Entropy(keys[0]), - new Nimiq.PrivateKey(keys[1]), -]; - -const encryptedKeys = [ - // eslint-disable-next-line max-len - Uint8Array.from([ 0x03, 0x08, 0x38, 0x8f, 0x65, 0x9f, 0x23, 0xfb, 0x80, 0x13, 0xd5, 0xef, 0x86, 0xdc, 0x4d, 0xdc, 0xd7, 0x13, 0xf4, 0x00, 0x34, 0x90, 0xe4, 0xc7, 0x9e, 0x78, 0x84, 0xa5, 0x5e, 0x21, 0x3a, 0xdb, 0x6a, 0xe1, 0x94, 0x48, 0xbd, 0x93, 0xed, 0xd1, 0x2b, 0xfd, 0x0e, 0x1f, 0x34, 0xd5, 0x4b, 0x38, 0x17, 0x4b, 0x8b, 0xf2, 0xd7, 0x4e, 0x1c, 0x10 ]), - // eslint-disable-next-line max-len - Uint8Array.from([ 0x03, 0x08, 0x44, 0x88, 0xd9, 0xbd, 0x82, 0xf8, 0x7b, 0x88, 0x69, 0xc4, 0xde, 0x03, 0x19, 0xe2, 0xc3, 0x95, 0xd3, 0x7d, 0x57, 0x17, 0x5a, 0x34, 0x48, 0xd2, 0x4c, 0x0d, 0xa5, 0x3d, 0xe8, 0xb4, 0x1a, 0x41, 0x13, 0xa2, 0xdc, 0xae, 0x54, 0x03, 0x0d, 0xce, 0xaf, 0x2a, 0x59, 0x10, 0xf1, 0x71, 0x7b, 0x0b, 0x2a, 0x90, 0x05, 0x6f, 0xb3, 0x46 ]), -]; - -/** @type {string[]} */ -let __hashes = []; -const hashes = () => { - return __hashes.length ? __hashes : (__hashes = secrets.map(secret => { - const input = secret instanceof Nimiq.Entropy - ? secret.serialize() - : Nimiq.PublicKey.derive(secret).toAddress().serialize(); - - return Nimiq.BufferUtils.toBase64(Nimiq.Hash.computeBlake2b(input)); - })); -}; - -const encryptionPassword = 'password'; -const encryptionPassword2 = 'password2'; - -const keyInfos = () => [ - new KeyInfo( - hashes()[0], - Nimiq.Secret.Type.ENTROPY, - false, - false, - new Uint8Array(new Nimiq.Entropy(keys[0]).toExtendedPrivateKey().derivePath(Constants.DEFAULT_DERIVATION_PATH).toAddress().serialize()), - ), - new KeyInfo( - hashes()[1], - Nimiq.Secret.Type.PRIVATE_KEY, - true, - false, - new Uint8Array(Nimiq.PublicKey.derive(new Nimiq.PrivateKey(keys[1])).toAddress().serialize()), - ), -]; - -const cookieKeyInfos = () => keyInfos().map(x => new KeyInfo(x.id, x.type, true, x.hasPin, new Uint8Array(20))); - -const keyInfoObjects = () => keyInfos().map(x => ({ - id: x.id, - type: x.type, - hasPin: x.hasPin, -})); - -const _purposeIdBuf = new Nimiq.SerialBuffer(4); -_purposeIdBuf.writeUint32(Nimiq.Entropy.PURPOSE_ID); -const _purposeIdArray = Array.from(_purposeIdBuf); - -/** @type {() => KeyRecord[]} */ -const keyRecords = () => [ - { - id: hashes()[0], - type: keyInfoObjects()[0].type, - hasPin: keyInfoObjects()[0].hasPin, - secret: new Uint8Array(_purposeIdArray.concat(Array.from(keys[0]))), - defaultAddress: new Uint8Array(keyInfos()[0].defaultAddress.serialize()), - }, - { - id: hashes()[1], - type: keyInfoObjects()[1].type, - hasPin: keyInfoObjects()[1].hasPin, - secret: encryptedKeys[1], - defaultAddress: new Uint8Array(keyInfos()[1].defaultAddress.serialize()), - }, -]; - -const deprecatedAccountInfos = [ - { - userFriendlyAddress: 'NQ71 CT4K 7R9R EHSB 7HY9 TSTP XNRQ L2RK 8U4U', - type: 'high', - label: 'Dummy account 1', - }, -]; - -const deprecatedAccountCookies = [ - { - address: 'NQ71 CT4K 7R9R EHSB 7HY9 TSTP XNRQ L2RK 8U4U', - type: 'high', - label: 'Dummy account 1', - }, -]; - -const deprecatedAccountRecords = [ - Object.assign({}, deprecatedAccountInfos[0], { encryptedKeyPair: encryptedKeys[1] }), -]; - -const deprecatedAccount2KeyInfoObjects = () => [{ - id: hashes()[1], - type: Nimiq.Secret.Type.PRIVATE_KEY, - hasPin: false, - legacyAccount: { - label: deprecatedAccountInfos[0].label, - address: Nimiq.Address.fromUserFriendlyAddress(deprecatedAccountInfos[0].userFriendlyAddress).serialize(), - }, -}]; - -const deprecatedAccount2KeyInfos = () => { - const keyInfos = [ - KeyInfo.fromObject( - deprecatedAccount2KeyInfoObjects()[0], +function DummyData() { + /* To generate HEX arrays, you can use [...].map(n => '0x' + n.toString(16)) from a decimal array, and then remove the quotes. */ + + const keys = [ + // eslint-disable-next-line max-len + Uint8Array.from([ 0x50, 0x07, 0xdf, 0x4a, 0xb4, 0x57, 0x2d, 0x5c, 0x3a, 0x30, 0xbf, 0x71, 0x2b, 0xd6, 0x22, 0x37, 0x2e, 0x7a, 0x48, 0x0b, 0x6b, 0x08, 0x41, 0xf1, 0x73, 0x5c, 0x82, 0xaf, 0xf1, 0xb9, 0xa9, 0xb7 ]), + // eslint-disable-next-line max-len + Uint8Array.from([ 0x68, 0x2d, 0xf0, 0x35, 0xc3, 0x0e, 0x8c, 0x55, 0xbd, 0xa4, 0xab, 0x51, 0x72, 0x13, 0x79, 0x2c, 0x2a, 0xf4, 0xb4, 0xe9, 0xc2, 0x3a, 0xc2, 0xcf, 0x11, 0xbd, 0x28, 0xb2, 0x37, 0xa6, 0x54, 0x4e ]), + ]; + + const mnemonics = [ + 'exotic discover sport hamster index puppy trip blood illness gadget mass hour tray catch color genre draft merit strong aim yellow system plug year', + 'habit hurdle aspect maid trip client walk nice fabric mountain tattoo flame quantum foil poem elder security joy hungry fabric casino spring eye scene', + ]; + + /** @type {[Nimiq.Entropy, Nimiq.PrivateKey]} */ + const secrets = [ + new Nimiq.Entropy(keys[0]), + new Nimiq.PrivateKey(keys[1]), + ]; + + const encryptedKeys = [ + // eslint-disable-next-line max-len + Uint8Array.from([ 0x03, 0x08, 0x38, 0x8f, 0x65, 0x9f, 0x23, 0xfb, 0x80, 0x13, 0xd5, 0xef, 0x86, 0xdc, 0x4d, 0xdc, 0xd7, 0x13, 0xf4, 0x00, 0x34, 0x90, 0xe4, 0xc7, 0x9e, 0x78, 0x84, 0xa5, 0x5e, 0x21, 0x3a, 0xdb, 0x6a, 0xe1, 0x94, 0x48, 0xbd, 0x93, 0xed, 0xd1, 0x2b, 0xfd, 0x0e, 0x1f, 0x34, 0xd5, 0x4b, 0x38, 0x17, 0x4b, 0x8b, 0xf2, 0xd7, 0x4e, 0x1c, 0x10 ]), + // eslint-disable-next-line max-len + Uint8Array.from([ 0x03, 0x08, 0x44, 0x88, 0xd9, 0xbd, 0x82, 0xf8, 0x7b, 0x88, 0x69, 0xc4, 0xde, 0x03, 0x19, 0xe2, 0xc3, 0x95, 0xd3, 0x7d, 0x57, 0x17, 0x5a, 0x34, 0x48, 0xd2, 0x4c, 0x0d, 0xa5, 0x3d, 0xe8, 0xb4, 0x1a, 0x41, 0x13, 0xa2, 0xdc, 0xae, 0x54, 0x03, 0x0d, 0xce, 0xaf, 0x2a, 0x59, 0x10, 0xf1, 0x71, 0x7b, 0x0b, 0x2a, 0x90, 0x05, 0x6f, 0xb3, 0x46 ]), + ]; + + /** @type {string[]} */ + let __hashes = []; + const hashes = () => { + return __hashes.length ? __hashes : (__hashes = secrets.map(secret => { + const input = secret instanceof Nimiq.Entropy + ? secret.serialize() + : Nimiq.PublicKey.derive(secret).toAddress().serialize(); + + return Nimiq.BufferUtils.toBase64(Nimiq.Hash.computeBlake2b(input)); + })); + }; + + const encryptionPassword = 'password'; + const encryptionPassword2 = 'password2'; + + const keyInfos = () => [ + new KeyInfo( + hashes()[0], + Nimiq.Secret.Type.ENTROPY, + false, + false, + new Nimiq.Entropy(keys[0]).toExtendedPrivateKey().derivePath(Constants.DEFAULT_DERIVATION_PATH).toAddress().serialize(), + ), + new KeyInfo( + hashes()[1], + Nimiq.Secret.Type.PRIVATE_KEY, true, - deprecatedAccount2KeyInfoObjects()[0].legacyAccount.address, + false, + Nimiq.PublicKey.derive(new Nimiq.PrivateKey(keys[1])).toAddress().serialize(), ), ]; - keyInfos.forEach(ki => ki.useLegacyStore = true); - return keyInfos; -}; - -const deprecatedAccount2Keys = () => [ - new Key(secrets[1], false) -]; - -const keyInfoCookieEncoded = '2071U/NKd5KzeimvyflGLYku6JfI9Mms2wGxWoCLmx9+0=#10LsYVUikGJA5z8p37+LqkH3EZ5opDz2zQRT1r8cGJ8dE='; - -/** @type {string} */ -const cookie = `k=${keyInfoCookieEncoded}; accounts=${JSON.stringify(deprecatedAccountCookies)}; some=thing`; - -const DUMMY_ACCOUNT_DATABASE_NAME = 'keyguard-dummy-account-database'; -const DUMMY_KEY_DATABASE_NAME = 'keyguard-dummy-key-database'; -AccountStore.ACCOUNT_DATABASE = DUMMY_ACCOUNT_DATABASE_NAME; -KeyStore.DB_NAME = DUMMY_KEY_DATABASE_NAME; - -const Utils = { - /** - * @param {IDBDatabase} db - * @param {string} objectStoreName - * @param {object} entry - * @returns {Promise} - */ - addEntryToDatabase: (db, objectStoreName, entry) => new Promise((resolve, reject) => { - const request = db.transaction([objectStoreName], 'readwrite') - .objectStore(objectStoreName) - .put(entry); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }), - - /** @returns {Promise} */ - createDummyAccountStore: async () => { - // create database - const db = await new Promise((resolve, reject) => { - const request = window.indexedDB.open(DUMMY_ACCOUNT_DATABASE_NAME, AccountStore.VERSION); - request.onsuccess = () => resolve(request.result); + + const cookieKeyInfos = () => keyInfos().map(x => new KeyInfo(x.id, x.type, true, x.hasPin, new Uint8Array(20))); + + const keyInfoObjects = () => keyInfos().map(x => ({ + id: x.id, + type: x.type, + hasPin: x.hasPin, + })); + + const _purposeIdBuf = new Nimiq.SerialBuffer(4); + _purposeIdBuf.writeUint32(Nimiq.Entropy.PURPOSE_ID); + const _purposeIdArray = Array.from(_purposeIdBuf); + + /** @type {() => KeyRecord[]} */ + const keyRecords = () => [ + { + id: hashes()[0], + type: keyInfoObjects()[0].type, + hasPin: keyInfoObjects()[0].hasPin, + secret: new Uint8Array(_purposeIdArray.concat(Array.from(keys[0]))), + defaultAddress: new Uint8Array(keyInfos()[0].defaultAddress.serialize()), + }, + { + id: hashes()[1], + type: keyInfoObjects()[1].type, + hasPin: keyInfoObjects()[1].hasPin, + secret: encryptedKeys[1], + defaultAddress: new Uint8Array(keyInfos()[1].defaultAddress.serialize()), + }, + ]; + + const deprecatedAccountInfos = [ + { + userFriendlyAddress: 'NQ71 CT4K 7R9R EHSB 7HY9 TSTP XNRQ L2RK 8U4U', + type: 'high', + label: 'Dummy account 1', + }, + ]; + + const deprecatedAccountCookies = [ + { + address: 'NQ71 CT4K 7R9R EHSB 7HY9 TSTP XNRQ L2RK 8U4U', + type: 'high', + label: 'Dummy account 1', + }, + ]; + + const deprecatedAccountRecords = [ + Object.assign({}, deprecatedAccountInfos[0], { encryptedKeyPair: encryptedKeys[1] }), + ]; + + const deprecatedAccount2KeyInfoObjects = () => [{ + id: hashes()[1], + type: Nimiq.Secret.Type.PRIVATE_KEY, + hasPin: false, + legacyAccount: { + label: deprecatedAccountInfos[0].label, + address: Nimiq.Address.fromUserFriendlyAddress(deprecatedAccountInfos[0].userFriendlyAddress).serialize(), + }, + }]; + + const deprecatedAccount2KeyInfos = () => { + const keyInfos = [ + KeyInfo.fromObject( + deprecatedAccount2KeyInfoObjects()[0], + true, + deprecatedAccount2KeyInfoObjects()[0].legacyAccount.address, + ), + ]; + keyInfos.forEach(ki => ki.useLegacyStore = true); + return keyInfos; + }; + + const deprecatedAccount2Keys = () => [ + new Key(secrets[1], false) + ]; + + const keyInfoCookieEncoded = '2071U/NKd5KzeimvyflGLYku6JfI9Mms2wGxWoCLmx9+0=#10LsYVUikGJA5z8p37+LqkH3EZ5opDz2zQRT1r8cGJ8dE='; + + /** @type {string} */ + const cookie = `k=${keyInfoCookieEncoded}; accounts=${JSON.stringify(deprecatedAccountCookies)}; some=thing`; + + const DUMMY_ACCOUNT_DATABASE_NAME = 'keyguard-dummy-account-database'; + const DUMMY_KEY_DATABASE_NAME = 'keyguard-dummy-key-database'; + AccountStore.ACCOUNT_DATABASE = DUMMY_ACCOUNT_DATABASE_NAME; + KeyStore.DB_NAME = DUMMY_KEY_DATABASE_NAME; + + const Utils = { + /** + * @param {IDBDatabase} db + * @param {string} objectStoreName + * @param {object} entry + * @returns {Promise} + */ + addEntryToDatabase: (db, objectStoreName, entry) => new Promise((resolve, reject) => { + const request = db.transaction([objectStoreName], 'readwrite') + .objectStore(objectStoreName) + .put(entry); + request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); - request.onupgradeneeded = () => { - const _db = request.result; - _db.createObjectStore(AccountStore.ACCOUNT_DATABASE, { keyPath: 'userFriendlyAddress' }); - }; - }); - - // fill database - await Utils.addEntryToDatabase(db, AccountStore.ACCOUNT_DATABASE, deprecatedAccountRecords[0]); - - db.close(); - }, - - /** @returns {Promise} */ - createDummyKeyStore: async () => { - // The key store can be created and filled by its api - await KeyStore.instance['connect'](); - await Promise.all([ - KeyStore.instance.putPlain(keyRecords()[0]), - KeyStore.instance.putPlain(keyRecords()[1]), - ]); - await KeyStore.instance.close(); - }, - - /** - * @param {string} dbName - * @returns {Promise} - */ - deleteDatabase: async dbName => { - return new Promise((resolve, reject) => { - const request = indexedDB.deleteDatabase(dbName); - request.onerror = () => { reject() }; - request.onsuccess = () => { resolve() }; - request.onblocked = () => { - // wait for open connections to get closed - setTimeout(() => reject(new Error('Can\'t delete database, there is still an open connection.')), 2000); - }; - }); - }, - - /** @returns {Promise} */ - deleteDummyAccountStore: async () => { - await AccountStore.instance.close(); - await Utils.deleteDatabase(DUMMY_ACCOUNT_DATABASE_NAME); - delete AccountStore._instance; - }, - - /** @returns {Promise} */ - deleteDummyKeyStore: async () => { - await KeyStore.instance.close(); - await Utils.deleteDatabase(DUMMY_KEY_DATABASE_NAME); - KeyStore._instance = null; - }, -}; - -const Dummy = { - keys, - secrets, - Utils, - encryptedKeys, - hashes, - keyInfos, - cookieKeyInfos, - keyRecords, - keyInfoCookieEncoded, - deprecatedAccountInfos, - cookie, - encryptionPassword, - encryptionPassword2, - keyInfoObjects, - deprecatedAccount2KeyInfoObjects, - deprecatedAccount2KeyInfos, - deprecatedAccount2Keys, - DUMMY_ACCOUNT_DATABASE_NAME, - DUMMY_KEY_DATABASE_NAME, - deprecatedAccountRecords, -}; + }), + + /** @returns {Promise} */ + createDummyAccountStore: async () => { + // create database + const db = await new Promise((resolve, reject) => { + const request = window.indexedDB.open(DUMMY_ACCOUNT_DATABASE_NAME, AccountStore.VERSION); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + request.onupgradeneeded = () => { + const _db = request.result; + _db.createObjectStore(AccountStore.ACCOUNT_DATABASE, { keyPath: 'userFriendlyAddress' }); + }; + }); + + // fill database + await Utils.addEntryToDatabase(db, AccountStore.ACCOUNT_DATABASE, deprecatedAccountRecords[0]); + + db.close(); + }, + + /** @returns {Promise} */ + createDummyKeyStore: async () => { + // The key store can be created and filled by its api + await KeyStore.instance['connect'](); + await Promise.all([ + KeyStore.instance.putPlain(keyRecords()[0]), + KeyStore.instance.putPlain(keyRecords()[1]), + ]); + await KeyStore.instance.close(); + }, + + /** + * @param {string} dbName + * @returns {Promise} + */ + deleteDatabase: async dbName => { + return new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(dbName); + request.onerror = () => { reject() }; + request.onsuccess = () => { resolve() }; + request.onblocked = () => { + // wait for open connections to get closed + setTimeout(() => reject(new Error('Can\'t delete database, there is still an open connection.')), 2000); + }; + }); + }, + + /** @returns {Promise} */ + deleteDummyAccountStore: async () => { + await AccountStore.instance.close(); + await Utils.deleteDatabase(DUMMY_ACCOUNT_DATABASE_NAME); + delete AccountStore._instance; + }, + + /** @returns {Promise} */ + deleteDummyKeyStore: async () => { + await KeyStore.instance.close(); + await Utils.deleteDatabase(DUMMY_KEY_DATABASE_NAME); + KeyStore._instance = null; + }, + }; + + return { + keys, + secrets, + Utils, + encryptedKeys, + hashes, + keyInfos, + cookieKeyInfos, + keyRecords, + keyInfoCookieEncoded, + deprecatedAccountInfos, + cookie, + encryptionPassword, + encryptionPassword2, + keyInfoObjects, + deprecatedAccount2KeyInfoObjects, + deprecatedAccount2KeyInfos, + deprecatedAccount2Keys, + DUMMY_ACCOUNT_DATABASE_NAME, + DUMMY_KEY_DATABASE_NAME, + deprecatedAccountRecords, + }; +} diff --git a/tests/lib/AccountStore.spec.js b/tests/lib/AccountStore.spec.js index ae1a0c0bf..e59265f4e 100644 --- a/tests/lib/AccountStore.spec.js +++ b/tests/lib/AccountStore.spec.js @@ -49,12 +49,14 @@ describe('AccountStore', () => { it('can get a KeyInfo', async function() { const keyInfo = await AccountStore.instance.getInfo('NQ71 CT4K 7R9R EHSB 7HY9 TSTP XNRQ L2RK 8U4U'); - expect(keyInfo).toEqual(Dummy.deprecatedAccount2KeyInfos()[0]); + if (!keyInfo) throw new Error('KeyInfo is falsy'); + expect(keyInfo.equals(Dummy.deprecatedAccount2KeyInfos()[0])).toBe(true); }); it('can get a Key', async function() { const passwordBytes = Nimiq.BufferUtils.fromUtf8(Dummy.encryptionPassword); const key = await AccountStore.instance.get('NQ71 CT4K 7R9R EHSB 7HY9 TSTP XNRQ L2RK 8U4U', passwordBytes); - expect(key).toEqual(Dummy.deprecatedAccount2Keys()[0]); + if (!key) throw new Error('KeyInfo is falsy'); + expect(key.equals(Dummy.deprecatedAccount2Keys()[0])).toBe(true); }); }); diff --git a/tests/lib/CookieJar.spec.js b/tests/lib/CookieJar.spec.js index 4d4cb2947..62a8034bd 100644 --- a/tests/lib/CookieJar.spec.js +++ b/tests/lib/CookieJar.spec.js @@ -8,7 +8,11 @@ describe('CookieJar', () => { it('can decode a cookie', () => { const decoded = CookieJar._decodeKeysCookie(Dummy.keyInfoCookieEncoded); - expect(decoded).toEqual(Dummy.cookieKeyInfos()); + + const expected = Dummy.cookieKeyInfos(); + for (let i = 0; i < Math.max(decoded.length, expected.length); i++) { + expect(decoded[i].equals(expected[i])).toBe(true); + } }); it('can be filled with key info', () => { @@ -27,6 +31,10 @@ describe('CookieJar', () => { const deprecatedAccountInfo = CookieJar.eatDeprecatedAccounts(); expect(deprecatedAccountInfo).toEqual(Dummy.deprecatedAccountInfos); const keyInfo = CookieJar.eatKeys(); - expect(keyInfo).toEqual(Dummy.cookieKeyInfos()); + + const expected = Dummy.cookieKeyInfos(); + for (let i = 0; i < Math.max(keyInfo.length, expected.length); i++) { + expect(keyInfo[i].equals(expected[i])).toBe(true); + } }); }); diff --git a/tests/lib/HtlcUtils.spec.js b/tests/lib/HtlcUtils.spec.js index 1f2e9924d..40644a3ca 100644 --- a/tests/lib/HtlcUtils.spec.js +++ b/tests/lib/HtlcUtils.spec.js @@ -4,12 +4,12 @@ describe('HtlcUtils', () => { it('can decode NIM HTLC data', () => { const vectors = [{ // Branches verified individually - data: Nimiq.BufferUtils.fromHex('eb933bf41fdcc9bc2ebf439305a0b2c64d5aea34df2953459bb18ca54537e55fef20144bc35816280309534fd599fcfd6ca4a0b02e93ca1b93e7c6275cb496b156fa20d950b7a7120c010014eb4f'), + data: Nimiq.BufferUtils.fromHex('eb933bf41fdcc9bc2ebf439305a0b2c64d5aea34df2953459bb18ca54537e55fef20144bc35816280309534fd599fcfd6ca4a0b02e93ca1b93e7c6275cb496b156fa20d950b7a7120c010000000051b72c98'), result: { refundAddress: 'NQ56 VE9K PV0Y TK4T QBMY 8E9G B85J QR6M MSHL', redeemAddress: 'NQ84 TULM 6HCT N66A AH9P UMFX X80L 9F1M G5H8', hash: '09534fd599fcfd6ca4a0b02e93ca1b93e7c6275cb496b156fa20d950b7a7120c', - timeoutBlockHeight: 1370959, + timeoutTimestamp: 1370959, }, }]; diff --git a/tests/lib/Key.spec.js b/tests/lib/Key.spec.js index df94b0d57..8402e021e 100644 --- a/tests/lib/Key.spec.js +++ b/tests/lib/Key.spec.js @@ -18,7 +18,7 @@ describe('Key', () => { const proof2 = Nimiq.SignatureProof.singleSig(pubkey, signature2); expect(proof2.verify(hashedSignedData)).toBe(true); - expect(signature1).toEqual(signature2); + expect(signature1.serialize()).toEqual(signature2.serialize()); }); it('can sign a message (BIP39)', () => { @@ -39,7 +39,7 @@ describe('Key', () => { const proof2 = Nimiq.SignatureProof.singleSig(pubkey2, signature2); expect(proof2.verify(hashedSignedData)).toBe(true); - expect(signature1).not.toEqual(signature2); + expect(signature1.serialize()).not.toEqual(signature2.serialize()); }); it('can derive addresses (LEGACY)', () => { diff --git a/tests/lib/KeyStore.spec.js b/tests/lib/KeyStore.spec.js index 9e709bbb5..7ee81c0ee 100644 --- a/tests/lib/KeyStore.spec.js +++ b/tests/lib/KeyStore.spec.js @@ -39,21 +39,30 @@ describe('KeyStore', () => { for (let [i, key] of keys.entries()) { if (!key) throw new Error(`Key with id ${Dummy.keyInfos()[i].id} not found!`); - expect(key.id).toEqual(Dummy.keyInfos()[i].id); - expect(key.type).toEqual(Dummy.keyInfos()[i].type); - expect(key.secret).toEqual(Dummy.secrets[i]); - expect(key.hasPin).toEqual(Dummy.keyInfos()[i].hasPin); + const expected = Dummy.keyInfos()[i]; + expect(key.id).toEqual(expected.id); + expect(key.type).toEqual(expected.type); + expect(key.secret.equals(/** @type {Nimiq.PrivateKey} */ (Dummy.secrets[i]))).toBe(true); + expect(key.hasPin).toEqual(expected.hasPin); } }); it('can list keys', async () => { const keyInfos = await KeyStore.instance.list(); - expect(keyInfos).toEqual(Dummy.keyInfos()); + + const expected = Dummy.keyInfos(); + for (let i = 0; i < Math.max(keyInfos.length, expected.length); i++) { + expect(keyInfos[i].equals(expected[i])).toBe(true); + } }); it('can remove keys', async () => { let currentKeys = await KeyStore.instance.list(); - expect(currentKeys).toEqual(Dummy.keyInfos()); + + const expected = Dummy.keyInfos(); + for (let i = 0; i < Math.max(currentKeys.length, expected.length); i++) { + expect(currentKeys[i].equals(expected[i])).toBe(true); + } await KeyStore.instance.remove(Dummy.keyInfos()[0].id); currentKeys = await KeyStore.instance.list(); @@ -95,7 +104,10 @@ describe('KeyStore', () => { Dummy.keyInfos()[0].hasPin, )); currentKeys = await KeyStore.instance.list(); - expect(currentKeys).toEqual(Dummy.keyInfos()); + const expected = Dummy.keyInfos(); + for (let i = 0; i < Math.max(currentKeys.length, expected.length); i++) { + expect(currentKeys[i].equals(expected[i])).toBe(true); + } // check that the keys have been stored correctly const [key1, key2] = await Promise.all([ diff --git a/tests/lib/RequestParser.spec.js b/tests/lib/RequestParser.spec.js index b06adb484..48ffa27fe 100644 --- a/tests/lib/RequestParser.spec.js +++ b/tests/lib/RequestParser.spec.js @@ -83,7 +83,7 @@ describe('RequestParser', () => { expect(error).toEqual(new Errors.KeyNotFoundError()); const parsedKeyInfo = await requestParser.parseKeyId(Dummy.keyInfos()[0].id); - expect(parsedKeyInfo).toEqual(Dummy.keyInfos()[0]); + expect(parsedKeyInfo.equals(Dummy.keyInfos()[0])).toBe(true); await Dummy.Utils.deleteDummyKeyStore(); }); @@ -118,21 +118,32 @@ describe('RequestParser', () => { validityStartHeight: 176450, value: 545000000, }; - expect(requestParser.parseTransaction(transaction)).toEqual( - new Nimiq.Transaction( - new Nimiq.Address(new Uint8Array([238, 61, 13, 183, 158, 200, 247, 106, 130, 61, 9, 123, 134, 82, 60, 95, 16, 71, 39, 70])), //sender - Nimiq.AccountType.Basic, // senderType - new Uint8Array(0), - new Nimiq.Address(new Uint8Array([225, 253, 0, 255, 238, 105, 158, 173, 122, 16, 27, 203, 31, 16, 3, 178, 231, 105, 81, 188])), // recipient - Nimiq.AccountType.Basic, //recipientType - new Uint8Array([84, 104, 97, 110, 107, 32, 121, 111, 117, 32, 102, 111, 114, 32, 115, 104, 111, 112, 112, 105, 110, 103, 32, 97, 116, 32, 115, 104, 111, 112, 46, 110, 105, 109, 105, 113, 46, 99, 111, 109, 32, 40, 72, 51, 88, 67, 48, 68, 41]), // recipientData - 545000000n, // value - 0n, // fee - 0, // flags - 176450, // validityStartHeight - 5, // networkId - ), + + const parsed = requestParser.parseTransaction(transaction); + const expected = new Nimiq.Transaction( + new Nimiq.Address(new Uint8Array([238, 61, 13, 183, 158, 200, 247, 106, 130, 61, 9, 123, 134, 82, 60, 95, 16, 71, 39, 70])), //sender + Nimiq.AccountType.Basic, // senderType + new Uint8Array(0), + new Nimiq.Address(new Uint8Array([225, 253, 0, 255, 238, 105, 158, 173, 122, 16, 27, 203, 31, 16, 3, 178, 231, 105, 81, 188])), // recipient + Nimiq.AccountType.Basic, //recipientType + new Uint8Array([84, 104, 97, 110, 107, 32, 121, 111, 117, 32, 102, 111, 114, 32, 115, 104, 111, 112, 112, 105, 110, 103, 32, 97, 116, 32, 115, 104, 111, 112, 46, 110, 105, 109, 105, 113, 46, 99, 111, 109, 32, 40, 72, 51, 88, 67, 48, 68, 41]), // recipientData + 545000000n, // value + 0n, // fee + 0, // flags + 176450, // validityStartHeight + 5, // networkId ); + expect(parsed.sender.equals(expected.sender)).toBe(true); + expect(parsed.senderType).toBe(expected.senderType); + expect(parsed.senderData).toEqual(expected.senderData); + expect(parsed.recipient.equals(expected.recipient)).toBe(true); + expect(parsed.recipientType).toBe(expected.recipientType); + expect(parsed.data).toEqual(expected.data); + expect(parsed.value).toEqual(expected.value); + expect(parsed.fee).toEqual(expected.fee); + expect(parsed.flags).toEqual(expected.flags); + expect(parsed.validityStartHeight).toEqual(expected.validityStartHeight); + expect(parsed.networkId).toEqual(expected.networkId); const sender = transaction.sender; transaction.sender = transaction.recipient; diff --git a/types/Keyguard.d.ts b/types/Keyguard.d.ts index 750b8c3ca..831398264 100644 --- a/types/Keyguard.d.ts +++ b/types/Keyguard.d.ts @@ -183,7 +183,7 @@ type NimHtlcContents = { refundAddress: string, redeemAddress: string, hash: string, - timeoutBlockHeight: number, + timeoutTimestamp: number, }; type BtcHtlcContents = {