diff --git a/web/alias-server/src/chronik.js b/web/alias-server/src/chronik.js --- a/web/alias-server/src/chronik.js +++ b/web/alias-server/src/chronik.js @@ -6,6 +6,26 @@ module.exports = { chronik, + getChaintip: async function () { + let info; + try { + info = await chronik.blockchainInfo(); + return info; + } catch (err) { + log(`Error in getChainTip()`, err); + return false; + } + }, + getBlockDetails: async function (blockhash) { + let blockdetails; + try { + blockdetails = await chronik.block(blockhash); + return blockdetails; + } catch (err) { + log(`Error in getBlockDetails()`, err); + return false; + } + }, getTxHistoryPage: async function (hash160, page = 0) { let txHistoryPage; try { diff --git a/web/alias-server/src/websocket.js b/web/alias-server/src/websocket.js --- a/web/alias-server/src/websocket.js +++ b/web/alias-server/src/websocket.js @@ -10,7 +10,8 @@ removeUnconfirmedTxsFromTxHistory, } = require('./utils'); const { returnTelegramBotSendMessagePromise } = require('./telegram'); -const { chronik } = require('./chronik'); +const { chronik, getChaintip, getBlockDetails } = require('./chronik'); +const { isFinalBlock } = require('./rpc'); const axios = require('axios'); module.exports = { @@ -34,16 +35,117 @@ // Determine type of tx const { type } = wsMsg; log(`msg type: ${type}`); - let isAppStartup = type === 'startup'; // type can be AddedToMempool, BlockConnected, or Confirmed // For now, we are only interested in "Confirmed", as only these are valid // We will want to look at AddedToMempool to process pending alias registrations later + let tipHash, tipHeight, chaintipInfo; switch (type) { case 'startup': + log(`Checking for new aliases on startup`); + + // If this is app startup, get the latest tipHash and tipHeight by querying the blockchain + chaintipInfo = await getChaintip(); + + if (!chaintipInfo) { + // If you have an API call error on app startup, exit the app + // Chronik must be online to add new valid aliases + log(`Exiting app startup due to error in getChaintip()`); + return process.exit(1); + } + + tipHash = chaintipInfo.tipHash; + tipHeight = chaintipInfo.tipHeight; + + // Fallthrough case 'BlockConnected': { - isAppStartup - ? log(`Checking for new aliases on startup`) - : log(`New block found: ${wsMsg.blockHash}`); + /* + * BlockConnected callback + * + * This is where alias-server queries the blockchain for new transactions and + * parses those transactions to determine if any are valid alias registrations + * + * The database may only be updated if we have a known blockhash and height with + * isFinalBlock = true confirmed by avalanche + * + * A number of error conditions may cause the loop to exit before any update to + * the database occurs. + * + * If alias-server determines a blockhash and height with isFinalBlock === true, + * valid alias registrations will be processed up to and including that blockheight + * + * Otherwise the loop will exit before any updates are made to the database + * + * Note: websockets disconnect and reconnect frequently. It cannot be assumed that + * every found block will triggger this loop. So, the loop must be designed such that + * it will always update for all unseen valid alias registrations. + * + */ + + // Use the block just found by chronik as your most recent block + if (typeof tipHash === 'undefined') { + tipHash = wsMsg.blockHash; + log(`New block found: ${tipHash}`); + } + + // Get tip height + const blockdetails = await getBlockDetails(tipHash); + if (!blockdetails) { + // If you have an API call error, do not complete the loop + // Chronik must be online to add new valid aliases + log( + `Exiting loop triggered by block ${tipHash} due to error in getBlockDetails()`, + ); + // TODO admin notification in diff enabling this feature + return; + } + + // chronik blockdetails returns the block height at the 'blockInfo.height' key + tipHeight = blockdetails.blockInfo.height; + // If you don't get what you expect for tipHeight, exit the loop + // Should be a number e.g. 785748 + if (typeof tipHeight !== 'number') { + return; + } + + // Initialize isAvalancheFinalized as false. Only set to true if you + // prove it so with a node rpc call + let isAvalancheFinalized = false; + + // Wait 10s before checking this block for avalanche finality + await new Promise(resolve => setTimeout(resolve, 10000)); + + // Check to see if block tipHash has been finalized by avalanche + try { + isAvalancheFinalized = await isFinalBlock(tipHash); + } catch (err) { + log( + `Error checking avalanche final status of block ${tipHash}`, + err, + ); + } + + // Exit this loop if your block is not finalized by avalanche + if (!isAvalancheFinalized) { + /* If this is normal operation, i.e. loop triggered by chronik websocket + * msg confirming a new block has been found, exit loop. + * You might not update valid aliases until the next block + * This is an acceptable condition, and already must be designed for as + * websocket disconnects mean the app does not receive a msg for every found block + */ + log( + `Block ${tipHash} is not avalanche finalized. Exiting loop before processing any alias txs.`, + ); + return; + } + + // If you are here, you have a finalized block of known height and hash + log( + `Chaintip ${tipHeight}:${tipHash} is finalized by avalanche.`, + ); + + log( + `Transactions with blockheight ${tipHeight} or lower may be valid alias registrations.`, + ); // Get the valid aliases already in the db let validAliasesInDb;