diff --git a/qortal-ui-plugins/plugins/core/streams/AddressWatcher.js b/qortal-ui-plugins/plugins/core/streams/AddressWatcher.js new file mode 100644 index 00000000..c214373a --- /dev/null +++ b/qortal-ui-plugins/plugins/core/streams/AddressWatcher.js @@ -0,0 +1,101 @@ +import { parentEpml } from '../connect.js' + +// Tests to see if a block or transaction should trigger an address reload...but we're not doing that yet, because of no time for good testing +const transactionTests = [] +const blockTests = [] + +const DEFAULT_ADDRESS_INFO = {} + +transactionTests.push((tx, addr) => { + return tx.recipient === addr || tx.sender === addr +}) + +blockTests.push((block, addr) => { + return block.generator === addr +}) + +export class AddressWatcher { + constructor (addresses) { + addresses = addresses || [] + this.reset() + + addresses.forEach(addr => this.addAddress(addr)) + } + + reset () { + this._addresses = {} + this._addressStreams = {} + } + + // Adds an address to watch + addAddress (address) { + const addr = address.address + this._addresses[addr] = address + + this._addressStreams[addr] = new EpmlStream(`address/${addr}`, () => this._addresses[addr]) + + this.updateAddress(addr) + } + + async testBlock (block) { + // console.log('TESTING BLOCK') + const pendingUpdateAddresses = [] + + // blockTests.forEach(fn => { + + // }) + // transactionTests.forEach(fn => { + // + const transactions = await parentEpml.request('apiCall', { url: `/transactions/block/${block.signature}` }) + transactions.forEach(transaction => { + // console.log(this) + // fn(transaction, Object.keys(this._addresses)) + // Guess the block needs transactions + for (const addr of Object.keys(this._addresses)) { + // const addrChanged = transactionTests.some(fn => { + // return fn(transaction, addr) + // }) + // console.log('checking ' + addr) + const addrChanged = true // Just update it every block...for now + if (!addrChanged) return + + if (!(addr in pendingUpdateAddresses)) pendingUpdateAddresses.push(addr) + /** + * In the future transactions are potentially stored from here...and address is updated excluding transactions...and also somehow manage tx pages... + * Probably will just make wallet etc. listen for address change and then do the api call itself. If tx. page is on, say, page 3...and there's a new transaction... + * it will refresh, changing the "page" to have 1 extra transaction at the top and losing 1 at the bottom (pushed to next page) + */ + } + }) + pendingUpdateAddresses.forEach(addr => this.updateAddress(addr)) + } + + async updateAddress (addr) { + // console.log('UPPPDDAAATTTINGGG AADDDRRR', addr) + let addressRequest = await parentEpml.request('apiCall', { + type: 'explorer', + data: { + addr: addr, + txOnPage: 10 + } + }) + // addressRequest = JSON.parse(addressRequest) + // console.log(addressRequest, 'AAADDDREESS REQQUEESTT') + // console.log('response: ', addressRequest) + + const addressInfo = addressRequest.success ? addressRequest.data : DEFAULT_ADDRESS_INFO + // const addressInfo = addressRequest.success ? addressRequest.data : DEFAULT_ADDRESS_INFO + addressInfo.transactions = [] + + for (let i = addressInfo.start; i >= addressInfo.end; i--) { + addressInfo.transactions.push(addressInfo[i]) + delete addressInfo[i] + } + // console.log('ADDRESS INFO', addressInfo) + if (!(addr in this._addresses)) return + + this._addresses[addr] = addressInfo + // console.log('---------------------------Emitting-----------------------------', this._addresses[addr], this._addressStreams[addr]) + this._addressStreams[addr].emit(addressInfo) + } +} diff --git a/qortal-ui-plugins/plugins/core/streams/UnconfirmedTransactionWatcher.js b/qortal-ui-plugins/plugins/core/streams/UnconfirmedTransactionWatcher.js new file mode 100644 index 00000000..e4f02f44 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/streams/UnconfirmedTransactionWatcher.js @@ -0,0 +1,65 @@ +import { parentEpml } from '../connect.js' +// import { EpmlStream } from 'epml' maybe not needed...loaded for me + +export class UnconfirmedTransactionWatcher { + constructor () { + this._unconfirmedTransactionStreams = {} + this.reset() // Sets defaults + + setInterval(() => { + Object.entries(this._addresses).forEach((addr) => this._addressTransactionCheck(addr[0])) + }, 10 * 1000) + } + + reset () { + this._addresses = {} + this._addressesUnconfirmedTransactions = {} + } + + // Adds an address to watch + addAddress (address) { + // console.log("Added address", address) + const addr = address.address + this._addresses[addr] = address + this._addressesUnconfirmedTransactions[addr] = [] + + if (this._unconfirmedTransactionStreams[addr]) return + // console.log("CREATING A STRTRREEAAAMMMM") + this._unconfirmedTransactionStreams[addr] = new EpmlStream(`unconfirmedOfAddress/${addr}`, () => this._addressesUnconfirmedTransactions[addr]) + + // this.updateAddress(address.address) + } + + check () { + // console.log("checkin for unconfirmed") + const c = this._addressTransactionCheck() + .then(() => setTimeout(() => this.check(), 5000)) + .catch(() => setTimeout(() => this.check(), 5000)) + // console.log(c) + } + + async _addressTransactionCheck () { + // console.log("Checking for unconfirmed transactions") + // console.log(this._addresses, Object.keys(this._addresses)) + return Promise.all(Object.keys(this._addresses).map(addr => { + // console.log(`checking ${addr}`) + return parentEpml.request('apiCall', { + type: 'api', + // url: `transactions/unconfirmedof/${addr}` + url: `/transactions/unconfirmed` + }).then(unconfirmedTransactions => { + // unconfirmedTransactions = JSON.parse(unconfirmedTransactions) + // console.log(unconfirmedTransactions) + unconfirmedTransactions.filter(tx => { + tx.creatorAddress === addr || tx.recipient === addr + }) + // console.log(unconfirmedTransactions, unconfirmedTransactions.length) + // if(unconfirmedTransactions.length === 0) { + // return + // } + this._unconfirmedTransactionStreams[addr].emit(unconfirmedTransactions) + // console.log(this._unconfirmedTransactionStreams[addr]) + }) + })) + } +} diff --git a/qortal-ui-plugins/plugins/core/streams/onNewBlock.js b/qortal-ui-plugins/plugins/core/streams/onNewBlock.js new file mode 100644 index 00000000..f02cc38b --- /dev/null +++ b/qortal-ui-plugins/plugins/core/streams/onNewBlock.js @@ -0,0 +1,336 @@ +import { parentEpml } from '../connect.js' + +let socketObject +let activeBlockSocketTimeout +let initial = 0 +let closeGracefully = false +let isCalled = false +let retryOnClose = false +let blockFirstCall = true + +let nodeStatusSocketObject +let nodeStatusSocketTimeout +let nodeStatusSocketcloseGracefully = false +let nodeStatusCount = 0 +let nodeStatusRetryOnClose = false +let nodeStateCall = false + +let isLoggedIn = false + +let oldAccountInfo + +parentEpml.subscribe('logged_in', loggedIn => { + + if (loggedIn === 'true') { + isLoggedIn = true + } else { + isLoggedIn = false + } +}) + +const setAccountInfo = async (addr) => { + + const names = await parentEpml.request('apiCall', { + url: `/names/address/${addr}` + }) + const addressInfo = await parentEpml.request('apiCall', { + url: `/addresses/${addr}` + }) + + let accountInfo = { + names: names, + addressInfo: addressInfo + } + + if (window.parent._.isEqual(oldAccountInfo, accountInfo) === true) { + + return + } else { + + parentEpml.request('setAccountInfo', accountInfo) + oldAccountInfo = accountInfo + } +} + +const doNodeInfo = async () => { + + const nodeInfo = await parentEpml.request('apiCall', { + url: '/admin/info' + }) + + parentEpml.request('updateNodeInfo', nodeInfo) +} + +let initStateCount = 0 +let oldState + +const closeSockets = () => { + + socketObject.close(); + closeGracefully = true + + nodeStatusSocketObject.close(); + nodeStatusSocketcloseGracefully = true +} + +export const startConfigWatcher = () => { + + parentEpml.ready().then(() => { + parentEpml.subscribe('node_config', c => { + + if (initStateCount === 0) { + let _oldState = JSON.parse(c) + oldState = { node: _oldState.node, knownNodes: _oldState.knownNodes } + initStateCount = initStateCount + 1 + + nodeStateCall = true + isCalled = true + socketObject !== undefined ? closeSockets() : undefined; + nodeStatusSocketObject !== undefined ? closeSockets() : undefined; + initNodeStatusCall(oldState) + pingactiveBlockSocket() + + // Call doNodeInfo + doNodeInfo() + } + + let _newState = JSON.parse(c); + let newState = { node: _newState.node, knownNodes: _newState.knownNodes } + + if (window.parent._.isEqual(oldState, newState) === true) { + return + } else { + oldState = newState + nodeStateCall = true + isCalled = true + socketObject !== undefined ? closeSockets() : undefined; + nodeStatusSocketObject !== undefined ? closeSockets() : undefined; + initNodeStatusCall(newState) + pingactiveBlockSocket() + + // Call doNodeInfo + doNodeInfo() + } + }) + }) + + parentEpml.imReady() +} + +const processBlock = (blockObject) => { + + parentEpml.request('updateBlockInfo', blockObject) +} + +const doNodeStatus = async (nodeStatusObject) => { + + parentEpml.request('updateNodeStatus', nodeStatusObject) +} + + +const initNodeStatusCall = (nodeConfig) => { + + if (nodeConfig.node == 0) { + pingNodeStatusSocket() + } else if (nodeConfig.node == 1) { + pingNodeStatusSocket() + } else if (nodeStatusSocketObject !== undefined) { + nodeStatusSocketObject.close() + nodeStatusSocketcloseGracefully = true + } else { + // ... + } +} + +const initBlockSocket = () => { + + let myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + let nodeUrl = myNode.domain + ":" + myNode.port + + let activeBlockSocketLink + + if (window.parent.location.protocol === "https:") { + + activeBlockSocketLink = `wss://${nodeUrl}/websockets/blocks`; + } else { + + activeBlockSocketLink = `ws://${nodeUrl}/websockets/blocks`; + } + + const activeBlockSocket = new WebSocket(activeBlockSocketLink); + + // Open Connection + activeBlockSocket.onopen = (e) => { + + console.log(`[SOCKET-BLOCKS]: Connected.`); + closeGracefully = false + socketObject = activeBlockSocket + + initial = initial + 1 + } + + // Message Event + activeBlockSocket.onmessage = (e) => { + + processBlock(JSON.parse(e.data)); + + if (isLoggedIn) { + + // Call Set Account Info... + setAccountInfo(window.parent.reduxStore.getState().app.selectedAddress.address) + } + } + + // Closed Event + activeBlockSocket.onclose = () => { + + console.log(`[SOCKET-BLOCKS]: CLOSED`); + + processBlock({}); + blockFirstCall = true + clearInterval(activeBlockSocketTimeout) + + if (closeGracefully === false && initial <= 52) { + + if (initial <= 52) { + + retryOnClose = true + setTimeout(pingactiveBlockSocket, 10000) + initial = initial + 1 + } else { + + // ... Stop retrying... + retryOnClose = false + } + } + } + + // Error Event + activeBlockSocket.onerror = (e) => { + + console.log(`[SOCKET-BLOCKS]: ${e.type}`); + blockFirstCall = true + processBlock({}); + } + + if (blockFirstCall) { + + parentEpml.request('apiCall', { + url: '/blocks/last' + }).then(res => { + + processBlock(res) + blockFirstCall = false + }) + } +} + + +const pingactiveBlockSocket = () => { + + if (isCalled) { + + isCalled = false + + initBlockSocket() + activeBlockSocketTimeout = setTimeout(pingactiveBlockSocket, 295000) + } else if (retryOnClose) { + + retryOnClose = false + clearTimeout(activeBlockSocketTimeout) + initBlockSocket() + isCalled = true + activeBlockSocketTimeout = setTimeout(pingactiveBlockSocket, 295000) + } else { + + socketObject.send("non-integer ping") + activeBlockSocketTimeout = setTimeout(pingactiveBlockSocket, 295000) + } +} + + +const initNodeStatusSocket = () => { + + let myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + let nodeUrl = myNode.domain + ":" + myNode.port + + let activeNodeStatusSocketLink + + if (window.parent.location.protocol === "https:") { + + activeNodeStatusSocketLink = `wss://${nodeUrl}/websockets/admin/status`; + } else { + + activeNodeStatusSocketLink = `ws://${nodeUrl}/websockets/admin/status`; + } + + const activeNodeStatusSocket = new WebSocket(activeNodeStatusSocketLink); + + // Open Connection + activeNodeStatusSocket.onopen = (e) => { + + console.log(`[SOCKET-NODE-STATUS]: Connected.`); + nodeStatusSocketcloseGracefully = false + nodeStatusSocketObject = activeNodeStatusSocket + + nodeStatusCount = nodeStatusCount + 1 + } + + // Message Event + activeNodeStatusSocket.onmessage = (e) => { + + doNodeStatus(JSON.parse(e.data)) + } + + // Closed Event + activeNodeStatusSocket.onclose = () => { + + console.log(`[SOCKET-NODE-STATUS]: CLOSED`); + + doNodeStatus({}); + clearInterval(nodeStatusSocketTimeout) + + if (nodeStatusSocketcloseGracefully === false && nodeStatusCount <= 52) { + + if (nodeStatusCount <= 52) { + + nodeStatusRetryOnClose = true + setTimeout(pingNodeStatusSocket, 10000) + nodeStatusCount = nodeStatusCount + 1 + } else { + + // ... Stop retrying... + nodeStatusRetryOnClose = false + } + } + } + + // Error Event + activeNodeStatusSocket.onerror = (e) => { + + console.log(`[SOCKET-NODE-STATUS]: ${e.type}`); + doNodeStatus({}); + } +} + + +const pingNodeStatusSocket = () => { + + if (nodeStateCall) { + + clearTimeout(nodeStatusSocketTimeout) + initNodeStatusSocket() + nodeStateCall = false + nodeStatusSocketTimeout = setTimeout(pingNodeStatusSocket, 295000) + } else if (nodeStatusRetryOnClose) { + + nodeStatusRetryOnClose = false + clearTimeout(nodeStatusSocketTimeout) + initNodeStatusSocket() + nodeStatusSocketTimeout = setTimeout(pingNodeStatusSocket, 295000) + } else { + + nodeStatusSocketObject.send("non-integer ping") + nodeStatusSocketTimeout = setTimeout(pingNodeStatusSocket, 295000) + } +} diff --git a/qortal-ui-plugins/plugins/core/streams/streams.js b/qortal-ui-plugins/plugins/core/streams/streams.js new file mode 100644 index 00000000..23fa2b0c --- /dev/null +++ b/qortal-ui-plugins/plugins/core/streams/streams.js @@ -0,0 +1,241 @@ +import { parentEpml } from '../connect.js' + +import { startConfigWatcher } from './onNewBlock.js' + +const setAccountInfo = async (addr) => { + + const names = await parentEpml.request('apiCall', { + url: `/names/address/${addr}` + }) + const addressInfo = await parentEpml.request('apiCall', { + url: `/addresses/${addr}` + }) + + let accountInfo = { + names: names, + addressInfo: addressInfo + } + + parentEpml.request('setAccountInfo', accountInfo) +} + + +const objectToArray = (object) => { + + let groupList = object.groups.map(group => group.groupId === 0 ? { groupId: group.groupId, url: `group/${group.groupId}`, groupName: "Qortal General Chat", sender: group.sender, senderName: group.senderName, timestamp: group.timestamp === undefined ? 1 : group.timestamp } : { ...group, url: `group/${group.groupId}` }) + let directList = object.direct.map(dc => { + return { ...dc, url: `direct/${dc.address}` } + }) + let chatHeadMasterList = [...groupList, ...directList] + + return chatHeadMasterList +} + +const sortActiveChat = (activeChatObject, localChatHeads) => { + + let oldChatHeads = JSON.parse(localChatHeads) + + if (window.parent._.isEqual(oldChatHeads, activeChatObject) === true) { + return + } else { + + let oldActiveChats = objectToArray(oldChatHeads) + let newActiveChats = objectToArray(activeChatObject) + + let results = newActiveChats.filter(newChat => { + let value = oldActiveChats.some(oldChat => newChat.timestamp === oldChat.timestamp) + return !value + }); + + results.forEach(chat => { + + if (chat.sender !== window.parent.reduxStore.getState().app.selectedAddress.address) { + + if (chat.sender !== undefined) parentEpml.request('showNotification', chat) + } else { + // ... + } + }) + + } + +} + + +let initialChatWatch = 0 + +const chatHeadWatcher = (activeChats) => { + + let addr = window.parent.reduxStore.getState().app.selectedAddress.address + + let key = `${addr.substr(0, 10)}_chat-heads` + + try { + let localChatHeads = localStorage.getItem(key) + + if (localChatHeads === null) { + parentEpml.request('setLocalStorage', { + key: key, + dataObj: activeChats + }).then(ms => { + parentEpml.request('setChatHeads', activeChats).then(ret => { + // ... + }) + }) + } else { + + parentEpml.request('setLocalStorage', { + key: key, + dataObj: activeChats + }).then(ms => { + parentEpml.request('setChatHeads', activeChats).then(ret => { + // ... + }) + }) + + if (initialChatWatch >= 1) { + + sortActiveChat(activeChats, localChatHeads) + } else { + + initialChatWatch = initialChatWatch + 1 + } + } + + } catch (e) { + console.error(e) + + } +} + +let socketObject +let activeChatSocketTimeout +let initial = 0 +let closeGracefully = false +let onceLoggedIn = false +let retryOnClose = false +let canPing = false + +parentEpml.subscribe('logged_in', async isLoggedIn => { + + const initChatHeadSocket = () => { + + let myNode = window.parent.reduxStore.getState().app.nodeConfig.knownNodes[window.parent.reduxStore.getState().app.nodeConfig.node] + let nodeUrl = myNode.domain + ":" + myNode.port + + let activeChatSocketLink + + if (window.parent.location.protocol === "https:") { + + activeChatSocketLink = `wss://${nodeUrl}/websockets/chat/active/${window.parent.reduxStore.getState().app.selectedAddress.address}`; + } else { + + activeChatSocketLink = `ws://${nodeUrl}/websockets/chat/active/${window.parent.reduxStore.getState().app.selectedAddress.address}`; + } + + const activeChatSocket = new WebSocket(activeChatSocketLink); + + // Open Connection + activeChatSocket.onopen = () => { + + console.log(`[SOCKET]: Connected.`); + socketObject = activeChatSocket + + initial = initial + 1 + canPing = true + } + + // Message Event + activeChatSocket.onmessage = (e) => { + + chatHeadWatcher(JSON.parse(e.data)) + } + + // Closed Event + activeChatSocket.onclose = () => { + + console.log(`[SOCKET]: CLOSED`); + clearInterval(activeChatSocketTimeout) + + if (closeGracefully === false && initial <= 52) { + + if (initial <= 52) { + + parentEpml.request('showSnackBar', "Connection to the Qortal Core was lost, is your Core running ?") + retryOnClose = true + setTimeout(pingActiveChatSocket, 10000) + initial = initial + 1 + } else { + + parentEpml.request('showSnackBar', "Cannot connect to the Qortal Core, restart UI and Core!") + } + } + } + + // Error Event + activeChatSocket.onerror = (e) => { + + console.log(`[SOCKET]: ${e.type}`); + } + } + + + const pingActiveChatSocket = () => { + + if (window.parent.reduxStore.getState().app.loggedIn === true) { + + if (!onceLoggedIn) { + + initChatHeadSocket() + onceLoggedIn = true + activeChatSocketTimeout = setTimeout(pingActiveChatSocket, 295000) + } else if (retryOnClose) { + + retryOnClose = false + clearTimeout(activeChatSocketTimeout) + initChatHeadSocket() + onceLoggedIn = true + activeChatSocketTimeout = setTimeout(pingActiveChatSocket, 295000) + } else if (canPing) { + + socketObject.send('ping') + activeChatSocketTimeout = setTimeout(pingActiveChatSocket, 295000) + } + + } else { + + if (onceLoggedIn && !closeGracefully) { + + closeGracefully = true + socketObject.close() + clearTimeout(activeChatSocketTimeout) + onceLoggedIn = false + canPing = false + } + } + } + + + if (isLoggedIn === 'true') { + + // Call Set Account Info... + setAccountInfo(window.parent.reduxStore.getState().app.selectedAddress.address) + + // Start Chat Watcher Socket + pingActiveChatSocket() + } else { + + if (onceLoggedIn) { + + closeGracefully = true + socketObject.close() + clearTimeout(activeChatSocketTimeout) + onceLoggedIn = false + canPing = false + } + + initialChatWatch = 0 + } +}) + +startConfigWatcher()