diff --git a/apps/alias-server/src/alias.js b/apps/alias-server/src/alias.js --- a/apps/alias-server/src/alias.js +++ b/apps/alias-server/src/alias.js @@ -11,6 +11,7 @@ isValidAliasString, getOutputScriptFromAddress, } = require('./utils'); +const { addOneAliasToDb } = require('./db'); module.exports = { getAliasTxs: function (aliasTxHistory, aliasConstants) { @@ -145,16 +146,20 @@ return aliasTxsSortedByTxidAndBlockheight; }, - getValidAliasRegistrations: function (registeredAliases, unsortedAliasTxs) { - /* Function that takes an array of already-registered aliases (strings) - * and an arbitrary collection of alias-prefixed txs from the alias registration - * address. + getValidAliasRegistrations: async function (db, unsortedAliasTxs) { + /* Add new valid aliases registration txs to the database. Return an array of what was added. * - * Outputs new valid alias registrations by discarding any repeated registrations - * as invalid. + * Input parameters + * db - the app database + * unsortedAliasTxs - array, arbitrary collection of alias-prefixed txs at the alias + * registration address * - * Will get all valid alias registrations if given the full tx history and an empty array - * for registeredAliases + * Outputs + * - The function adds new valid alias txs to the database + * - An array of objects, each one a new valid alias registration tx that was added to the database + * * + * Will get all valid alias registrations if given the full tx history and + * the database is empty */ // Sort aliases such that the earliest aliases are the valid ones @@ -168,37 +173,39 @@ // (and alphabetically first txids to last) for (let i = 0; i < aliasesSortedByTxidAndBlockheight.length; i += 1) { const thisAliasTx = aliasesSortedByTxidAndBlockheight[i]; - const { alias, blockheight } = thisAliasTx; - - // If you haven't seen this alias yet, it's a valid registered alias - if (!registeredAliases.includes(alias)) { - // If the tx is confirmed, add this alias to the registeredAlias array - registeredAliases.push(alias); - // If the tx is confirmed, - if (blockheight < 100000000) { - // Add thisAliasObject to the validAliasObjects array - validAliasRegistrations.push(thisAliasTx); + const { blockheight } = thisAliasTx; + + /* If + * - You haven't seen this alias yet while going through aliasesSortedByTxidAndBlockheight + * - the alias does not exist in the database + * - the transaction is confirmed + * + * Then it is a valid alias + * + * The first two conditions are checked by trying to add this alias to the db + * If it's already there, this will fail, as the database is unique keyed to + * the 'alias' parameter + */ + if (blockheight < config.unconfirmedBlockheight) { + // Attempt to add to the database + const aliasAdded = await addOneAliasToDb(db, thisAliasTx); + console.log(`aliasAdded ${thisAliasTx.alias}: ${aliasAdded}`); + + // If database add is successful, + // add thisAliasObject to the validAliasObjects array + if (aliasAdded) { + // Because thisAliasTx receives an "_id" key on being added to the db, + // clone it without this field to return + const { address, alias, blockheight, txid } = thisAliasTx; + validAliasRegistrations.push({ + address, + alias, + blockheight, + txid, + }); } - } else { - // If you've already seen it at an earlier blockheight or earlier alphabetical txid, - // then this is not a valid registration. - // Do not include it in valid registrations - - // Note, we could just remove this else block. But it's useful for code readability. - continue; } } return validAliasRegistrations; }, - getAliasStringsFromValidAliasTxs: function (validAliases) { - /* Input - * validAliases, an array of alias registrations objects like those - * stored in the validAliases collection of the database - * Output - * an array of strings of all alias registrations - */ - return validAliases.map(aliasObj => { - return aliasObj.alias; - }); - }, }; diff --git a/apps/alias-server/src/db.js b/apps/alias-server/src/db.js --- a/apps/alias-server/src/db.js +++ b/apps/alias-server/src/db.js @@ -105,6 +105,21 @@ return false; } }, + addOneAliasToDb: async function (db, newAliasTx) { + try { + await db + .collection(config.database.collections.validAliases) + .insertOne(newAliasTx); + return true; + } catch (err) { + // Only log some error other than duplicate key error + if (err && err.code !== 11000) { + log(`Error in function addOneAliasToDb.`); + log(err); + } + return false; + } + }, addAliasesToDb: async function (db, newValidAliases) { let validAliasesAddedToDbSuccess; try { @@ -138,4 +153,37 @@ return false; } }, + checkAliasAlreadyRegistered: async function (db, unprocessedAlias) { + // Check the database to see if this alias exists + let findThisAliasResult; + // Query for alias tx in db that has the alias 'unprocessedAlias' + const query = { alias: unprocessedAlias }; + try { + findThisAliasResult = await db + .collection(config.database.collections.validAliases) + .findOne(query); + /* If the alias exists, you will get an object like + * {_id, address, alias, txid, blockheight} + * + * If the alias does not exist, you will get null + */ + return findThisAliasResult !== null; + } catch (err) { + log( + `Error in determining findThisAliasResult in function checkAliasAlreadyRegistered.`, + ); + log(err); + + /* This database lookup function requires special error handling + * + * If there is an error in looking up any alias, we want to notify the admin + * and not include any of the batch of aliases + * + * We do not want to assume the alias cannot be added + * We definitely do not want to return false and assume the alias can be added + * + */ + return 'error'; + } + }, }; diff --git a/apps/alias-server/src/events.js b/apps/alias-server/src/events.js --- a/apps/alias-server/src/events.js +++ b/apps/alias-server/src/events.js @@ -5,8 +5,11 @@ 'use strict'; const config = require('../config'); const log = require('./log'); -const { wait } = require('./utils'); +const { wait, removeUnconfirmedTxsFromTxHistory } = require('./utils'); const { isFinalBlock } = require('./rpc'); +const { getServerState, updateServerState } = require('./db'); +const { getUnprocessedTxHistory } = require('./chronik'); +const { getAliasTxs, getValidAliasRegistrations } = require('./alias'); module.exports = { handleAppStartup: async function ( @@ -122,23 +125,72 @@ return false; } - // TODO Get the valid aliases already in the db + const serverState = await getServerState(db); + if (!serverState) { + // TODO notify admin + return false; + } + + const { processedBlockheight, processedConfirmedTxs } = serverState; + + // If serverState is, somehow, ahead of the calling block, return false + if (processedBlockheight > tipHeight) { + // TODO notify admin + return false; + } - // TODO get server state - // processedConfirmedTxs - count of processed confirmed txs - // processedBlockheight - highest blockheight seen by the server + const allUnprocessedTxs = await getUnprocessedTxHistory( + chronik, + config.aliasConstants.registrationAddress, + processedBlockheight, + processedConfirmedTxs, + ); - // TODO get set of transactions not yet processed by the server - // If app startup, this is full tx history of alias registration address + // Remove unconfirmed txs as these are not eligible for valid alias registrations + const confirmedUnprocessedTxs = + removeUnconfirmedTxsFromTxHistory(allUnprocessedTxs); - // TODO parse tx history for latest valid alias registrations - // with valid format and fee + // Get all potentially valid alias registrations + // i.e. correct fee is paid, prefix is good, everything good but not yet checked against + // conflicting aliases that registered earlier or have alphabetically earlier txid in + // same block + const unprocessedAliasTxs = getAliasTxs( + confirmedUnprocessedTxs, + config.aliasConstants, + ); - // TODO update database with latest valid alias information + // Add new valid alias txs to the database and get a list of what was added + await getValidAliasRegistrations(db, unprocessedAliasTxs); + + // If you processed more confirmed txs for aliases, update serverState appropriately + if (confirmedUnprocessedTxs.length > 0) { + // New processedBlockheight is the highest one seen, or the + // height of the first entry of the confirmedUnprocessedTxs array + // New processedConfirmedTxs is determined by adding the count of now-processed txs + const newServerState = { + processedBlockheight: confirmedUnprocessedTxs[0].block.height, + processedConfirmedTxs: + processedConfirmedTxs + confirmedUnprocessedTxs.length, + }; + // Update serverState + const serverStateUpdated = await updateServerState( + db, + newServerState, + ); + if (!serverStateUpdated) { + // Don't exit loop, since you've already added aliases to the db here + // App will run next on the old server state, so will re-process txs + // These can't be added to the db, so you will get errors + // If you get here, there is something wrong with the server that needs to be checked out + // TODO notify admin + log( + `serverState failed to update to new serverState`, + newServerState, + ); + } + } - // TODO update server state - // TODO If you have new aliases to add to the db, add them + send a tg msg - // TODO If not, exit loop + // TODO telegram notifications for new alias registrations log( `Alias registrations updated to block ${tipHash} at height ${tipHeight}`, diff --git a/apps/alias-server/test/aliasTests.js b/apps/alias-server/test/aliasTests.js --- a/apps/alias-server/test/aliasTests.js +++ b/apps/alias-server/test/aliasTests.js @@ -10,7 +10,6 @@ getAliasTxs, sortAliasTxsByTxidAndBlockheight, getValidAliasRegistrations, - getAliasStringsFromValidAliasTxs, } = require('../src/alias'); const { getOutputScriptFromAddress } = require('../src/utils'); const { @@ -18,8 +17,25 @@ testAddressAliasesWithUnconfirmedTxs, aliases_fake_data, } = require('./mocks/aliasMocks'); +// Mock mongodb +const { MongoClient } = require('mongodb'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { initializeDb, addAliasesToDb } = require('../src/db'); -describe('alias-server alias.js', function () { +describe('alias-server alias.js', async function () { + let mongoServer, testMongoClient; + before(async () => { + // Start mongo memory server before running this suite of unit tests + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + testMongoClient = new MongoClient(mongoUri); + }); + + after(async () => { + // Shut down mongo memory server after running this suite of unit tests + await testMongoClient.close(); + await mongoServer.stop(); + }); it('Correctly parses a 5-character alias transaction', function () { const registrationOutputScript = getOutputScriptFromAddress( config.aliasConstants.registrationAddress, @@ -143,57 +159,88 @@ testAddressAliasesWithUnconfirmedTxs.allAliasTxsSortedByTxidAndBlockheight, ); }); - it('Correctly returns only valid alias registrations at test address ecash:qp3c268rd5946l2f5m5es4x25f7ewu4sjvpy52pqa8', function () { + it('Correctly returns only valid alias registrations at test address ecash:qp3c268rd5946l2f5m5es4x25f7ewu4sjvpy52pqa8 starting with an empty database', async function () { + // Initialize db before each unit test + let testDb = await initializeDb(testMongoClient); + + // Clone unprocessedAliasTxs since the act of adding to db gives it an _id field + const mockAllAliasTxs = JSON.parse( + JSON.stringify(testAddressAliases.allAliasTxs), + ); + assert.deepEqual( - getValidAliasRegistrations([], testAddressAliases.allAliasTxs), + await getValidAliasRegistrations(testDb, mockAllAliasTxs), testAddressAliases.validAliasTxs, ); + + // Wipe the database after this unit test + await testDb.dropDatabase(); }); - it('Correctly returns only new valid alias registrations at test address ecash:qp3c268rd5946l2f5m5es4x25f7ewu4sjvpy52pqa8 given partial txHistory and list of registered aliases', function () { + it('Correctly returns only new valid alias registrations at test address ecash:qp3c268rd5946l2f5m5es4x25f7ewu4sjvpy52pqa8 given partial txHistory and list of registered aliases', async function () { // Take only txs after registration of alias 'bytesofman' + // Note: allAliasTxs are sorted with most recent txs first const unprocessedAliasTxs = testAddressAliases.allAliasTxs.slice( + 0, testAddressAliases.allAliasTxs.findIndex( i => i.alias === 'bytesofman', ), ); + + // Clone unprocessedAliasTxs since the act of adding to db gives it an _id field + const mockUnprocessedAliasTxs = JSON.parse( + JSON.stringify(unprocessedAliasTxs), + ); + // Get list of all valid alias registrations before 'bytesofman' - const registeredAliases = getAliasStringsFromValidAliasTxs( - testAddressAliases.validAliasTxs.slice( - 0, - testAddressAliases.validAliasTxs.findIndex( - i => i.alias === 'bytesofman', - ), - ), + // Note: validAliasTxs are sorted with most recent txs last + // Note: you want to include bytesofman here + const registeredAliases = testAddressAliases.validAliasTxs.slice( + 0, + testAddressAliases.validAliasTxs.findIndex( + i => i.alias === 'bytesofman', + ) + 1, ); // newlyValidAliases will be all the valid alias txs registered after 'bytesofman' + // Note: you do not want bytesofman in this set const newlyValidAliases = testAddressAliases.validAliasTxs.slice( testAddressAliases.validAliasTxs.findIndex( i => i.alias === 'bytesofman', - ), + ) + 1, + ); + // Initialize db before each unit test + let testDb = await initializeDb(testMongoClient); + + // mockRegisteredAliases needs to be a clone of the mock because + // each object gets an _id field when added to the database + const mockRegisteredAliases = JSON.parse( + JSON.stringify(registeredAliases), ); + // Add expected registered aliases to the db + await addAliasesToDb(testDb, mockRegisteredAliases); assert.deepEqual( - getValidAliasRegistrations(registeredAliases, unprocessedAliasTxs), + await getValidAliasRegistrations(testDb, mockUnprocessedAliasTxs), newlyValidAliases, ); + // Wipe the database after this unit test + await testDb.dropDatabase(); }); - it('Correctly returns valid alias registrations at test address ecash:qp3c268rd5946l2f5m5es4x25f7ewu4sjvpy52pqa8 given some unconfirmed txs in history', function () { - assert.deepEqual( - getValidAliasRegistrations( - [], - testAddressAliasesWithUnconfirmedTxs.allAliasTxs, - ), + it('Correctly returns valid alias registrations at test address ecash:qp3c268rd5946l2f5m5es4x25f7ewu4sjvpy52pqa8 given some unconfirmed txs in history', async function () { + // Initialize db before each unit test + let testDb = await initializeDb(testMongoClient); - testAddressAliasesWithUnconfirmedTxs.validAliasTxs, + // Clone all alias mock since the act of adding to db gives it an _id field + const mockAllAliases = JSON.parse( + JSON.stringify(testAddressAliasesWithUnconfirmedTxs.allAliasTxs), ); - }); - it('getAliasStringsFromValidAliasTxs returns an array of string of the alias object key from an array of valid alias registrations', function () { + + // This tests startup condition, so add no aliases to the database assert.deepEqual( - getAliasStringsFromValidAliasTxs( - testAddressAliasesWithUnconfirmedTxs.validAliasTxs, - ), - testAddressAliasesWithUnconfirmedTxs.validAliasStrings, + await getValidAliasRegistrations(testDb, mockAllAliases), + testAddressAliasesWithUnconfirmedTxs.validAliasTxs, ); + // Wipe the database after this unit test + await testDb.dropDatabase(); }); }); diff --git a/apps/alias-server/test/chronikWsHandlerTests.js b/apps/alias-server/test/chronikWsHandlerTests.js --- a/apps/alias-server/test/chronikWsHandlerTests.js +++ b/apps/alias-server/test/chronikWsHandlerTests.js @@ -4,6 +4,7 @@ 'use strict'; const assert = require('assert'); +const config = require('../config'); const cashaddr = require('ecashaddrjs'); const { initializeWebsocket, @@ -14,8 +15,36 @@ const mockSecrets = require('../secrets.sample'); const MockAdapter = require('axios-mock-adapter'); const axios = require('axios'); +// Mock mongodb +const { initializeDb } = require('../src/db'); +const { MongoClient } = require('mongodb'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { testAddressAliases } = require('./mocks/aliasMocks'); describe('alias-server chronikWsHandler.js', async function () { + let mongoServer, testMongoClient; + before(async () => { + // Start mongo memory server before running this suite of unit tests + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + testMongoClient = new MongoClient(mongoUri); + }); + + after(async () => { + // Shut down mongo memory server after running this suite of unit tests + await testMongoClient.close(); + await mongoServer.stop(); + }); + + let testDb; + beforeEach(async () => { + // Initialize db before each unit test + testDb = await initializeDb(testMongoClient); + }); + afterEach(async () => { + // Wipe the database after each unit test + await testDb.dropDatabase(); + }); it('initializeWebsocket returns expected websocket object for a p2pkh address', async function () { const wsTestAddress = 'ecash:qp3c268rd5946l2f5m5es4x25f7ewu4sjvpy52pqa8'; @@ -77,7 +106,7 @@ it('parseWebsocketMessage correctly processes a chronik websocket BlockConnected message if block is avalanche finalized', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); - const db = null; + const db = testDb; const telegramBot = null; const channelId = null; const { avalancheRpc } = mockSecrets; @@ -97,6 +126,16 @@ output: mockBlock, }); + // Add tx history to mockedChronik + // Set the script + const { type, hash } = cashaddr.decode( + config.aliasConstants.registrationAddress, + true, + ); + mockedChronik.setScript(type, hash); + // Set the mock tx history + mockedChronik.setTxHistory(testAddressAliases.txHistory); + // Mock avalanche RPC call // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); @@ -162,7 +201,7 @@ it('If parseWebsocketMessage is called before a previous call to handleBlockConnected has completed, the next call to handleBlockConnected will not enter until the first is completed', async function () { // Initialize mocks for the first call to parseWebsocketMessage const mockedChronik = new MockChronikClient(); - const db = null; + const db = testDb; const telegramBot = null; const channelId = null; const { avalancheRpc } = mockSecrets; @@ -183,6 +222,16 @@ output: mockBlock, }); + // Add tx history to mockedChronik + // Set the script + const { type, hash } = cashaddr.decode( + config.aliasConstants.registrationAddress, + true, + ); + mockedChronik.setScript(type, hash); + // Set the mock tx history + mockedChronik.setTxHistory(testAddressAliases.txHistory); + // Mock avalanche RPC call // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); @@ -205,11 +254,20 @@ height: 786879, }, }; + // Tell mockedChronik what response we expect nextMockedChronik.setMock('block', { input: nextWsMsg.blockHash, output: nextMockBlock, }); + + // Add tx history to nextMockedChronik + // Set the script + nextMockedChronik.setScript(type, hash); + // Set the mock tx history + // For now, assume it's the same as before, i.e. no new txs found + nextMockedChronik.setTxHistory(testAddressAliases.txHistory); + const firstCallPromise = parseWebsocketMessage( mockedChronik, db, diff --git a/apps/alias-server/test/dbTests.js b/apps/alias-server/test/dbTests.js --- a/apps/alias-server/test/dbTests.js +++ b/apps/alias-server/test/dbTests.js @@ -7,8 +7,10 @@ initializeDb, getServerState, updateServerState, + addOneAliasToDb, addAliasesToDb, getAliasesFromDb, + checkAliasAlreadyRegistered, } = require('../src/db'); // Mock mongodb const { MongoClient } = require('mongodb'); @@ -46,6 +48,7 @@ it('getServerState returns expected initial server state on initialized database', async function () { // Check that serverState was initialized properly const initialServerState = await getServerState(testDb); + assert.deepEqual(initialServerState, { processedBlockheight: 0, processedConfirmedTxs: 0, @@ -95,7 +98,6 @@ const fetchedServerStateOnRestart = await getServerState( testDbOnRestart, ); - // Verify serverState has not reverted to initial value assert.deepEqual(fetchedServerStateOnRestart, newServerState); }); it('addAliasesToDb successfully adds new valid aliases to an empty collection', async function () { @@ -112,6 +114,94 @@ // Verify addedValidAliases match the added mock assert.deepEqual(addedValidAliases, testAddressAliases.validAliasTxs); }); + it('addOneAliasToDb successfully adds a new valid alias to an empty collection', async function () { + // newValidAliases needs to be a clone of the mock because + // each object gets an _id field when added to the database + const newValidAliases = JSON.parse( + JSON.stringify(testAddressAliases.validAliasTxs), + ); + const aliasAddedSuccess = await addOneAliasToDb( + testDb, + newValidAliases[0], + ); + // Get the newly added valid aliases + // Note we return valid aliases without the database _id field + const addedValidAliases = await getAliasesFromDb(testDb); + + // Verify the function returns true on alias add success + assert.strictEqual(aliasAddedSuccess, true); + // Verify the database has the expected alias + assert.deepEqual(addedValidAliases, [ + testAddressAliases.validAliasTxs[0], + ]); + }); + it('addOneAliasToDb successfully adds a new valid alias to an existing collection', async function () { + // newValidAliases needs to be a clone of the mock because + // each object gets an _id field when added to the database + const newValidAliases = JSON.parse( + JSON.stringify(testAddressAliases.validAliasTxs), + ); + // Pre-populate the aliases collection + await addAliasesToDb(testDb, newValidAliases); + + const newMockAlias = { + address: 'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48', + alias: 'rico', + txid: '3ff9c28fa07cb88c87000ef0f5ee61953d874ffade154cd3f88fd60b88ea2879', + blockheight: 787674, + }; + + // clone to check unit test result as _id will be added to newMockAlias + const newMockAliasClone = JSON.parse(JSON.stringify(newMockAlias)); + + // Add an alias tx that does not exist + const aliasAddedSuccess = await addOneAliasToDb(testDb, newMockAlias); + // Get the newly added valid aliases + // Note we return valid aliases without the database _id field + const addedValidAliases = await getAliasesFromDb(testDb); + + // Verify the function returns true on alias add success + assert.strictEqual(aliasAddedSuccess, true); + // Verify the database has the expected alias + assert.deepEqual( + addedValidAliases, + testAddressAliases.validAliasTxs.concat(newMockAliasClone), + ); + }); + it('addOneAliasToDb returns false and fails to add an alias if it is already in the database', async function () { + // newValidAliases needs to be a clone of the mock because + // each object gets an _id field when added to the database + const newValidAliases = JSON.parse( + JSON.stringify(testAddressAliases.validAliasTxs), + ); + // Pre-populate the aliases collection + await addAliasesToDb(testDb, newValidAliases); + + const newMockAliasTxRegisteringExistingAlias = { + address: 'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed', + alias: 'foo', + txid: 'f41ccfbd88d228bbb695b771dd0c266b0351eda9a35aeb8c5e3cb7670e7e17cc', + blockheight: 776576, + }; + + // Add an alias tx that does not exist + const aliasAddedSuccess = await addOneAliasToDb( + testDb, + newMockAliasTxRegisteringExistingAlias, + ); + // Get the newly added valid aliases + // Note we return valid aliases without the database _id field + const addedValidAliases = await getAliasesFromDb(testDb); + + // Verify the function returns true on alias add success + assert.strictEqual(aliasAddedSuccess, false); + // Verify the database has the expected aliases (without the failed add) + assert.deepEqual(addedValidAliases, testAddressAliases.validAliasTxs); + }); + it('getAliasesFromDb returns an empty array if no aliases have been added to the collection', async function () { + const validAliases = await getAliasesFromDb(testDb); + assert.deepEqual(validAliases, []); + }); it('addAliasesToDb returns false if you attempt to add aliases whose txid already exists in the database', async function () { // Startup the app and initialize serverState const testDb = await initializeDb(testMongoClient); @@ -134,4 +224,41 @@ // Verify addAliasesToDb returned false on attempt to add duplicate aliases to the db assert.deepEqual(failedResult, false); }); + it('checkAliasAlreadyRegistered returns false if alias is not in an empty database collection', async function () { + const aliasAlreadyRegistered = await checkAliasAlreadyRegistered( + testDb, + 'sometestalias', + ); + assert.strictEqual(aliasAlreadyRegistered, false); + }); + it('checkAliasAlreadyRegistered returns true for an alias that already exists in the database collection', async function () { + // Add aliases to the database + // aliasesInDb needs to be a clone of the mock because + // each object gets an _id field when added to the database + const aliasesInDb = JSON.parse( + JSON.stringify(testAddressAliases.validAliasTxs), + ); + await addAliasesToDb(testDb, aliasesInDb); + + const aliasAlreadyRegistered = await checkAliasAlreadyRegistered( + testDb, + 'nfs', + ); + assert.strictEqual(aliasAlreadyRegistered, true); + }); + it('checkAliasAlreadyRegistered returns false for an alias that does not exist in a non-empty database collection', async function () { + // Add aliases to the database + // aliasesInDb needs to be a clone of the mock because + // each object gets an _id field when added to the database + const aliasesInDb = JSON.parse( + JSON.stringify(testAddressAliases.validAliasTxs), + ); + await addAliasesToDb(testDb, aliasesInDb); + + const aliasAlreadyRegistered = await checkAliasAlreadyRegistered( + testDb, + 'thisoneisnew', + ); + assert.strictEqual(aliasAlreadyRegistered, false); + }); }); diff --git a/apps/alias-server/test/eventsTests.js b/apps/alias-server/test/eventsTests.js --- a/apps/alias-server/test/eventsTests.js +++ b/apps/alias-server/test/eventsTests.js @@ -3,14 +3,44 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. 'use strict'; +const config = require('../config'); const assert = require('assert'); +const cashaddr = require('ecashaddrjs'); const mockSecrets = require('../secrets.sample'); -const { handleAppStartup } = require('../src/events'); +const { handleAppStartup, handleBlockConnected } = require('../src/events'); const { MockChronikClient } = require('./mocks/chronikMock'); const MockAdapter = require('axios-mock-adapter'); const axios = require('axios'); +// Mock mongodb +const { initializeDb, updateServerState } = require('../src/db'); +const { MongoClient } = require('mongodb'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { testAddressAliases } = require('./mocks/aliasMocks'); describe('alias-server events.js', async function () { + let mongoServer, testMongoClient; + before(async () => { + // Start mongo memory server before running this suite of unit tests + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + testMongoClient = new MongoClient(mongoUri); + }); + + after(async () => { + // Shut down mongo memory server after running this suite of unit tests + await testMongoClient.close(); + await mongoServer.stop(); + }); + + let testDb; + beforeEach(async () => { + // Initialize db before each unit test + testDb = await initializeDb(testMongoClient); + }); + afterEach(async () => { + // Wipe the database after each unit test + await testDb.dropDatabase(); + }); it('handleAppStartup calls handleBlockConnected with tipHeight and completes function if block is avalanche finalized', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); @@ -27,6 +57,16 @@ output: mockBlockchaininfoResponse, }); + // Add tx history to mockedChronik + // Set the script + const { type, hash } = cashaddr.decode( + config.aliasConstants.registrationAddress, + true, + ); + mockedChronik.setScript(type, hash); + // Set the mock tx history + mockedChronik.setTxHistory(testAddressAliases.txHistory); + // Mock avalanche RPC call // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); @@ -37,7 +77,7 @@ id: 'isfinalblock', }); - const db = null; + const db = testDb; const telegramBot = null; const channelId = null; const { avalancheRpc } = mockSecrets; @@ -128,6 +168,128 @@ avalancheRpc, ); + assert.deepEqual(result, false); + }); + it('handleBlockConnected returns false if the function fails to obtain serverState', async function () { + // tipHash called with + const tipHash = + '00000000000000000b0519ddbffcf6dbab212b95207e398ae3ed2ba312fa561d'; + + // Initialize chronik mock + const mockedChronik = new MockChronikClient(); + + const mockBlock = { + blockInfo: { + height: 783136, + }, + }; + + // Tell mockedChronik what response we expect + mockedChronik.setMock('block', { + input: tipHash, + output: mockBlock, + }); + + // Add tx history to mockedChronik + // Set the script + const { type, hash } = cashaddr.decode( + config.aliasConstants.registrationAddress, + true, + ); + mockedChronik.setScript(type, hash); + // Set the mock tx history + mockedChronik.setTxHistory(testAddressAliases.txHistory); + + // Mock avalanche RPC call + // onNoMatch: 'throwException' helps to debug if mock is not being used + const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); + // Mock response for rpc return of true for isfinalblock method + mock.onPost().reply(200, { + result: true, + error: null, + id: 'isfinalblock', + }); + + const telegramBot = null; + const channelId = null; + const { avalancheRpc } = mockSecrets; + + // Rename the serverState collection + await testDb + .collection(config.database.collections.serverState) + .rename('notTheSameName'); + + const result = await handleBlockConnected( + mockedChronik, + testDb, + telegramBot, + channelId, + avalancheRpc, + tipHash, + ); + + assert.deepEqual(result, false); + }); + it('handleBlockConnected returns false if called with a block of height lower than serverState', async function () { + // tipHash called with + const tipHash = + '00000000000000000b0519ddbffcf6dbab212b95207e398ae3ed2ba312fa561d'; + + // Initialize chronik mock + const mockedChronik = new MockChronikClient(); + + const mockBlock = { + blockInfo: { + height: 783136, + }, + }; + + // Tell mockedChronik what response we expect + mockedChronik.setMock('block', { + input: tipHash, + output: mockBlock, + }); + + // Add tx history to mockedChronik + // Set the script + const { type, hash } = cashaddr.decode( + config.aliasConstants.registrationAddress, + true, + ); + mockedChronik.setScript(type, hash); + // Set the mock tx history + mockedChronik.setTxHistory(testAddressAliases.txHistory); + + // Mock avalanche RPC call + // onNoMatch: 'throwException' helps to debug if mock is not being used + const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); + // Mock response for rpc return of true for isfinalblock method + mock.onPost().reply(200, { + result: true, + error: null, + id: 'isfinalblock', + }); + + const telegramBot = null; + const channelId = null; + const { avalancheRpc } = mockSecrets; + + // Give the app a serverState in the future of the calling block + const mockServerState = { + processedBlockheight: 783137, + processedConfirmedTxs: 100, + }; + await updateServerState(testDb, mockServerState); + + const result = await handleBlockConnected( + mockedChronik, + testDb, + telegramBot, + channelId, + avalancheRpc, + tipHash, + ); + assert.deepEqual(result, false); }); }); diff --git a/apps/alias-server/test/mainTests.js b/apps/alias-server/test/mainTests.js --- a/apps/alias-server/test/mainTests.js +++ b/apps/alias-server/test/mainTests.js @@ -9,6 +9,7 @@ const mockSecrets = require('../secrets.sample'); const MockAdapter = require('axios-mock-adapter'); const axios = require('axios'); +const { testAddressAliases } = require('./mocks/aliasMocks'); // Mock mongodb const { MongoClient } = require('mongodb'); @@ -16,22 +17,20 @@ // Mock chronik const { MockChronikClient } = require('./mocks/chronikMock'); -let mongoServer, testMongoClient; -before(async () => { - mongoServer = await MongoMemoryServer.create(); - const mongoUri = mongoServer.getUri(); - testMongoClient = new MongoClient(mongoUri); -}); +describe('alias-server main.js', async function () { + let mongoServer, testMongoClient; + before(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + testMongoClient = new MongoClient(mongoUri); + }); -after(async () => { - await testMongoClient.close(); - await mongoServer.stop(); -}); + after(async () => { + await testMongoClient.close(); + await mongoServer.stop(); + }); -describe('alias-server main.js', async function () { it('main() intializes database correctly, connects to a websocket, and runs handleAppStartup() correctly', async function () { - // Define params - const mongoClient = testMongoClient; // Initialize chronik mock const mockedChronik = new MockChronikClient(); const mockBlockchaininfoResponse = { @@ -46,6 +45,16 @@ output: mockBlockchaininfoResponse, }); + // Add tx history to mockedChronik + // Set the script + const { type, hash } = cashaddr.decode( + config.aliasConstants.registrationAddress, + true, + ); + mockedChronik.setScript(type, hash); + // Set the mock tx history + mockedChronik.setTxHistory(testAddressAliases.txHistory); + // Mock avalanche RPC call // onNoMatch: 'throwException' helps to debug if mock is not being used const mock = new MockAdapter(axios, { onNoMatch: 'throwException' }); @@ -55,6 +64,9 @@ error: null, id: 'isfinalblock', }); + + // Define params + const mongoClient = testMongoClient; const chronik = mockedChronik; const address = config.aliasConstants.registrationAddress; const telegramBot = null; @@ -70,7 +82,6 @@ avalancheRpc, returnMocks, ); - const { type, hash } = cashaddr.decode(address, true); // Check that the database was initialized properly assert.strictEqual(result.db.namespace, config.database.name); // Check that websocket is connected