diff --git a/app/screens/WalletOverview/index.js b/app/screens/WalletOverview/index.js index 5f16c649..5f2b71c5 100644 --- a/app/screens/WalletOverview/index.js +++ b/app/screens/WalletOverview/index.js @@ -18,7 +18,6 @@ import { Schema } from 'shock-common' // @ts-expect-error import bech32 from 'bech32' -import SocketManager from '../../services/socket' import * as Navigation from '../../services/navigation' import Nav from '../../components/Nav' import wavesBG from '../../assets/images/waves-bg.png' @@ -26,15 +25,12 @@ import wavesBGDark from '../../assets/images/waves-bg-dark.png' import ShockIcon from '../../res/icons' import btcConvert from '../../services/convertBitcoin' import * as CSS from '../../res/css' -import * as Wallet from '../../services/wallet' import { getUSDRate, getWalletBalance } from '../../store/actions/WalletActions' import { fetchNodeInfo } from '../../store/actions/NodeActions' import { fetchRecentTransactions, fetchRecentPayments, fetchRecentInvoices, - loadNewInvoice, - loadNewTransaction, } from '../../store/actions/HistoryActions' import { subscribeOnChats } from '../../store/actions/ChatActions' import { @@ -72,8 +68,6 @@ import { Color } from 'shock-common/dist/constants' * @prop {() => Promise} fetchNodeInfo * @prop {() => Promise} subscribeOnChats * @prop {() => Promise} getUSDRate - * @prop {(invoice: Wallet.Invoice) => void} loadNewInvoice - * @prop {(transaction: Wallet.Transaction) => void} loadNewTransaction * @prop {{notifyDisconnect:boolean, notifyDisconnectAfterSeconds:number}} settings * @prop {() => void} forceInvoicesRefresh * @prop {boolean} isOnline @@ -124,21 +118,12 @@ class WalletOverview extends React.PureComponent { theme = 'dark' - /** - * @param {Schema.InvoiceWhenListed} invoice - */ - loadNewInvoice = invoice => { - // @ts-expect-error - this.props.loadNewInvoice(invoice) - } - componentDidMount = async () => { const { fetchNodeInfo, subscribeOnChats, fetchRecentTransactions, fetchRecentInvoices, - loadNewTransaction, navigation, forceInvoicesRefresh, forcePaymentsRefresh, @@ -173,10 +158,6 @@ class WalletOverview extends React.PureComponent { this.startNotificationService() - if (!SocketManager.socket?.connected) { - await SocketManager.connectSocket() - } - subscribeOnChats() await Promise.all([ fetchRecentInvoices(), @@ -184,28 +165,6 @@ class WalletOverview extends React.PureComponent { fetchRecentPayments(), fetchNodeInfo(), ]) - - SocketManager.socket.on( - 'invoice:new', - /** - * @param {Schema.InvoiceWhenListed} data - */ - data => { - Logger.log('[SOCKET] New Invoice!', data) - this.loadNewInvoice(data) - }, - ) - - SocketManager.socket.on( - 'transaction:new', - /** - * @param {Wallet.Transaction} data - */ - data => { - Logger.log('[SOCKET] New Transaction!', data) - loadNewTransaction(data) - }, - ) } fetchRecentPayments = () => @@ -471,8 +430,6 @@ const mapDispatchToProps = { fetchRecentInvoices, fetchRecentPayments, subscribeOnChats, - loadNewInvoice, - loadNewTransaction, forceInvoicesRefresh: invoicesRefreshForced, forcePaymentsRefresh: paymentsRefreshForced, getMoreFeed, diff --git a/app/services/index.ts b/app/services/index.ts index 91b11d08..f16f5895 100644 --- a/app/services/index.ts +++ b/app/services/index.ts @@ -8,7 +8,6 @@ export * from './encryption' export * from './errorReporter' export * from './navigation' export * from './seedServer' -export * from './socket' export * from './utils' export * from './validators' export * from './http' diff --git a/app/services/socket.js b/app/services/socket.js deleted file mode 100644 index 60377ea6..00000000 --- a/app/services/socket.js +++ /dev/null @@ -1,204 +0,0 @@ -import SocketIO from 'socket.io-client' -import isEmpty from 'lodash/isEmpty' -import Logger from 'react-native-file-log' - -import { DISABLE_ENCRYPTION } from '../config' -import * as Actions from '../store/actions' - -import * as Cache from './cache' -import * as Encryption from './encryption' - -/** - * @typedef {import('../store').Store} ReduxStore - */ - -class Socket { - /** @type {any} */ - socketInstance = null - - /** @type {ReduxStore?} */ - store = null - - /** - * Set Redux Store for use along with end-to-end encryption - * @param {ReduxStore} store - * @returns {ReduxStore} Returns the initialized Redux store - */ - setStore = store => { - this.store = store - return store - } - - /** - * @private - * @param {SocketIOClient.Socket} socket - */ - encryptSocketInstance = socket => ({ - connect: () => socket.connect(), - get connected() { - return socket.connected - }, - // @ts-expect-error - off: () => socket.off(), - disconnect: () => socket.disconnect(), - get disconnected() { - return socket.disconnected - }, - // @ts-expect-error - binary: b => this.encryptSocketInstance(socket.binary(b)), - /** - * @param {string} eventName - * @param {(handler: any) => void} cb - */ - on: (eventName, cb) => { - socket.on( - eventName, - /** - * @param {any} data - */ - async data => { - // Logger.log('Listening to Event:', eventName) - - if (Encryption.isNonEncrypted(eventName)) { - cb(data) - return - } - - const decryptedData = await this.decryptSocketData(data).catch( - err => { - Logger.log( - `Error decrypting data for event: ${eventName} - msg: ${err.message}`, - ) - }, - ) - - cb(decryptedData) - }, - ) - }, - /** - * @param {string} eventName - * @param {any} data - */ - emit: async (eventName, data) => { - if (Encryption.isNonEncrypted(eventName)) { - socket.emit(eventName, data) - return - } - - // Logger.log('Encrypting socket...', eventName, data) - const encryptedData = await this.encryptSocketData(data) - // Logger.log('Encrypted Socket Data:', encryptedData) - socket.emit(eventName, encryptedData) - return this.encryptSocketInstance(socket) - }, - }) - - /** - * @param {object} data - */ - encryptSocketData = async data => { - if (DISABLE_ENCRYPTION) { - return data - } - - if (this.store) { - const { APIPublicKey } = this.store.getState().connection - - Logger.log('APIPublicKey', APIPublicKey) - - if (!APIPublicKey && !isEmpty(data)) { - throw new Error( - 'Please exchange keys with the API before sending any data through WebSockets', - ) - } - - if (APIPublicKey && !isEmpty(data)) { - Logger.log('encryptSocketData APIPublicKey:', APIPublicKey, data) - const stringifiedData = JSON.stringify(data) - const encryptedData = await Encryption.encryptData( - stringifiedData, - APIPublicKey, - ) - Logger.log('Original Data:', data) - Logger.log('Encrypted Data:', encryptedData) - return encryptedData - } - } - - return null - } - - /** - * @param {any} data - */ - decryptSocketData = async data => { - if (data && data.encryptedKey && this.store) { - // const decryptionTime = Date.now() - Logger.log('[LND SOCKET] Decrypting Daobjectta...' /*, data*/) - const { sessionId } = this.store.getState().connection - const decryptedKey = await Encryption.decryptKey( - data.encryptedKey, - sessionId, - ) - const { decryptedData } = await Encryption.decryptData({ - encryptedData: data.encryptedData, - key: decryptedKey, - iv: data.iv, - }) - // Logger.log( - // `[LND SOCKET] Decryption took: ${Date.now() - decryptionTime}ms`, - // ) - return JSON.parse(decryptedData) - } - - // Logger.log('[LND SOCKET] Data is non-encrypted', data) - - return data - } - - connectSocket = async () => { - const { store } = this - if (store) { - const { connection } = store.getState() - const nodeURL = await Cache.getNodeURL() - - const socket = SocketIO(`http://${nodeURL}/default`, { - query: { - 'x-shockwallet-device-id': connection.deviceId, - IS_LND_SOCKET: true, - }, - }) - - socket.on('connect', () => { - store.dispatch(Actions.socketDidConnect()) - }) - - socket.on('disconnect', () => { - store.dispatch(Actions.socketDidDisconnect()) - }) - - socket.on('shockping', () => { - store.dispatch(Actions.ping(Date.now())) - }) - - this.socketInstance = this.encryptSocketInstance(socket) - - Logger.log('[LND SOCKET] New socket instance created successfully') - - return this.socketInstance - } - - Logger.log('[LND SOCKET] Error: Store is not initialized yet.') - - return null - } - - get socket() { - return this.socketInstance - } -} - -const socket = new Socket() - -export default socket diff --git a/app/store/actions/ConnectionActions/v1.js b/app/store/actions/ConnectionActions/v1.js index ad1773ba..c6a5b8da 100644 --- a/app/store/actions/ConnectionActions/v1.js +++ b/app/store/actions/ConnectionActions/v1.js @@ -2,17 +2,10 @@ import Http from 'axios' import { RSAKeychain } from 'react-native-rsa-native' import Logger from 'react-native-file-log' -import { - KEYS_LOADED, - keysLoaded, - SOCKET_DID_CONNECT, - SOCKET_DID_DISCONNECT, -} from './v2' +import { KEYS_LOADED, keysLoaded } from './v2' export const ACTIONS = { LOAD_NEW_KEYS: KEYS_LOADED, - SOCKET_DID_CONNECT, - SOCKET_DID_DISCONNECT, } /** @type {Promise?} */ diff --git a/app/store/actions/ConnectionActions/v2.ts b/app/store/actions/ConnectionActions/v2.ts index 7afd4715..0d6df473 100644 --- a/app/store/actions/ConnectionActions/v2.ts +++ b/app/store/actions/ConnectionActions/v2.ts @@ -21,22 +21,6 @@ export const keysLoaded = (keys: LoadedKeys) => data: keys, } as const) -export const SOCKET_DID_CONNECT = 'socket/socketDidConnect' - -export const socketDidConnect = () => - ({ - type: SOCKET_DID_CONNECT, - } as const) - -export const SOCKET_DID_DISCONNECT = 'socket/socketDidDisconnect' - -export const socketDidDisconnect = () => - ({ - type: SOCKET_DID_DISCONNECT, - } as const) - export type ConnectionAction = | ReturnType | ReturnType - | ReturnType - | ReturnType diff --git a/app/store/reducers/ConnectionReducer.js b/app/store/reducers/ConnectionReducer.js index 8badd903..27f91347 100644 --- a/app/store/reducers/ConnectionReducer.js +++ b/app/store/reducers/ConnectionReducer.js @@ -1,6 +1,5 @@ import uuid from 'uuid/v4' import { Action } from '../actions' -import { ACTIONS } from '../actions/ConnectionActions' /** * @typedef {object} State @@ -8,7 +7,6 @@ import { ACTIONS } from '../actions/ConnectionActions' * @prop {string?} APIPublicKey * @prop {string?} sessionId * @prop {string} deviceId - * @prop {boolean} socketConnected * @prop {number} lastPing */ @@ -20,8 +18,8 @@ const INITIAL_STATE = { APIPublicKey: null, sessionId: null, deviceId: uuid(), - socketConnected: false, - lastPing: 0, + // setting the initial value to Date.now() simplifies things elsewhere + lastPing: Date.now(), } /** @@ -40,18 +38,6 @@ const connection = (state = INITIAL_STATE, action) => { APIPublicKey, } } - case ACTIONS.SOCKET_DID_CONNECT: { - return { - ...state, - socketConnected: true, - } - } - case ACTIONS.SOCKET_DID_DISCONNECT: { - return { - ...state, - socketConnected: false, - } - } case 'socket/ping': { return { ...state, diff --git a/app/store/sagas/ping.ts b/app/store/sagas/ping.ts index 9d5270e4..1899fca4 100644 --- a/app/store/sagas/ping.ts +++ b/app/store/sagas/ping.ts @@ -1,4 +1,4 @@ -import { takeEvery, select } from 'redux-saga/effects' +import { takeEvery, select, put } from 'redux-saga/effects' import Logger from 'react-native-file-log' import SocketIO from 'socket.io-client' import { Constants } from 'shock-common' @@ -9,12 +9,40 @@ import { getStore } from '../store' let socket: ReturnType | null = null +/** + * Allow some leeway for the new socket to actually receive that first ping. + */ +let lastHandshake = Date.now() + function* ping() { try { - const state = Selectors.getStateRoot(yield select()) - const { token, host } = state.auth + const { + auth: { token, host }, + connection: { lastPing }, + } = Selectors.getStateRoot(yield select()) + + const socketIsDead = + socket && + Date.now() - lastPing > 12000 && + Date.now() - lastHandshake > 12000 + + if (socketIsDead) { + Logger.log('Socket is dead') + } + + if ((!token && socket) || socketIsDead) { + Logger.log(`Will kill ping socket`) + socket!.off('*') + socket!.close() + socket = null + + // force next tick + yield put({ type: Math.random().toString() }) + } if (token && !socket) { + lastHandshake = Date.now() + Logger.log(`Will try to connect ping socket`) socket = SocketIO(`http://${host}/shockping`, { query: { token, @@ -28,12 +56,10 @@ function* ping() { socket.on(Constants.ErrorCode.NOT_AUTH, () => { getStore().dispatch(Actions.tokenDidInvalidate()) }) - } - if (!token && socket) { - socket.off('*') - socket.close() - socket = null + socket.on('$error', (e: string) => { + Logger.log(`Error received by ping socket: ${e}`) + }) } } catch (err) { Logger.log('Error inside ping* ()') diff --git a/app/store/sagas/users.ts b/app/store/sagas/users.ts index c224fadb..a811e1f2 100644 --- a/app/store/sagas/users.ts +++ b/app/store/sagas/users.ts @@ -2,6 +2,7 @@ import { takeEvery, select } from 'redux-saga/effects' import Logger from 'react-native-file-log' import SocketIO from 'socket.io-client' import { Constants } from 'shock-common' +import size from 'lodash/size' import * as Actions from '../actions' import * as Selectors from '../selectors' @@ -17,10 +18,11 @@ function* users() { const allPublicKeys = Selectors.getAllPublicKeys(state) if (isReady) { - assignSocketsToPublicKeys(allPublicKeys) + assignSocketsToPublicKeysIfNeeded(allPublicKeys) } - if (!isReady) { + if (!isReady && size(sockets)) { + Logger.log(`Will remove user sockets from all subbed public keys`) for (const publicKey of allPublicKeys) { const normalSocket = sockets['normal' + publicKey] const binarySocket = sockets['binary' + publicKey] @@ -44,7 +46,7 @@ function* users() { } } -const assignSocketsToPublicKeys = (publicKeys: string[]) => { +const assignSocketsToPublicKeysIfNeeded = (publicKeys: string[]) => { for (const publicKey of publicKeys) { const normalSocketName = 'normal' + publicKey const binarySocketName = 'binary' + publicKey @@ -53,6 +55,8 @@ const assignSocketsToPublicKeys = (publicKeys: string[]) => { continue } + Logger.log(`Assigning socket to publicKey: ${publicKey}`) + if (!(!sockets[normalSocketName] && !sockets[binarySocketName])) { throw new Error( `Assertion: !sockets[normalSocketName] && !sockets[binarySocketName] failed`, diff --git a/app/store/selectors/connection.ts b/app/store/selectors/connection.ts index de90d263..7e4db87d 100644 --- a/app/store/selectors/connection.ts +++ b/app/store/selectors/connection.ts @@ -1,15 +1,4 @@ -import { createSelector } from 'reselect' - import { State } from '../reducers' -const isSocketConnectedSelector = (state: State) => - state.connection.socketConnected -const lastPingWasLessThan10SecondsAgoSelector = (state: State) => +export const isOnline = (state: State): boolean => Date.now() - state.connection.lastPing < 6000 - -export const isOnline = createSelector( - isSocketConnectedSelector, - lastPingWasLessThan10SecondsAgoSelector, - (isSocketConnected, lastPingWasLessThan10SecondsAgo) => - isSocketConnected && lastPingWasLessThan10SecondsAgo, -) diff --git a/app/store/store.ts b/app/store/store.ts index 544389cb..e99334b1 100644 --- a/app/store/store.ts +++ b/app/store/store.ts @@ -6,7 +6,6 @@ import createSagaMiddleware from 'redux-saga' import createSensitiveStorage from 'redux-persist-sensitive-storage' import thunk from 'redux-thunk' -import SocketManager from '../services/socket' import reducers, { State } from './reducers' import { Action as _Action } from './actions' @@ -42,8 +41,6 @@ export default () => { const persistor = persistStore(store) - SocketManager.setStore(store) - sagaMiddleware.run(rootSaga) return { persistor, store }