From 977d9f3983cbd6da289cba453ea01f95624bc662 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Wed, 23 May 2018 10:52:25 +0200 Subject: [PATCH] DevicesReducer substitute for TrezorConnectReducer.devices --- src/js/reducers/DevicesReducer.js | 310 ++++++++++++++++++++++++++++++ src/js/reducers/index.js | 4 +- 2 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 src/js/reducers/DevicesReducer.js diff --git a/src/js/reducers/DevicesReducer.js b/src/js/reducers/DevicesReducer.js new file mode 100644 index 00000000..76682f30 --- /dev/null +++ b/src/js/reducers/DevicesReducer.js @@ -0,0 +1,310 @@ +/* @flow */ +'use strict'; + +import { TRANSPORT, DEVICE } from 'trezor-connect'; +import * as CONNECT from '../actions/constants/TrezorConnect'; +import * as WALLET from '../actions/constants/wallet'; + +import type { Action, TrezorDevice } from '~/flowtype'; +import type { Device } from 'trezor-connect'; + +export type State = Array; + +const initialState: State = []; + +const mergeDevices = (current: TrezorDevice, upcoming: Device | TrezorDevice): TrezorDevice => { + + // do not merge if passphrase protection was changed + // if (upcoming.features && current.features) { + // if (upcoming.features.passphrase_protection !== current.features.passphrase_protection) { + // // device settings has been changed, reset state + // // dev.state = null; + // } + // } + + let instanceLabel = current.instanceLabel; + if (upcoming.label !== current.label) { + instanceLabel = upcoming.label + if (typeof current.instance === 'number') { + instanceLabel += ` (${current.instanceName || current.instance})`; + } + } + + const dev: TrezorDevice = { + // ...current, + ...upcoming, + // make sure that instance specific variables will not be overridden + connected: typeof upcoming.connected === 'boolean' ? upcoming.connected : current.connected, + available: typeof upcoming.available === 'boolean' ? upcoming.available : current.available, + remember: typeof upcoming.remember === 'boolean' ? upcoming.remember : current.remember, + instance: current.instance, + instanceLabel, + instanceName: typeof upcoming.instanceName === 'string' ? upcoming.instanceName : current.instanceName, + state: current.state, + ts: typeof upcoming.ts === 'number' ? upcoming.ts : current.ts, + } + // corner-case: trying to merge unacquired device with acquired + // make sure that sensitive fields will not be changed and device will remain acquired + if (upcoming.unacquired && current.state) { + dev.unacquired = false; + dev.features = current.features; + dev.label = current.label; + } + + return dev; +} + +const addDevice = (state: State, device: Device): State => { + + let affectedDevices: Array = []; + let otherDevices: Array = []; + if (!device.features) { + // check if connected device is unacquired, and it's already exists + affectedDevices = state.filter(d => d.path === device.path); + // if so, ignore this action + if (affectedDevices.length > 0) { + return state; + } + otherDevices = state.filter(d => affectedDevices.indexOf(d) === -1); + } else { + affectedDevices = state.filter(d => d.features && d.features.device_id === device.features.device_id); + const unacquiredDevices = state.filter(d => d.path.length > 0 && d.path === device.path); + otherDevices = state.filter(d => affectedDevices.indexOf(d) < 0 && unacquiredDevices.indexOf(d) < 0); + } + + const newDevice: TrezorDevice = { + ...device, + acquiring: false, + remember: false, + connected: true, + available: true, + path: device.path, + label: device.label, + state: null, + instanceLabel: device.label, + instanceName: null, + ts: new Date().getTime(), + } + + if (affectedDevices.length > 0 ) { + // check if freshly added device has different "passphrase_protection" settings + + let hasDifferentPassphraseSettings: boolean = false; + let hasInstancesWithPassphraseSettings: boolean = false; + const changedDevices: Array = affectedDevices.map(d => { + if (d.features && d.features.passphrase_protection === device.features.passphrase_protection) { + hasInstancesWithPassphraseSettings = true; + return mergeDevices(d, { ...device, connected: true, available: true } ); + } else { + hasDifferentPassphraseSettings = true; + d.connected = true; + d.available = false; + return d; + } + }); + + // edge case: freshly connected device has different "passphrase_protection" than saved instances + // need to automatically create another instance with default instance name + if (hasDifferentPassphraseSettings && !hasInstancesWithPassphraseSettings) { + const instance = getNewInstance(affectedDevices, device); + + newDevice.instance = instance; + newDevice.instanceLabel = `${device.label} (${instance})`; + + changedDevices.push(newDevice); + } + + return otherDevices.concat(changedDevices); + + } else { + return otherDevices.concat([newDevice]); + } +} + + +const duplicate = (state: State, device: TrezorDevice): State => { + + if (!device.features) return state; + + const newState: State = [ ...state ]; + + const instance: number = getNewInstance(state, device); + + const newDevice: TrezorDevice = { + ...device, + // acquiring: false, + remember: false, + state: null, + // instance, (instance is already part of device - added in modal) + instanceLabel: `${device.label} (${ device.instanceName || instance })`, + ts: new Date().getTime(), + } + newState.push(newDevice); + + return newState; +} + +const changeDevice = (state: State, device: Device): State => { + + // change only acquired devices + if (!device.features) return state; + + // find devices with the same device_id and passphrase_protection settings + // or devices with the same path (TODO: should be that way?) + const affectedDevices: Array = state.filter(d => + (d.features && d.features.device_id === device.features.device_id && d.features.passphrase_protection === device.features.passphrase_protection) || + (d.features && d.path.length > 0 && d.path === device.path) + ); + + const otherDevices: Array = state.filter(d => affectedDevices.indexOf(d) === -1); + + if (affectedDevices.length > 0) { + // merge incoming device with State + const changedDevices = affectedDevices.map(d => mergeDevices(d, device)); + return otherDevices.concat(changedDevices); + } + + return state; +} + +const authDevice = (state: State, device: TrezorDevice, deviceState: string): State => { + const affectedDevice: ?TrezorDevice = state.find(d => d.path === device.path && d.instance === device.instance); + // device could already have own state from trezor-connect, do not override it + if (affectedDevice && !affectedDevice.state) { + const otherDevices: Array = state.filter(d => d !== affectedDevice); + affectedDevice.state = deviceState; + return otherDevices.concat([affectedDevice]); + } + return state; +} + + +// Transform JSON form local storage into State +const devicesFromStorage = (devices: Array): State => { + return devices.map( (d: TrezorDevice) => { + return { + ...d, + connected: false, + available: false, + path: '', + acquiring: false, + featuresNeedsReload: false, + isUsedElsewhere: false + } + }); +} + +// Remove all device reference from State +const forgetDevice = (state: State, device: TrezorDevice): State => { + // remove all instances after disconnect (remember request declined) + // TODO: filter clones + return state.filter(d => d.remember || (d.features && device.features && d.features.device_id !== device.features.device_id) || (!d.features && d.path !== device.path)); +} + +// Remove single device reference from State +const forgetSingleDevice = (state: State, device: TrezorDevice): State => { + // remove only one instance (called from Aside button) + const newState: State = [ ...state ]; + newState.splice(newState.indexOf(device), 1); + return newState; +} + +const disconnectDevice = (state: State, device: Device): State => { + + const affectedDevices: State = state.filter(d => d.path === device.path || (d.features && device.features && d.features.device_id === device.features.device_id)); + const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1); + + if (affectedDevices.length > 0) { + const acquiredDevices = affectedDevices.filter(d => !d.unacquired && d.state); + return otherDevices.concat( acquiredDevices.map(d => { + d.connected = false; + d.available = false; + d.isUsedElsewhere = false; + d.featuresNeedsReload = false; + d.path = ''; + return d; + })); + } + + return state; +} + +const onSelectedDevice = (state: State, device: ?TrezorDevice): State => { + if (device) { + const otherDevices: Array = state.filter(d => d !== device); + return otherDevices.concat([ { ...device, ts: new Date().getTime() } ]); + } + return state; +} + +export default function devices(state: State = initialState, action: Action): State { + + switch (action.type) { + + case CONNECT.DEVICE_FROM_STORAGE : + return devicesFromStorage(action.payload); + + case CONNECT.DUPLICATE : + return duplicate(state, action.device); + + case CONNECT.AUTH_DEVICE : + return authDevice(state, action.device, action.state); + + case CONNECT.REMEMBER : + return changeDevice(state, { ...action.device, path: '', remember: true } ); + + case CONNECT.FORGET : + return forgetDevice(state, action.device); + case CONNECT.FORGET_SINGLE : + return forgetSingleDevice(state, action.device); + + case DEVICE.CONNECT : + case DEVICE.CONNECT_UNACQUIRED : + return addDevice(state, action.device); + + case DEVICE.CHANGED : + return changeDevice(state, { ...action.device, connected: true, available: true }); + // TODO: check if available will propagate to unavailable + + case DEVICE.DISCONNECT : + case DEVICE.DISCONNECT_UNACQUIRED : + return disconnectDevice(state, action.device); + + case WALLET.SET_SELECTED_DEVICE : + return onSelectedDevice(state, action.device); + + default: + return state; + } + +} + +// UTILS + +export const getNewInstance = (devices: State, device: Device | TrezorDevice): number => { + + const affectedDevices: State = devices.filter(d => d.features && device.features && d.features.device_id === device.features.device_id) + .sort((a, b) => { + if (!a.instance) { + return -1; + } else { + return !b.instance || a.instance > b.instance ? 1 : -1; + } + }); + + const instance: number = affectedDevices.reduce((inst, dev) => { + return dev.instance ? dev.instance + 1 : inst + 1; + }, 0); + + return instance; +} + +export const findDevice = (devices: State, deviceId: string, deviceState: string, instance: ?number): ?TrezorDevice => { + return devices.find(d => { + // TODO: && (instance && d.instance === instance) + if (d.features && d.features.device_id === deviceId && d.state === deviceState) { + return true; + } + return false; + }); +} \ No newline at end of file diff --git a/src/js/reducers/index.js b/src/js/reducers/index.js index 973bc404..37e3a1b4 100644 --- a/src/js/reducers/index.js +++ b/src/js/reducers/index.js @@ -20,6 +20,7 @@ import discovery from './DiscoveryReducer.js'; import pending from './PendingTxReducer.js'; import fiat from './FiatRateReducer.js'; import wallet from './WalletReducer.js'; +import devices from './DevicesReducer.js'; const reducers = { router: routerReducer, @@ -38,7 +39,8 @@ const reducers = { discovery, pending, fiat, - wallet + wallet, + devices } export type Reducers = typeof reducers;