Skip to content

Commit

Permalink
feat(ui): implement basic backuping via gdrive
Browse files Browse the repository at this point in the history
resolve LN-Zap#1971
  • Loading branch information
korhaliv committed May 17, 2019
1 parent df5e9ef commit 24d1dd7
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 1 deletion.
4 changes: 4 additions & 0 deletions electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import themes from '@zap/renderer/themes'
import getDbName from '@zap/utils/db'
import ZapMenuBuilder from './menuBuilder'
import ZapController from './controller'
import createBackupService from './walletBackup/service'
import ZapUpdater from './updater'
import ZapMigrator from './migrator'

Expand Down Expand Up @@ -269,6 +270,9 @@ app.on('ready', async () => {
zap = new ZapController(mainWindow)
zap.init()

// initialize backup system
createBackupService(mainWindow)

// Initialise the application menus.
menuBuilder = new ZapMenuBuilder(mainWindow)
menuBuilder.buildMenu(locale)
Expand Down
1 change: 1 addition & 0 deletions electron/walletBackup/gdrive/gdrive.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ async function createClient({ clientId, authRedirectUrl, scope, tokens }) {
listFiles: apiCall.bind(this, api.listFiles),
on: emitter.on.bind(emitter),
off: emitter.off.bind(emitter),
removeAllListeners: emitter.removeAllListeners.bind(emitter),
}
}

Expand Down
3 changes: 2 additions & 1 deletion electron/walletBackup/gdrive/gdriveApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,11 @@ export async function listFiles(drive, params = {}) {
}

export function createAuthWindow(oAuthClient, scope, windowParams = { width: 500, height: 600 }) {
// TODO. disable node int. add preload
const authWindow = new BrowserWindow({
...windowParams,
show: true,
nodeIntegration: false,
contextIsolation: true,
})

const authUrl = createAuthUrl({
Expand Down
95 changes: 95 additions & 0 deletions electron/walletBackup/gdrive/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import EventEmitter from 'events'
import config from 'config'
import { mainLog } from '@zap/utils/log'
import createClient from './gdrive'

export function forwardEvent(service, event, target) {
service.on(event, data => target.emit(event, data))
}

class BackupService extends EventEmitter {
drive = null

constructor() {
super()
}

async logout() {
const { drive } = this
drive && drive.removeAllListeners('tokensReceived')
this.drive = null
}

async login(tokens) {
const { redirectUrl, clientId, scope } = config.backup.gdrive
const { drive } = this
if (!drive) {
this.drive = await createClient({
clientId,
authRedirectUrl: redirectUrl,
scope,
tokens,
})
mainLog.info('forwardEvent')
forwardEvent(this.drive, 'tokensReceived', this)
}

return true
}

async isLoggedIn() {
const { drive } = this
return await drive.testConnection()
}

getTokens() {
const { drive } = this
return drive.getTokens()
}
getBackupId() {}
async loadBackup(walletId) {
const { drive, getBackupId } = this
const fileId = getBackupId(walletId)
if (fileId) {
const backup = await drive.downloadToBuffer(fileId)
return backup
}
return null
}

async saveBackup(walletId, fileId, backup) {
const backupExists = async () => {
try {
await drive.getFileInfo(fileId)
return true
} catch (e) {
return false
}
}
const { drive } = this
// if fileId is provded and backup exists - update it
if (fileId && (await backupExists())) {
await drive.updateFromBuffer(fileId, backup)
return fileId
} else {
// create new file
const { id } = await drive.uploadFromBuffer(walletId, backup)
return id
}
}

get name() {
return 'gdrive'
}
}
// singleton backup service

let backupService

export default function getBackupService() {
if (!backupService) {
backupService = new BackupService()
}

return backupService
}
60 changes: 60 additions & 0 deletions electron/walletBackup/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ipcMain } from 'electron'
import { mainLog } from '@zap/utils/log'
import getBackupService from './serviceFactory'

export default function createBackupService(mainWindow) {
const send = (msg, params) => mainWindow.webContents.send(msg, params)

ipcMain.on('initBackupService', async (event, { walletId, tokens, provider }) => {
mainLog.info('Initializing backup service powered by: %o', provider)

const backupService = getBackupService(provider)
if (backupService) {
// cleanup existing instance if any
backupService.logout()
const handleTokensReceived = tokens => {
// ensure the we are always storing the latest tokens available
send('backupTokensUpdated', {
tokens,
provider: backupService.name,
walletId,
})
mainLog.info('Tokens received %o: ', tokens)
}
// re-subscribe for token updates
backupService.removeAllListeners('tokensReceived')
backupService.on('tokensReceived', handleTokensReceived)

await backupService.login(tokens)
send('backupServiceInitialized', { walletId, provider })
}
})

ipcMain.on(
'saveBackup',
async (event, { backup, walletId, provider, backupMetadata, nodePub }) => {
try {
const backupService = getBackupService(provider)
if (backupService) {
const backupId = await backupService.saveBackup(
nodePub,
backupMetadata && backupMetadata.backupId,
backup
)
mainLog.info('Backup updated. GDrive fileID: %o', backupId)
mainWindow.webContents.send('backupSaveSuccess', {
backupId,
provider: backupService.name,
walletId,
})
mainLog.info(`saveBackup ${walletId} ${nodePub}`)
}
} catch (e) {
mainLog.warn('Unable to backup wallet %o: ', e)
mainWindow.webContents.send('backupSaveError')
}
}
)
}

export getBackupService from './gdrive'
18 changes: 18 additions & 0 deletions electron/walletBackup/serviceFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import getGDrive from './gdrive'

export const GOOGLE_DRIVE = 'gdrive'
export const DROPBOX = 'dropbox'
export const LOCAL = 'local'

export default function getBackupService(provider) {
switch (provider) {
case GOOGLE_DRIVE:
return getGDrive()
case DROPBOX:
throw new Error('not implemented')
case LOCAL:
throw new Error('not implemented')
default:
throw new Error('not implemented')
}
}
112 changes: 112 additions & 0 deletions renderer/reducers/backup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { send } from 'redux-electron-ipc'
import { grpcService } from 'workers'
import { walletSelectors } from './wallet'
import { infoSelectors } from './info'

const getDbRec = async walletId => await window.db.backup.get(walletId)

const setDbRec = async (walletId, update) => {
const updated = await window.db.backup.update(walletId, update)
if (updated === 0) {
try {
await window.db.backup.add({ id: walletId, ...update })
} catch (e) {
// Do nothing if there was an error - this indicates that the item already exists and was unchanged.
}
}
}

export async function backupTokensUpdated(event, { provider, tokens, walletId }) {
const backupDesc = await getDbRec(walletId)
await setDbRec(walletId, {
[provider]: { ...backupDesc[provider], tokens },
})
}

export async function hasBackupSetup(walletId) {
const backupDesc = await getDbRec(walletId)
if (backupDesc) {
const { activeProvider } = backupDesc
return activeProvider && backupDesc[activeProvider]
}

return false
}

/**
*
*
* @export
* @param {string} walletId wallet identifier. if not specified uses current active wallet
* @param {string} provider backup provider. if not specified attempts to use current active provider
* @returns
*/
export function initBackupService(walletId, provider) {
return async (dispatch, getState) => {
const wId = walletId || walletSelectors.activeWallet(getState())

const getServiceParams = async () => {
const backupDesc = await getDbRec(wId)
// attempt to initialize backup service with stored tokens
if (backupDesc) {
const { activeProvider } = backupDesc
const { tokens } = backupDesc[activeProvider] || {}
return { walletId: wId, tokens, provider: activeProvider }
}

return { walletId: wId, provider }
}

return dispatch(send('initBackupService', await getServiceParams()))
}
}

export const backupCurrentWallet = backup => async (dispatch, getState) => {
const getFreshBackup = async () => {
const grpc = await grpcService
if (grpc.services.Lightning.exportAllChannelBackups) {
return await grpc.services.Lightning.exportAllChannelBackups({})
}
return null
}

const getBackupBuff = backupData =>
backupData && backupData.multi_chan_backup && backupData.multi_chan_backup.multi_chan_backup

try {
const state = getState()
const walletId = walletSelectors.activeWallet(state)
const nodePub = infoSelectors.nodePub(state)
if (walletId) {
const backupData = backup || (await getFreshBackup())
const { activeProvider, ...rest } = (await getDbRec(walletId)) || {}
const backupMetadata = activeProvider && rest[activeProvider]
const canBackup = backupData && activeProvider
canBackup &&
dispatch(
send('saveBackup', {
backup: getBackupBuff(backupData),
walletId,
backupMetadata,
nodePub,
provider: activeProvider,
})
)
}
} catch (e) {
// Do nothing
}
}

export const backupSaveSuccess = async (event, { provider, backupId, walletId }) => {
const backupDesc = await getDbRec(walletId)
await setDbRec(walletId, {
[provider]: { ...backupDesc[provider], backupId },
})
}
export const backupServiceInitialized = (event, { walletId, provider }) => async dispatch => {
await setDbRec(walletId, {
activeProvider: provider,
})
dispatch(backupCurrentWallet())
}
4 changes: 4 additions & 0 deletions renderer/reducers/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { killNeutrino } from './neutrino'
import { receiveLocale } from './locale'
import { bitcoinPaymentUri, lightningPaymentUri } from './pay'
import { lndconnectUri } from './onboarding'
import { backupTokensUpdated, backupSaveSuccess, backupServiceInitialized } from './backup'

const ipc = createIpc({
initApp,
Expand All @@ -13,6 +14,9 @@ const ipc = createIpc({
bitcoinPaymentUri,
lightningPaymentUri,
lndconnectUri,
backupSaveSuccess,
backupTokensUpdated,
backupServiceInitialized,
})

export default ipc
4 changes: 4 additions & 0 deletions renderer/store/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export const getDb = name => {
autopay: 'id',
})

db.version(4).stores({
backup: 'id',
})

/**
* @class Wallet
* Wallet helper class.
Expand Down

0 comments on commit 24d1dd7

Please sign in to comment.