2018-05-23 08:52:25 +00:00
|
|
|
/* @flow */
|
2018-07-30 10:52:13 +00:00
|
|
|
|
2018-08-20 11:01:43 +00:00
|
|
|
import { DEVICE } from 'trezor-connect';
|
2018-07-30 10:52:13 +00:00
|
|
|
import type { Device } from 'trezor-connect';
|
2018-08-14 13:11:52 +00:00
|
|
|
import * as CONNECT from 'actions/constants/TrezorConnect';
|
|
|
|
import * as WALLET from 'actions/constants/wallet';
|
2018-08-20 11:01:43 +00:00
|
|
|
import { getDuplicateInstanceNumber } from 'reducers/utils';
|
2018-05-23 08:52:25 +00:00
|
|
|
|
2018-08-14 12:56:47 +00:00
|
|
|
import type { Action, TrezorDevice } from 'flowtype';
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
export type State = Array<TrezorDevice>;
|
|
|
|
|
|
|
|
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;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
2018-09-06 15:04:28 +00:00
|
|
|
let { instanceLabel } = current;
|
2018-05-23 08:52:25 +00:00
|
|
|
if (upcoming.label !== current.label) {
|
2018-07-30 10:52:13 +00:00
|
|
|
instanceLabel = upcoming.label;
|
2018-05-23 08:52:25 +00:00
|
|
|
if (typeof current.instance === 'number') {
|
|
|
|
instanceLabel += ` (${current.instanceName || current.instance})`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-08 09:42:12 +00:00
|
|
|
const extended = {
|
2018-05-23 08:52:25 +00:00
|
|
|
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,
|
2019-03-04 12:33:02 +00:00
|
|
|
instanceName:
|
|
|
|
typeof upcoming.instanceName === 'string'
|
|
|
|
? upcoming.instanceName
|
|
|
|
: current.instanceName,
|
2018-05-23 08:52:25 +00:00
|
|
|
state: current.state,
|
|
|
|
ts: typeof upcoming.ts === 'number' ? upcoming.ts : current.ts,
|
2019-03-04 12:33:02 +00:00
|
|
|
useEmptyPassphrase:
|
|
|
|
typeof upcoming.useEmptyPassphrase === 'boolean'
|
|
|
|
? upcoming.useEmptyPassphrase
|
|
|
|
: current.useEmptyPassphrase,
|
2018-08-14 14:06:34 +00:00
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
2018-08-08 09:42:12 +00:00
|
|
|
if (upcoming.type === 'acquired') {
|
2018-08-14 14:06:34 +00:00
|
|
|
return { ...upcoming, ...extended };
|
2019-03-04 12:33:02 +00:00
|
|
|
}
|
|
|
|
if (upcoming.type === 'unacquired' && current.features && current.state) {
|
2018-08-08 09:42:12 +00:00
|
|
|
// corner-case: trying to merge unacquired device with acquired
|
|
|
|
// make sure that sensitive fields will not be changed and device will remain acquired
|
|
|
|
return {
|
|
|
|
type: 'acquired',
|
|
|
|
path: upcoming.path,
|
|
|
|
...current,
|
2018-08-14 14:06:34 +00:00
|
|
|
...extended,
|
|
|
|
};
|
2018-08-08 09:42:12 +00:00
|
|
|
}
|
2018-08-14 14:06:34 +00:00
|
|
|
return {
|
|
|
|
...upcoming,
|
|
|
|
features: null,
|
|
|
|
...extended,
|
|
|
|
};
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
const addDevice = (state: State, device: Device): State => {
|
|
|
|
let affectedDevices: Array<TrezorDevice> = [];
|
|
|
|
let otherDevices: Array<TrezorDevice> = [];
|
|
|
|
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 {
|
2019-03-04 12:33:02 +00:00
|
|
|
affectedDevices = state.filter(
|
|
|
|
d => d.features && device.features && d.features.device_id === device.features.device_id
|
|
|
|
);
|
2018-05-23 08:52:25 +00:00
|
|
|
const unacquiredDevices = state.filter(d => d.path.length > 0 && d.path === device.path);
|
2019-03-04 12:33:02 +00:00
|
|
|
otherDevices = state.filter(
|
|
|
|
d => affectedDevices.indexOf(d) < 0 && unacquiredDevices.indexOf(d) < 0
|
|
|
|
);
|
2018-05-23 08:52:25 +00:00
|
|
|
}
|
|
|
|
|
2018-08-08 09:42:12 +00:00
|
|
|
const extended = {
|
2018-05-23 08:52:25 +00:00
|
|
|
remember: false,
|
|
|
|
connected: true,
|
|
|
|
available: true,
|
|
|
|
path: device.path,
|
|
|
|
label: device.label,
|
|
|
|
state: null,
|
|
|
|
instanceLabel: device.label,
|
|
|
|
instanceName: null,
|
|
|
|
ts: new Date().getTime(),
|
2018-10-05 13:31:03 +00:00
|
|
|
useEmptyPassphrase: true,
|
2018-08-14 14:06:34 +00:00
|
|
|
};
|
2018-08-08 09:42:12 +00:00
|
|
|
|
2019-03-04 12:33:02 +00:00
|
|
|
const newDevice: TrezorDevice =
|
|
|
|
device.type === 'acquired'
|
|
|
|
? {
|
|
|
|
...device,
|
|
|
|
...extended,
|
|
|
|
}
|
|
|
|
: {
|
|
|
|
...device,
|
|
|
|
features: null,
|
|
|
|
...extended,
|
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
if (affectedDevices.length > 0) {
|
2018-05-23 08:52:25 +00:00
|
|
|
// check if freshly added device has different "passphrase_protection" settings
|
|
|
|
|
2018-05-28 17:42:03 +00:00
|
|
|
// let hasDifferentPassphraseSettings: boolean = false;
|
|
|
|
// let hasInstancesWithPassphraseSettings: boolean = false;
|
|
|
|
// const changedDevices: Array<TrezorDevice> = 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;
|
|
|
|
// }
|
|
|
|
// });
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
// edge case: freshly connected device has different "passphrase_protection" than saved instances
|
|
|
|
// need to automatically create another instance with default instance name
|
2018-05-28 17:42:03 +00:00
|
|
|
// if (hasDifferentPassphraseSettings && !hasInstancesWithPassphraseSettings) {
|
2018-08-17 13:20:42 +00:00
|
|
|
// const instance = getDuplicateInstanceNumber(affectedDevices, device);
|
2018-05-28 17:42:03 +00:00
|
|
|
|
|
|
|
// newDevice.instance = instance;
|
|
|
|
// newDevice.instanceLabel = `${device.label} (${instance})`;
|
2018-05-23 08:52:25 +00:00
|
|
|
|
2018-05-28 17:42:03 +00:00
|
|
|
// changedDevices.push(newDevice);
|
|
|
|
// }
|
2018-05-23 08:52:25 +00:00
|
|
|
|
2019-03-04 12:33:02 +00:00
|
|
|
const changedDevices: Array<TrezorDevice> = affectedDevices
|
|
|
|
.filter(
|
|
|
|
d =>
|
|
|
|
d.features &&
|
|
|
|
device.features &&
|
|
|
|
d.features.passphrase_protection === device.features.passphrase_protection
|
|
|
|
)
|
|
|
|
.map(d => {
|
|
|
|
const extended2: Object = { connected: true, available: true };
|
|
|
|
return mergeDevices(d, { ...device, ...extended2 });
|
|
|
|
});
|
2018-05-28 17:42:03 +00:00
|
|
|
if (changedDevices.length !== affectedDevices.length) {
|
2018-05-23 08:52:25 +00:00
|
|
|
changedDevices.push(newDevice);
|
|
|
|
}
|
|
|
|
|
|
|
|
return otherDevices.concat(changedDevices);
|
|
|
|
}
|
2018-07-30 10:52:13 +00:00
|
|
|
return otherDevices.concat([newDevice]);
|
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
const duplicate = (state: State, device: TrezorDevice): State => {
|
|
|
|
if (!device.features) return state;
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
const newState: State = [...state];
|
2018-05-23 08:52:25 +00:00
|
|
|
|
2018-08-17 13:20:42 +00:00
|
|
|
const instance: number = getDuplicateInstanceNumber(state, device);
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
const newDevice: TrezorDevice = {
|
|
|
|
...device,
|
|
|
|
remember: false,
|
|
|
|
state: null,
|
|
|
|
// instance, (instance is already part of device - added in modal)
|
2018-07-30 10:52:13 +00:00
|
|
|
instanceLabel: `${device.label} (${device.instanceName || instance})`,
|
2018-05-23 08:52:25 +00:00
|
|
|
ts: new Date().getTime(),
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
newState.push(newDevice);
|
|
|
|
|
|
|
|
return newState;
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
2018-08-08 09:42:12 +00:00
|
|
|
const changeDevice = (state: State, device: Device | TrezorDevice, extended: Object): State => {
|
2018-05-23 08:52:25 +00:00
|
|
|
// 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?)
|
2019-03-04 12:33:02 +00:00
|
|
|
const affectedDevices: Array<TrezorDevice> = state.filter(
|
|
|
|
d =>
|
|
|
|
(d.features &&
|
|
|
|
device.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)
|
|
|
|
);
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
const otherDevices: Array<TrezorDevice> = state.filter(d => affectedDevices.indexOf(d) === -1);
|
|
|
|
|
|
|
|
if (affectedDevices.length > 0) {
|
|
|
|
// merge incoming device with State
|
2019-03-04 12:33:02 +00:00
|
|
|
const changedDevices = affectedDevices.map(d =>
|
|
|
|
mergeDevices(d, { ...device, ...extended })
|
|
|
|
);
|
2018-05-23 08:52:25 +00:00
|
|
|
return otherDevices.concat(changedDevices);
|
|
|
|
}
|
|
|
|
|
|
|
|
return state;
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
const authDevice = (state: State, device: TrezorDevice, deviceState: string): State => {
|
2019-03-04 12:33:02 +00:00
|
|
|
const affectedDevice: ?TrezorDevice = state.find(
|
|
|
|
d => d.path === device.path && d.instance === device.instance
|
|
|
|
);
|
2018-05-23 08:52:25 +00:00
|
|
|
// device could already have own state from trezor-connect, do not override it
|
2018-10-03 17:54:22 +00:00
|
|
|
if (affectedDevice && !affectedDevice.state && affectedDevice.type === 'acquired') {
|
2018-05-23 08:52:25 +00:00
|
|
|
const otherDevices: Array<TrezorDevice> = state.filter(d => d !== affectedDevice);
|
2018-10-03 17:54:22 +00:00
|
|
|
return otherDevices.concat([{ ...affectedDevice, state: deviceState }]);
|
2018-05-23 08:52:25 +00:00
|
|
|
}
|
|
|
|
return state;
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
// Transform JSON form local storage into State
|
2019-03-04 12:33:02 +00:00
|
|
|
const devicesFromStorage = ($devices: Array<TrezorDevice>): State =>
|
|
|
|
$devices.map((device: TrezorDevice) => {
|
|
|
|
const extended = {
|
|
|
|
connected: false,
|
|
|
|
available: false,
|
|
|
|
path: '',
|
|
|
|
};
|
2018-08-08 09:42:12 +00:00
|
|
|
|
2019-03-04 12:33:02 +00:00
|
|
|
return device.type === 'acquired'
|
|
|
|
? {
|
|
|
|
...device,
|
|
|
|
...extended,
|
|
|
|
}
|
|
|
|
: {
|
|
|
|
...device,
|
|
|
|
features: null,
|
|
|
|
...extended,
|
|
|
|
};
|
|
|
|
});
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
// Remove all device reference from State
|
2019-03-04 12:33:02 +00:00
|
|
|
const forgetDevice = (state: State, device: TrezorDevice): State =>
|
|
|
|
state.filter(
|
|
|
|
d =>
|
|
|
|
d.remember ||
|
|
|
|
(d.features && device.features && d.features.device_id !== device.features.device_id) ||
|
|
|
|
(!d.features && d.path !== device.path)
|
|
|
|
);
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
// Remove single device reference from State
|
|
|
|
const forgetSingleDevice = (state: State, device: TrezorDevice): State => {
|
|
|
|
// remove only one instance (called from Aside button)
|
2018-07-30 10:52:13 +00:00
|
|
|
const newState: State = [...state];
|
2018-05-23 08:52:25 +00:00
|
|
|
newState.splice(newState.indexOf(device), 1);
|
|
|
|
return newState;
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
const disconnectDevice = (state: State, device: Device): State => {
|
2019-03-04 12:33:02 +00:00
|
|
|
const affectedDevices: State = state.filter(
|
|
|
|
d =>
|
|
|
|
d.path === device.path ||
|
|
|
|
(d.features && device.features && d.features.device_id === device.features.device_id)
|
|
|
|
);
|
2018-05-23 08:52:25 +00:00
|
|
|
const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1);
|
|
|
|
|
|
|
|
if (affectedDevices.length > 0) {
|
2019-03-04 12:33:02 +00:00
|
|
|
const acquiredDevices = affectedDevices
|
|
|
|
.filter(d => d.features && d.state)
|
|
|
|
.map(d => {
|
|
|
|
// eslint-disable-line arrow-body-style
|
|
|
|
return d.type === 'acquired'
|
|
|
|
? {
|
|
|
|
...d,
|
|
|
|
connected: false,
|
|
|
|
available: false,
|
|
|
|
status: 'available',
|
|
|
|
path: '',
|
|
|
|
}
|
|
|
|
: d;
|
|
|
|
});
|
2018-08-08 09:42:12 +00:00
|
|
|
return otherDevices.concat(acquiredDevices);
|
2018-05-23 08:52:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return state;
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
|
|
|
const onSelectedDevice = (state: State, device: ?TrezorDevice): State => {
|
2018-10-02 08:57:17 +00:00
|
|
|
if (!device) return state;
|
|
|
|
|
|
|
|
const otherDevices: Array<TrezorDevice> = state.filter(d => d !== device);
|
2019-03-04 12:33:02 +00:00
|
|
|
const extended =
|
|
|
|
device.type === 'acquired'
|
|
|
|
? {
|
|
|
|
...device,
|
|
|
|
ts: new Date().getTime(),
|
|
|
|
}
|
|
|
|
: {
|
|
|
|
...device,
|
|
|
|
features: null,
|
|
|
|
ts: new Date().getTime(),
|
|
|
|
};
|
2018-10-02 08:57:17 +00:00
|
|
|
return otherDevices.concat([extended]);
|
2018-07-30 10:52:13 +00:00
|
|
|
};
|
2018-05-23 08:52:25 +00:00
|
|
|
|
2018-10-05 13:31:03 +00:00
|
|
|
const onChangeWalletType = (state: State, device: TrezorDevice, hidden: boolean): State => {
|
2019-03-04 12:33:02 +00:00
|
|
|
const affectedDevices: State = state.filter(
|
|
|
|
d =>
|
|
|
|
d.path === device.path ||
|
|
|
|
(d.features && device.features && d.features.device_id === device.features.device_id)
|
|
|
|
);
|
2018-10-05 13:31:03 +00:00
|
|
|
const otherDevices: State = state.filter(d => affectedDevices.indexOf(d) === -1);
|
|
|
|
if (affectedDevices.length > 0) {
|
2019-03-04 12:33:02 +00:00
|
|
|
const changedDevices = affectedDevices.map(d => {
|
|
|
|
// eslint-disable-line arrow-body-style
|
|
|
|
return d.type === 'acquired'
|
|
|
|
? {
|
|
|
|
...d,
|
|
|
|
remember: false,
|
|
|
|
state: null,
|
|
|
|
useEmptyPassphrase: !hidden,
|
|
|
|
ts: new Date().getTime(),
|
|
|
|
}
|
|
|
|
: d;
|
2018-10-05 13:31:03 +00:00
|
|
|
});
|
|
|
|
return otherDevices.concat(changedDevices);
|
|
|
|
}
|
|
|
|
return state;
|
|
|
|
};
|
|
|
|
|
2018-05-23 08:52:25 +00:00
|
|
|
export default function devices(state: State = initialState, action: Action): State {
|
|
|
|
switch (action.type) {
|
2018-07-30 10:52:13 +00:00
|
|
|
case CONNECT.DEVICE_FROM_STORAGE:
|
2018-05-23 08:52:25 +00:00
|
|
|
return devicesFromStorage(action.payload);
|
2018-07-30 10:52:13 +00:00
|
|
|
|
|
|
|
case CONNECT.DUPLICATE:
|
2018-05-23 08:52:25 +00:00
|
|
|
return duplicate(state, action.device);
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
case CONNECT.AUTH_DEVICE:
|
2018-05-23 08:52:25 +00:00
|
|
|
return authDevice(state, action.device, action.state);
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
case CONNECT.REMEMBER:
|
2018-08-08 09:42:12 +00:00
|
|
|
// return changeDevice(state, { ...action.device, path: '', remember: true });
|
|
|
|
return changeDevice(state, action.device, { path: '', remember: true });
|
2018-07-30 10:52:13 +00:00
|
|
|
|
|
|
|
case CONNECT.FORGET:
|
2018-05-23 08:52:25 +00:00
|
|
|
return forgetDevice(state, action.device);
|
2018-07-30 10:52:13 +00:00
|
|
|
case CONNECT.FORGET_SINGLE:
|
2018-10-05 15:55:26 +00:00
|
|
|
case CONNECT.FORGET_SILENT:
|
2018-05-23 08:52:25 +00:00
|
|
|
return forgetSingleDevice(state, action.device);
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
case DEVICE.CONNECT:
|
|
|
|
case DEVICE.CONNECT_UNACQUIRED:
|
2018-05-23 08:52:25 +00:00
|
|
|
return addDevice(state, action.device);
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
case DEVICE.CHANGED:
|
2018-08-08 09:42:12 +00:00
|
|
|
// return changeDevice(state, { ...action.device, connected: true, available: true });
|
|
|
|
return changeDevice(state, action.device, { connected: true, available: true });
|
2019-03-04 12:33:02 +00:00
|
|
|
// TODO: check if available will propagate to unavailable
|
2018-05-23 08:52:25 +00:00
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
case DEVICE.DISCONNECT:
|
2019-03-04 12:33:02 +00:00
|
|
|
// case DEVICE.DISCONNECT_UNACQUIRED:
|
2018-05-23 08:52:25 +00:00
|
|
|
return disconnectDevice(state, action.device);
|
|
|
|
|
2018-07-30 10:52:13 +00:00
|
|
|
case WALLET.SET_SELECTED_DEVICE:
|
2018-05-23 08:52:25 +00:00
|
|
|
return onSelectedDevice(state, action.device);
|
|
|
|
|
2018-10-05 13:31:03 +00:00
|
|
|
case CONNECT.RECEIVE_WALLET_TYPE:
|
2018-11-01 17:47:44 +00:00
|
|
|
case CONNECT.UPDATE_WALLET_TYPE:
|
2018-10-05 13:31:03 +00:00
|
|
|
return onChangeWalletType(state, action.device, action.hidden);
|
|
|
|
|
2018-05-23 08:52:25 +00:00
|
|
|
default:
|
|
|
|
return state;
|
|
|
|
}
|
2019-03-04 12:33:02 +00:00
|
|
|
}
|