diff --git a/apps/alias-server/index.js b/apps/alias-server/index.js --- a/apps/alias-server/index.js +++ b/apps/alias-server/index.js @@ -24,12 +24,7 @@ polling: true, }); -async function main( - chronik, - telegramBot, - channelId, - avalancheCheckWaitInterval, -) { +async function main(chronik, telegramBot, channelId, avalancheRpc) { // Initialize db connection const db = await initializeDb(); @@ -39,7 +34,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, ); if (aliasWebsocket && aliasWebsocket._subs && aliasWebsocket._subs[0]) { const subscribedHash160 = aliasWebsocket._subs[0].scriptPayload; @@ -47,13 +42,7 @@ } // Get the latest alias information on app startup - await handleAppStartup( - chronik, - db, - telegramBot, - channelId, - avalancheCheckWaitInterval, - ); + await handleAppStartup(chronik, db, telegramBot, channelId, avalancheRpc); // Set up your API endpoints const app = express(); @@ -81,4 +70,4 @@ app.listen(config.express.port); } -main(chronik, telegramBot, channelId, config.avalancheCheckWaitInterval); +main(chronik, telegramBot, channelId, secrets.avalancheRpc); 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 @@ -6,6 +6,7 @@ const config = require('../config'); const log = require('./log'); const { wait } = require('./utils'); +const { isFinalBlock } = require('./rpc'); module.exports = { handleAppStartup: async function ( @@ -13,7 +14,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, ) { log(`Checking for new aliases on startup`); // If this is app startup, get the latest tipHash and tipHeight by querying the blockchain @@ -35,7 +36,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, tipHash, tipHeight, ); @@ -47,7 +48,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, tipHash, tipHeight, ) { @@ -90,10 +91,35 @@ } } + // Initialize isAvalancheFinalized as false. Only set to true if you + // prove it so with a node rpc call + let isAvalancheFinalized = false; + for (let i = 0; i < config.avalancheCheckCount; i += 1) { - // TODO check isFinalBlock - wait(avalancheCheckWaitInterval); - // TODO if isFinalBlock, break loop + // Check to see if block tipHash has been finalized by avalanche + try { + isAvalancheFinalized = await isFinalBlock( + avalancheRpc, + tipHash, + ); + } catch (err) { + log(`Error in isFinalBlock for ${tipHash}`, err); + } + if (isAvalancheFinalized) { + // If isAvalancheFinalized, stop checking + break; + } + wait(config.avalancheCheckWaitInterval); + } + + if (!isAvalancheFinalized) { + log( + `Block ${tipHash} is not avalanche finalized after ${ + config.avalancheCheckWaitInterval * + config.avalancheCheckCount + } ms. Exiting handleBlockConnected().`, + ); + return false; } // TODO Get the valid aliases already in the db diff --git a/apps/alias-server/src/websocket.js b/apps/alias-server/src/websocket.js --- a/apps/alias-server/src/websocket.js +++ b/apps/alias-server/src/websocket.js @@ -14,7 +14,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, ) { // Subscribe to chronik websocket const ws = chronik.ws({ @@ -24,7 +24,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, msg, ); }, @@ -41,7 +41,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, wsMsg = { type: 'BlockConnected' }, ) { log(`parseWebsocketMessage called on`, wsMsg); @@ -60,7 +60,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, wsMsg.blockHash, ); case 'AddedToMempool': 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 @@ -4,11 +4,14 @@ 'use strict'; const assert = require('assert'); +const mockSecrets = require('../secrets.sample'); const { handleAppStartup } = require('../src/events'); const { MockChronikClient } = require('./mocks/chronikMock'); +const MockAdapter = require('axios-mock-adapter'); +const axios = require('axios'); describe('alias-server events.js', async function () { - it('handleAppStartup calls handleBlockConnected with tipHeight', async function () { + it('handleAppStartup calls handleBlockConnected with tipHeight and completes function if block is avalanche finalized', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); @@ -24,17 +27,27 @@ output: mockBlockchaininfoResponse, }); + // 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 db = null; const telegramBot = null; const channelId = null; - const avalancheCheckWaitInterval = 0; + const { avalancheRpc } = mockSecrets; const result = await handleAppStartup( mockedChronik, db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, ); assert.deepEqual( @@ -42,6 +55,47 @@ `Alias registrations updated to block ${mockBlockchaininfoResponse.tipHash} at height ${mockBlockchaininfoResponse.tipHeight}`, ); }); + it('handleAppStartup calls handleBlockConnected with tipHeight and returns false if block is not avalanche finalized', async function () { + // Initialize chronik mock + const mockedChronik = new MockChronikClient(); + + const mockBlockchaininfoResponse = { + tipHash: + '00000000000000000ce690f27bc92c46863337cc9bd5b7c20aec094854db26e3', + tipHeight: 786878, + }; + + // Tell mockedChronik what response we expect + mockedChronik.setMock('blockchainInfo', { + input: null, + output: mockBlockchaininfoResponse, + }); + + // 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: false, + error: null, + id: 'isfinalblock', + }); + + const db = null; + const telegramBot = null; + const channelId = null; + const { avalancheRpc } = mockSecrets; + + const result = await handleAppStartup( + mockedChronik, + db, + telegramBot, + channelId, + avalancheRpc, + ); + + assert.deepEqual(result, false); + }); it('handleAppStartup returns false on chronik error', async function () { // Initialize chronik mock const mockedChronik = new MockChronikClient(); @@ -59,17 +113,19 @@ output: mockBlockchaininfoResponse, }); + // Function will not get to RPC call, no need for axios mock + const db = null; const telegramBot = null; const channelId = null; - const avalancheCheckWaitInterval = 0; + const { avalancheRpc } = mockSecrets; const result = await handleAppStartup( mockedChronik, db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, ); assert.deepEqual(result, false); diff --git a/apps/alias-server/test/mocks/chronikResponses.js b/apps/alias-server/test/mocks/chronikResponses.js new file mode 100644 --- /dev/null +++ b/apps/alias-server/test/mocks/chronikResponses.js @@ -0,0 +1,11 @@ +// Copyright (c) 2023 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +'use strict'; +module.exports = { + mockBlock: { + blockInfo: { + height: 786878, + }, + }, +}; diff --git a/apps/alias-server/test/websocketTests.js b/apps/alias-server/test/websocketTests.js --- a/apps/alias-server/test/websocketTests.js +++ b/apps/alias-server/test/websocketTests.js @@ -10,6 +10,10 @@ parseWebsocketMessage, } = require('../src/websocket'); const { MockChronikClient } = require('./mocks/chronikMock'); +const { mockBlock } = require('./mocks/chronikResponses'); +const mockSecrets = require('../secrets.sample'); +const MockAdapter = require('axios-mock-adapter'); +const axios = require('axios'); describe('alias-server websocket.js', async function () { it('initializeWebsocket returns expected websocket object for a p2pkh address', async function () { @@ -21,7 +25,7 @@ const db = null; const telegramBot = null; const channelId = null; - const avalancheCheckWaitInterval = 0; + const { avalancheRpc } = mockSecrets; const result = await initializeWebsocket( mockedChronik, @@ -29,7 +33,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, ); // Confirm websocket opened @@ -50,7 +54,7 @@ const db = null; const telegramBot = null; const channelId = null; - const avalancheCheckWaitInterval = 0; + const { avalancheRpc } = mockSecrets; const result = await initializeWebsocket( mockedChronik, @@ -58,7 +62,7 @@ db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, ); // Confirm websocket opened @@ -70,13 +74,13 @@ { scriptType: type, scriptPayload: hash }, ]); }); - it('parseWebsocketMessage correctly processes a chronik websocket BlockConnected message', async function () { + 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 telegramBot = null; const channelId = null; - const avalancheCheckWaitInterval = 0; + const { avalancheRpc } = mockSecrets; const wsMsg = { type: 'BlockConnected', blockHash: @@ -92,12 +96,22 @@ input: wsMsg.blockHash, output: mockBlock, }); + + // 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 result = await parseWebsocketMessage( mockedChronik, db, telegramBot, channelId, - avalancheCheckWaitInterval, + avalancheRpc, wsMsg, ); @@ -106,4 +120,43 @@ `Alias registrations updated to block ${wsMsg.blockHash} at height ${mockBlock.blockInfo.height}`, ); }); + it('parseWebsocketMessage calls handleBlockConnected, which exits if block is not avalanche finalized', async function () { + // Initialize chronik mock + const mockedChronik = new MockChronikClient(); + const db = null; + const telegramBot = null; + const channelId = null; + const { avalancheRpc } = mockSecrets; + const wsMsg = { + type: 'BlockConnected', + blockHash: + '000000000000000015713b0407590ab1481fd7b8430f87e19cf768bec285ad55', + }; + + // Tell mockedChronik what response we expect + mockedChronik.setMock('block', { + input: wsMsg.blockHash, + output: mockBlock, + }); + + // 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: false, + error: null, + id: 'isfinalblock', + }); + const result = await parseWebsocketMessage( + mockedChronik, + db, + telegramBot, + channelId, + avalancheRpc, + wsMsg, + ); + + assert.deepEqual(result, false); + }); });