mirror of https://github.com/trezor/trezor-wallet
commit
976222476b
@ -1,11 +1,19 @@
|
||||
module.exports = {
|
||||
rootDir: './src',
|
||||
automock: false,
|
||||
coverageDirectory: 'coverage/',
|
||||
collectCoverage: true,
|
||||
testURL: 'http://localhost',
|
||||
modulePathIgnorePatterns: [
|
||||
'node_modules',
|
||||
'utils/windowUtils.js',
|
||||
'utils/promiseUtils.js',
|
||||
'utils/networkUtils.js',
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
'utils/**.js',
|
||||
],
|
||||
setupFiles: [
|
||||
'./support/setupJest.js',
|
||||
],
|
||||
};
|
||||
|
@ -0,0 +1,304 @@
|
||||
/* @flow */
|
||||
|
||||
import { push, LOCATION_CHANGE } from 'react-router-redux';
|
||||
import { routes } from 'support/routes';
|
||||
|
||||
import type {
|
||||
RouterLocationState,
|
||||
Device,
|
||||
TrezorDevice,
|
||||
ThunkAction,
|
||||
PayloadAction,
|
||||
Dispatch,
|
||||
GetState,
|
||||
} from 'flowtype';
|
||||
import type { RouterAction } from 'react-router-redux';
|
||||
|
||||
/*
|
||||
* Parse url string to RouterLocationState object (key/value)
|
||||
*/
|
||||
export const pathToParams = (path: string): PayloadAction<RouterLocationState> => (): RouterLocationState => {
|
||||
// split url into parts
|
||||
const parts: Array<string> = path.split('/').slice(1);
|
||||
const params: RouterLocationState = {};
|
||||
// return empty params
|
||||
if (parts.length < 1 || path === '/') return params;
|
||||
|
||||
// map parts to params by key/value
|
||||
// assuming that url is in format: "/key/value"
|
||||
for (let i = 0, len = parts.length; i < len; i += 2) {
|
||||
params[parts[i]] = parts[i + 1] || parts[i];
|
||||
}
|
||||
|
||||
// check for special case: /device/device-id:instance-id
|
||||
if (params.hasOwnProperty('device')) {
|
||||
const isClonedDevice: Array<string> = params.device.split(':');
|
||||
if (isClonedDevice.length > 1) {
|
||||
const [device, instance] = isClonedDevice;
|
||||
params.device = device;
|
||||
params.deviceInstance = instance;
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
/*
|
||||
* RouterLocationState validation
|
||||
* Check if requested device or network exists in reducers
|
||||
*/
|
||||
export const paramsValidation = (params: RouterLocationState): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
|
||||
// validate requested device
|
||||
if (params.hasOwnProperty('device')) {
|
||||
const { devices } = getState();
|
||||
|
||||
let device: ?TrezorDevice;
|
||||
if (params.hasOwnProperty('deviceInstance')) {
|
||||
device = devices.find(d => d.features && d.features.device_id === params.device && d.instance === parseInt(params.deviceInstance, 10));
|
||||
} else {
|
||||
device = devices.find(d => d.path === params.device || (d.features && d.features.device_id === params.device));
|
||||
}
|
||||
|
||||
if (!device) return false;
|
||||
}
|
||||
|
||||
// validate requested network
|
||||
if (params.hasOwnProperty('network')) {
|
||||
const { config } = getState().localStorage;
|
||||
const coin = config.coins.find(c => c.network === params.network);
|
||||
if (!coin) return false;
|
||||
if (!params.account) return false;
|
||||
}
|
||||
|
||||
// validate requested account
|
||||
// TODO: only if discovery on this network is completed
|
||||
// if (params.hasOwnProperty('account')) {
|
||||
|
||||
// }
|
||||
return true;
|
||||
};
|
||||
|
||||
/*
|
||||
* Composing url string from given RouterLocationState object
|
||||
* Filters unrecognized fields and sorting in correct order
|
||||
*/
|
||||
export const paramsToPath = (params: RouterLocationState): PayloadAction<?string> => (): ?string => {
|
||||
// get patterns (fields) from routes and sort them by complexity
|
||||
const patterns: Array<Array<string>> = routes.map(r => r.fields).sort((a, b) => (a.length > b.length ? -1 : 1));
|
||||
|
||||
// find pattern
|
||||
const keys: Array<string> = Object.keys(params);
|
||||
let patternToUse: ?Array<string>;
|
||||
let i: number;
|
||||
for (i = 0; i < patterns.length; i++) {
|
||||
const pattern = patterns[i];
|
||||
const match: Array<string> = keys.filter(key => pattern.indexOf(key) >= 0);
|
||||
if (match.length === pattern.length) {
|
||||
patternToUse = pattern;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// pattern not found, redirect back
|
||||
if (!patternToUse) return null;
|
||||
|
||||
// compose url string from pattern
|
||||
let url: string = '';
|
||||
patternToUse.forEach((field) => {
|
||||
if (field === params[field]) {
|
||||
// standalone (odd) fields
|
||||
url += `/${field}`;
|
||||
} else {
|
||||
url += `/${field}/${params[field]}`;
|
||||
if (field === 'device') {
|
||||
if (params.hasOwnProperty('deviceInstance')) {
|
||||
url += `:${params.deviceInstance}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getValidUrl = (action: RouterAction): PayloadAction<string> => (dispatch: Dispatch, getState: GetState): string => {
|
||||
const { location } = getState().router;
|
||||
|
||||
// redirect to landing page (loading screen)
|
||||
// and wait until application is ready
|
||||
if (!location) return '/';
|
||||
|
||||
const requestedUrl = action.payload.pathname;
|
||||
// Corner case: LOCATION_CHANGE was called but pathname didn't changed (redirect action from RouterService)
|
||||
if (requestedUrl === location.pathname) return requestedUrl;
|
||||
|
||||
// Modal is opened
|
||||
// redirect to previous url
|
||||
if (getState().modal.opened) {
|
||||
// Corner case: modal is opened and currentParams are still valid
|
||||
// example 1 (valid blocking): url changed while passphrase modal opened but device is still connected (we want user to finish this action)
|
||||
// example 2 (invalid blocking): url changes while passphrase modal opened because device disconnect
|
||||
const currentParams = dispatch(pathToParams(location.pathname));
|
||||
const currentParamsAreValid = dispatch(paramsValidation(currentParams));
|
||||
if (currentParamsAreValid) { return location.pathname; }
|
||||
}
|
||||
|
||||
// there are no connected devices or application isn't ready or initialization error occurred
|
||||
// redirect to landing page
|
||||
const shouldBeLandingPage = getState().devices.length < 1 || !getState().wallet.ready || getState().connect.error !== null;
|
||||
const landingPageUrl = dispatch(isLandingPageUrl(requestedUrl));
|
||||
if (shouldBeLandingPage) {
|
||||
return !landingPageUrl ? '/' : requestedUrl;
|
||||
}
|
||||
|
||||
// Disallow displaying landing page
|
||||
// redirect to previous url
|
||||
if (!shouldBeLandingPage && landingPageUrl) {
|
||||
return location.pathname;
|
||||
}
|
||||
|
||||
// Regular url change during application live cycle
|
||||
const requestedParams = dispatch(pathToParams(requestedUrl));
|
||||
const requestedParamsAreValid: boolean = dispatch(paramsValidation(requestedParams));
|
||||
|
||||
// Requested params are not valid
|
||||
// Neither device or network doesn't exists
|
||||
if (!requestedParamsAreValid) {
|
||||
return location.pathname;
|
||||
}
|
||||
|
||||
// Compose valid url from requested params
|
||||
const composedUrl = dispatch(paramsToPath(requestedParams));
|
||||
return composedUrl || location.pathname;
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Utility used in "selectDevice" and "selectFirstAvailableDevice"
|
||||
* sorting device array by "ts" (timestamp) field
|
||||
*/
|
||||
const sortDevices = (devices: Array<TrezorDevice>): Array<TrezorDevice> => devices.sort((a, b) => {
|
||||
if (!a.ts || !b.ts) {
|
||||
return -1;
|
||||
}
|
||||
return a.ts > b.ts ? -1 : 1;
|
||||
});
|
||||
|
||||
/*
|
||||
* Compose url from given device object and redirect
|
||||
*/
|
||||
export const selectDevice = (device: TrezorDevice | Device): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
let url: ?string;
|
||||
if (!device.features) {
|
||||
url = `/device/${device.path}/${device.type === 'unreadable' ? 'unreadable' : 'acquire'}`;
|
||||
} else if (device.features.bootloader_mode) {
|
||||
url = `/device/${device.path}/bootloader`;
|
||||
} else if (!device.features.initialized) {
|
||||
url = `/device/${device.features.device_id}/initialize`;
|
||||
} else if (typeof device.instance === 'number') {
|
||||
url = `/device/${device.features.device_id}:${device.instance}`;
|
||||
} else {
|
||||
url = `/device/${device.features.device_id}`;
|
||||
// make sure that device is not TrezorDevice type
|
||||
if (!device.hasOwnProperty('ts')) {
|
||||
// it is device from trezor-connect triggered by DEVICE.CONNECT event
|
||||
// need to lookup if there are unavailable instances
|
||||
const available: Array<TrezorDevice> = getState().devices.filter(d => d.path === device.path);
|
||||
const latest: Array<TrezorDevice> = sortDevices(available);
|
||||
if (latest.length > 0 && latest[0].instance) {
|
||||
url += `:${latest[0].instance}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentParams: RouterLocationState = getState().router.location.state;
|
||||
const requestedParams = dispatch(pathToParams(url));
|
||||
if (currentParams.device !== requestedParams.device || currentParams.deviceInstance !== requestedParams.deviceInstance) {
|
||||
dispatch(goto(url));
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Try to find first available device using order:
|
||||
* 1. First unacquired
|
||||
* 2. First connected
|
||||
* 3. Saved with latest timestamp
|
||||
* OR redirect to landing page
|
||||
*/
|
||||
export const selectFirstAvailableDevice = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const { devices } = getState();
|
||||
if (devices.length > 0) {
|
||||
const unacquired = devices.find(d => !d.features);
|
||||
if (unacquired) {
|
||||
dispatch(selectDevice(unacquired));
|
||||
} else {
|
||||
const latest: Array<TrezorDevice> = sortDevices(devices);
|
||||
const firstConnected: ?TrezorDevice = latest.find(d => d.connected);
|
||||
dispatch(selectDevice(firstConnected || latest[0]));
|
||||
}
|
||||
} else {
|
||||
dispatch(gotoLandingPage());
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Internal method. redirect to given url
|
||||
*/
|
||||
const goto = (url: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
if (getState().router.location.pathname !== url) {
|
||||
dispatch(push(url));
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Check if requested OR current url is landing page
|
||||
*/
|
||||
export const isLandingPageUrl = ($url?: string): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
|
||||
let url: ?string = $url;
|
||||
if (typeof url !== 'string') {
|
||||
url = getState().router.location.pathname;
|
||||
}
|
||||
// TODO: add more landing page cases/urls to config.json (like /tools etc)
|
||||
return (url === '/' || url === '/bridge');
|
||||
};
|
||||
|
||||
/*
|
||||
* Try to redirect to landing page
|
||||
*/
|
||||
export const gotoLandingPage = (): ThunkAction => (dispatch: Dispatch): void => {
|
||||
const isLandingPage = dispatch(isLandingPageUrl());
|
||||
if (!isLandingPage) {
|
||||
dispatch(goto('/'));
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Go to given device settings page
|
||||
*/
|
||||
export const gotoDeviceSettings = (device: TrezorDevice): ThunkAction => (dispatch: Dispatch): void => {
|
||||
if (device.features) {
|
||||
const devUrl: string = `${device.features.device_id}${device.instance ? `:${device.instance}` : ''}`;
|
||||
dispatch(goto(`/device/${devUrl}/settings`));
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Try to redirect to initial url
|
||||
*/
|
||||
export const setInitialUrl = (): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
|
||||
const { initialPathname } = getState().wallet;
|
||||
if (typeof initialPathname === 'string' && !dispatch(isLandingPageUrl(initialPathname))) {
|
||||
const valid = dispatch(getValidUrl({
|
||||
type: LOCATION_CHANGE,
|
||||
payload: {
|
||||
pathname: initialPathname,
|
||||
hash: '',
|
||||
search: '',
|
||||
state: {},
|
||||
},
|
||||
}));
|
||||
if (valid === initialPathname) {
|
||||
dispatch(goto(valid));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
@ -1,156 +1,46 @@
|
||||
/* @flow */
|
||||
import { LOCATION_CHANGE/* , replace */ } from 'react-router-redux';
|
||||
import * as CONNECT from 'actions/constants/TrezorConnect';
|
||||
import * as WALLET from 'actions/constants/wallet';
|
||||
import * as NotificationActions from 'actions/NotificationActions';
|
||||
import { LOCATION_CHANGE, replace } from 'react-router-redux';
|
||||
import * as RouterActions from 'actions/RouterActions';
|
||||
|
||||
import type {
|
||||
Middleware,
|
||||
MiddlewareAPI,
|
||||
MiddlewareDispatch,
|
||||
Action,
|
||||
RouterLocationState,
|
||||
TrezorDevice,
|
||||
} from 'flowtype';
|
||||
|
||||
/**
|
||||
* Middleware used for init application and managing router path.
|
||||
* Redux Middleware used for managing router path
|
||||
* This middleware couldn't use async/await because LOCATION_CHANGE action is also synchronized with RouterReducer (react-router-redux)
|
||||
*/
|
||||
|
||||
const pathToParams = (path: string): RouterLocationState => {
|
||||
const urlParts: Array<string> = path.split('/').slice(1);
|
||||
const params: RouterLocationState = {};
|
||||
if (urlParts.length < 1 || path === '/') return params;
|
||||
|
||||
for (let i = 0, len = urlParts.length; i < len; i += 2) {
|
||||
params[urlParts[i]] = urlParts[i + 1] || urlParts[i];
|
||||
}
|
||||
|
||||
if (params.hasOwnProperty('device')) {
|
||||
const isClonedDevice: Array<string> = params.device.split(':');
|
||||
if (isClonedDevice.length > 1) {
|
||||
params.device = isClonedDevice[0];
|
||||
params.deviceInstance = isClonedDevice[1];
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
const validation = (api: MiddlewareAPI, params: RouterLocationState): boolean => {
|
||||
if (params.hasOwnProperty('device')) {
|
||||
const { devices } = api.getState();
|
||||
|
||||
let device: ?TrezorDevice;
|
||||
if (params.hasOwnProperty('deviceInstance')) {
|
||||
device = devices.find(d => d.features && d.features.device_id === params.device && d.instance === parseInt(params.deviceInstance, 10));
|
||||
} else {
|
||||
device = devices.find(d => d.path === params.device || (d.features && d.features.device_id === params.device));
|
||||
}
|
||||
|
||||
if (!device) return false;
|
||||
}
|
||||
|
||||
if (params.hasOwnProperty('network')) {
|
||||
const { config } = api.getState().localStorage;
|
||||
const coin = config.coins.find(c => c.network === params.network);
|
||||
if (!coin) return false;
|
||||
if (!params.account) return false;
|
||||
}
|
||||
|
||||
// if (params.account) {
|
||||
|
||||
// }
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
let __unloading: boolean = false;
|
||||
|
||||
const RouterService: Middleware = (api: MiddlewareAPI) => (next: MiddlewareDispatch) => (action: Action): Action => {
|
||||
if (action.type === WALLET.ON_BEFORE_UNLOAD) {
|
||||
__unloading = true;
|
||||
} else if (action.type === LOCATION_CHANGE && !__unloading) {
|
||||
const { location } = api.getState().router;
|
||||
const { devices } = api.getState();
|
||||
const { error } = api.getState().connect;
|
||||
|
||||
const requestedParams: RouterLocationState = pathToParams(action.payload.pathname);
|
||||
const currentParams: RouterLocationState = pathToParams(location ? location.pathname : '/');
|
||||
const postActions: Array<Action> = [];
|
||||
|
||||
let redirectPath: ?string;
|
||||
// first event after application loads
|
||||
if (!location) {
|
||||
postActions.push({
|
||||
type: WALLET.SET_INITIAL_URL,
|
||||
pathname: action.payload.pathname,
|
||||
state: requestedParams,
|
||||
});
|
||||
|
||||
redirectPath = '/';
|
||||
} else {
|
||||
const isModalOpened: boolean = api.getState().modal.opened;
|
||||
// there are no devices attached or initialization error occurs
|
||||
const landingPage: boolean = devices.length < 1 || error !== null;
|
||||
|
||||
// modal is still opened and currentPath is still valid
|
||||
// example 1 (valid blocking): url changes while passphrase modal opened but device is still connected (we want user to finish this action)
|
||||
// example 2 (invalid blocking): url changes while passphrase modal opened because device disconnect
|
||||
if (isModalOpened && action.payload.pathname !== location.pathname && validation(api, currentParams)) {
|
||||
redirectPath = location.pathname;
|
||||
console.warn('Modal still opened');
|
||||
} else if (landingPage) {
|
||||
// keep route on landing page
|
||||
if (action.payload.pathname !== '/' && action.payload.pathname !== '/bridge') {
|
||||
redirectPath = '/';
|
||||
}
|
||||
} else {
|
||||
// PATH VALIDATION
|
||||
// redirect from root view
|
||||
if (action.payload.pathname === '/' || !validation(api, requestedParams)) {
|
||||
// TODO redirect to first device
|
||||
redirectPath = '/device/x';
|
||||
} else if (requestedParams.device) {
|
||||
if (requestedParams.network !== currentParams.network) {
|
||||
postActions.push({
|
||||
type: CONNECT.COIN_CHANGED,
|
||||
payload: {
|
||||
network: requestedParams.network,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// make sure that middleware should process this action
|
||||
if (action.type !== LOCATION_CHANGE || api.getState().wallet.unloading) {
|
||||
// pass action
|
||||
return next(action);
|
||||
}
|
||||
|
||||
if (redirectPath) {
|
||||
console.warn('Redirecting...', redirectPath);
|
||||
// override action to keep routerReducer sync
|
||||
const url: string = redirectPath;
|
||||
action.payload.state = pathToParams(url);
|
||||
action.payload.pathname = url;
|
||||
// change url
|
||||
// api.dispatch(replace(url));
|
||||
} else {
|
||||
action.payload.state = requestedParams;
|
||||
// compose valid url
|
||||
const validUrl = api.dispatch(RouterActions.getValidUrl(action));
|
||||
// override action state (to be stored in RouterReducer)
|
||||
const override = action;
|
||||
override.payload.state = api.dispatch(RouterActions.pathToParams(validUrl));
|
||||
const redirect = action.payload.pathname !== validUrl;
|
||||
if (redirect) {
|
||||
// override action pathname
|
||||
override.payload.pathname = validUrl;
|
||||
}
|
||||
|
||||
// resolve LOCATION_CHANGE action
|
||||
next(action);
|
||||
|
||||
// resolve post actions
|
||||
postActions.forEach((a) => {
|
||||
api.dispatch(a);
|
||||
});
|
||||
// pass action
|
||||
next(override);
|
||||
|
||||
api.dispatch(NotificationActions.clear(currentParams, requestedParams));
|
||||
|
||||
return action;
|
||||
if (redirect) {
|
||||
// replace invalid url
|
||||
api.dispatch(replace(validUrl));
|
||||
}
|
||||
|
||||
// Pass all actions through by default
|
||||
return next(action);
|
||||
return override;
|
||||
};
|
||||
|
||||
export default RouterService;
|
@ -0,0 +1,94 @@
|
||||
/* @flow */
|
||||
|
||||
export type Route = {
|
||||
+name: string;
|
||||
+pattern: string;
|
||||
fields: Array<string>;
|
||||
}
|
||||
|
||||
export const routes: Array<Route> = [
|
||||
{
|
||||
name: 'landing-home',
|
||||
pattern: '/',
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
name: 'landing-bridge',
|
||||
pattern: '/bridge',
|
||||
fields: ['bridge'],
|
||||
},
|
||||
{
|
||||
name: 'landing-import',
|
||||
pattern: '/import',
|
||||
fields: ['import'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-setting',
|
||||
pattern: '/settings',
|
||||
fields: ['settings'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-acquire',
|
||||
pattern: '/device/:device/acquire',
|
||||
fields: ['device', 'acquire'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-unreadable',
|
||||
pattern: '/device/:device/unreadable',
|
||||
fields: ['device', 'unreadable'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-bootloader',
|
||||
pattern: '/device/:device/bootloader',
|
||||
fields: ['device', 'bootloader'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-initialize',
|
||||
pattern: '/device/:device/initialize',
|
||||
fields: ['device', 'initialize'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-device-settings',
|
||||
pattern: '/device/:device/settings',
|
||||
fields: ['device', 'settings'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-dashboard',
|
||||
pattern: '/device/:device',
|
||||
fields: ['device'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-account-summary',
|
||||
pattern: '/device/:device/network/:network/account/:account',
|
||||
fields: ['device', 'network', 'account'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-account-send',
|
||||
pattern: '/device/:device/network/:network/account/:account/send',
|
||||
fields: ['device', 'network', 'account', 'send'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-account-send-override',
|
||||
pattern: '/device/:device/network/:network/account/:account/send/override',
|
||||
fields: ['device', 'network', 'account', 'send'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-account-receive',
|
||||
pattern: '/device/:device/network/:network/account/:account/receive',
|
||||
fields: ['device', 'network', 'account', 'receive'],
|
||||
},
|
||||
{
|
||||
name: 'wallet-account-signverify',
|
||||
pattern: '/device/:device/network/:network/account/:account/signverify',
|
||||
fields: ['device', 'network', 'account', 'signverify'],
|
||||
},
|
||||
];
|
||||
|
||||
export const getPattern = (name: string): string => {
|
||||
const entry = routes.find(r => r.name === name);
|
||||
if (!entry) {
|
||||
console.error(`Route for ${name} not found`);
|
||||
return '/';
|
||||
}
|
||||
return entry.pattern;
|
||||
};
|
@ -0,0 +1 @@
|
||||
global.fetch = require('jest-fetch-mock');
|
@ -0,0 +1,45 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`format utils btckb2satoshib 1`] = `0`;
|
||||
|
||||
exports[`format utils btckb2satoshib 2`] = `100000`;
|
||||
|
||||
exports[`format utils btckb2satoshib 3`] = `200000`;
|
||||
|
||||
exports[`format utils btckb2satoshib 4`] = `10000000`;
|
||||
|
||||
exports[`format utils btckb2satoshib 5`] = `99900000`;
|
||||
|
||||
exports[`format utils formatAmount 1`] = `"0 btc"`;
|
||||
|
||||
exports[`format utils formatAmount 2`] = `"10 mBTC"`;
|
||||
|
||||
exports[`format utils formatAmount 3`] = `"0.000005 mBTC"`;
|
||||
|
||||
exports[`format utils formatAmount 4`] = `"1e-8 eth"`;
|
||||
|
||||
exports[`format utils formatAmount 5`] = `"0.00099999 tau"`;
|
||||
|
||||
exports[`format utils formatTime 1`] = `"No time estimate"`;
|
||||
|
||||
exports[`format utils formatTime 2`] = `"1 minutes"`;
|
||||
|
||||
exports[`format utils formatTime 3`] = `"2 minutes"`;
|
||||
|
||||
exports[`format utils formatTime 4`] = `"1 hour 40 minutes"`;
|
||||
|
||||
exports[`format utils formatTime 5`] = `"16 hours 39 minutes"`;
|
||||
|
||||
exports[`format utils formatTime 6`] = `"45 minutes"`;
|
||||
|
||||
exports[`format utils hexToString 1`] = `"test"`;
|
||||
|
||||
exports[`format utils hexToString 2`] = `"0001"`;
|
||||
|
||||
exports[`format utils hexToString 3`] = `"test99999"`;
|
||||
|
||||
exports[`format utils stringToHex 1`] = `"0074006500730074"`;
|
||||
|
||||
exports[`format utils stringToHex 2`] = `"0030003000300031"`;
|
||||
|
||||
exports[`format utils stringToHex 3`] = `"007400650073007400390039003900390039"`;
|
@ -1,11 +1,53 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as ethUtils from '../ethUtils';
|
||||
|
||||
describe('eth utils', () => {
|
||||
it('decimalToHex', () => {
|
||||
const input = [0, 1, 2, 100, 999];
|
||||
const input = [0, 1, 2, 100, 9999999999];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(ethUtils.decimalToHex(entry)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('hexToDecimal', () => {
|
||||
const input = ['2540be3ff', '64', '2', '1', '0', ''];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(ethUtils.hexToDecimal(entry)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('padLeftEven', () => {
|
||||
const input = ['2540be3ff'];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(ethUtils.padLeftEven(entry)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('sanitizeHex', () => {
|
||||
const input = ['0x2540be3ff', '1', '2', '100', 999, ''];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(ethUtils.sanitizeHex(entry)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('strip', () => {
|
||||
const input = ['0x', '0x2540be3ff', '2540be3ff'];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(ethUtils.strip(entry)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('calcGasPrice', () => {
|
||||
const input = [{ price: new BigNumber(9898998989), limit: '9' }];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(ethUtils.calcGasPrice(entry.price, entry.limit)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,49 @@
|
||||
import * as formatUtils from '../formatUtils';
|
||||
|
||||
describe('format utils', () => {
|
||||
it('formatAmount', () => {
|
||||
const input = [
|
||||
{ amount: 0, coinInfo: { isBitcoin: true, currencyUnits: 'mbtc', shortcut: 'btc' } },
|
||||
{ amount: 1000000, coinInfo: { isBitcoin: true, currencyUnits: 'mbtc', shortcut: 'btc' } },
|
||||
{ amount: 0.5, coinInfo: { isBitcoin: true, currencyUnits: 'mbtc', shortcut: 'btc' } },
|
||||
{ amount: 1, coinInfo: { isBitcoin: false, shortcut: 'eth' } },
|
||||
{ amount: 99999, coinInfo: { isBitcoin: false, shortcut: 'tau' } },
|
||||
];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(formatUtils.formatAmount(entry.amount, entry.coinInfo, entry.coinInfo.currencyUnits)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('formatTime', () => {
|
||||
const input = [0, 1, 2, 100, 999, 45];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(formatUtils.formatTime(entry)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('btckb2satoshib', () => {
|
||||
const input = [0, 1, 2, 100, 999];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(formatUtils.btckb2satoshib(entry)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('stringToHex', () => {
|
||||
const input = ['test', '0001', 'test99999'];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(formatUtils.stringToHex(entry)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('hexToString', () => {
|
||||
const input = ['0074006500730074', '0030003000300031', '007400650073007400390039003900390039'];
|
||||
|
||||
input.forEach((entry) => {
|
||||
expect(formatUtils.hexToString(entry)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in new issue