Productionize Transaction Stack (#456)

* export conditional input and hoc

* Move typings and fields out of send transaction

* Move fields into their own component for decoupled handling, use conditional inputs to simplify disabled components

* Handle hex and non hex strings automatically in BN conversion

* Fix handling of strings and numbers for BN

* add web3 fixes & comments

* Display short balances on deterministic modals

* add more tests, fix rounding

* Add spacer to balance sidebar network name

* Fix tsc error

* Add offline render CB

* Make more render callbacks

* Transform NonceField into its self contained component

* Remove styling from nonce field

* Better network handling in nonce cb

* Move network nonce initialization to componentDidMount

* Remove unessesary conditional input

* Make nonce component return a BN

* Simplify Query render cb

* Add gas query and token query render cbs

* Re-write address field component, strip out ENS name functionality for now

* Add address and data as unit types

* Cleanup Address Field component

* Export gas query

* Re-write gas field component

* Cleanup gas estimation check

* Re-write Data field

* Transaction field skeleton

* Export transaction field actions

* Rename fields to map to ethtx fields

* Make reducers for fields

* Fix reducer module exports

* Export reducer

* Formatting fix

* Type return of GasQuery

* Add transaction field getter / setter component

* Make transaction fields more flexible

* Formatting fix

* Split transaction fields component into two

* Remove erroneous prop

* Fix field naming to follow ethereum transaction fields

* Merge valid prop into componeent

* Change address field to be redux based

* Convert nonce field to redux based

* Make component for passing in current transaction

* Re-write Gas component to use redux state

* Reduxify data field component

* seperate transaction fields redux state into field data and meta data

* Rename SetTransactionFields to be singular

* Make render callback components for getting/setting meta fields

* Add non-zero option prop for token balance render cb

* re-write unit dropdown component to be redux based

* Make ether the first option

* Fix tsc error on tokenquery

* Handle query string default values in unit drop down

* Add thunks to package

* Add helper function for encoding transfer data

* Handle co-dependencies between fields via thunks, seperate value fields into ether and token based

* Fix wrong typing

* Add token metafield as export

* Start scaffolding out amount field component

* Make render cbs for conditional selection of value and balance

* Make render callbacks nullable

* Progress commit -- get dynaming swapping between tokens and ether working

* Get gas estimation working between ether and tokens

* Remove nonce from breaking gas estimation

* Add better validation for amount field

* Add 500ms debounce to gas saga

* Self contain custom message component

* Add web3 awareness to wallet render cb

* Add render cb for checking if  wallet is unlocked

* Cleanup inline typing

* export available params

* Add render cb to render component when a query string exists

* Add boolean callback param that check that the transaction is filled by user

* Remove uneeded typings from send transaction

* Fix misnomer

* Self contain generate transaction button

* Compartmentalize more send transaction components

* Add query string warning, custom message and generate tx button to fields

* Cleanup send tx component with new components

* export render callbacks

* saga transaction scaffolding

* make gas saga fully declarative

* transaction lib renaming

* Seperate gasprice into its own generator

* Make action creators for tx sign actions

* Clean up signing saga, introduce reducer for signing, make HW wallet libs compatible with new tx format, fix some typing with ethereumjs-tx

* Add TransactionComparison component

* Add pushTx

* Progress commit --  Streamline web3 and local signing / broadcasting flows. Need to still implement reducers for broadcasting and notifications

* Get local transaction broadcasting working

* re-write confirmation modal to be redux based

* Fix spacing and import

* Move confirmation modal to be attached to send button, create send button

* Properly handle broadcasting for conf modal

* Handle gas cost > balance for send everything

* Add signing status as its own component (#454)

* Fix ledger errors not showing on notifs

* Make dedicated actions for swapping from tokens to ether and ether to tokens

* Split actionTypes file

* Cleanup comments

* Cleanup comments

* Fix various tsc errors

* Lay down infrastructure for saving configurations per-wallet.

* Add pending and rejected states properly to token values.

* Add custom token form improvements.

* Fix metamask transaction errors

* Fix send entire balance estimation

* Fix add token form from never being enabled.

* Initial pass at account tab with send and view wallet tabs.

* Fix inactive tab.

* Hide private key, toggling

* Progress commit -- Replacing render callbacks with selectors, put  validation logic in sagas

* Moved the restore keystore functionality to view wallet info, and put it in a modal / util file.

* Fix navigation link active

* Force read only wallets to info tab.

* Remove commented code

* Saga-ify send everything

* Scan for new tokens, track saved tokens, only request tracked tokens on initial load.

* Add custom token to current wallets tracked tokens.

* Rework remove token icon.

* Adjust button margin

* Remove the rest of the needless render callbacks for selectors, sagaify nonce

* Bug fix send transaction

* remove unused redux-thunk

* Move fields to general components

* Clean up saga structure

* Refactor broadcast tx

* Implement better validation logic, get contract deploy working

* PR feedback.

* Convert tokenbalances component to connected redux component.

* Addressed feedback from Henry.

* Progress commit -- Implement Interact logic, needs manual testing

* Get rid of commented code

* move exports after declarations

* add tests, rough draft

* Get contract method calls working

* Bugfix contracts

* Cleanup hex prefixing

* Reset transaction state on wallet change

* Get rid of old send transaction component

* Disable sign transaction button when network request is underway

* Flatten send button tree, make nonce human readable in confirmation modal

* Add ghetto cost breakdown component, fix token field validation

* Create Generic SubTab and use in Send

* MVP of mnemonics with sub-tabs in Create Wallet view.

* Do dynamic revalidation

* move exports after declarations

* add forgotten signing tests

* update token spec

* update currentValue spec

* update validationHelpers spec

* Address TODO - use injected history to push navigation state instead of hardcoding window.location

* Use SubTabs in Contracts

* Fix revertPath prop for AcceptOrRedirectModal

* Use subtabs in SignAndVerifyMessage

* Routing for subtabs

* Fix routes, adjust sizing.

* Remove unused import

* Request nonce in base 10

* Add offline override to unit display

* Make cost breakdown less buggy

* Add non standard transaction warning

* Fix amount validity

* Cleanup datafield validity

* Display notif on gas estimation failure

* Add post-signing verification against fields, clean up gas price

* Fix tsc errors

* Code cleanup

* add exports to functions

* add specs for sendEverything and reset sagas

* delete duplicate files

* make tslint happy

* Merge develop

* Fix develop regressions

* Delegate nonce pulling  to wallet being set

* Clarify non standard transaction

* Make address a buffer to avoid leading 0's bug

* Clarify validation helper comment

* Increase debounce time, add console error

* Better validation for non-standard transactions

* Add verification skipping for broadcasting txs

* Fix state and wallet resetting for contract tabs

* Fix some spec files, remove contract.spec

* Remove broadcasting specs from wallet

* Close DeterministicWalletModal on confirm

* Revert "Close DeterministicWalletModal on confirm"

This reverts commit 16c860e854ca29e9de754164d8be5e24f722cbad.

* Reset hardware wallet state on unlocking. Dont render walletdecrypt content when its hidden.

* Fix client side broadcast checking

* Add more state resetters in error scenarios

* Fix gas estimation

* Add validation for value transactions to contract creation

* Add transaction comparaision differentiation depending on wallet type

* Fix token row display balance showing twice

* Properly handle failed transactions

* Handle bad error messages

* fix broken tests

* fix broken test

* Progress commit -- Implement generic subtab types

* Remove react router v3

* Remove unused routes

* Clean up Tabbing code, add onTabChange handler

* Fix tests

* Add nav fix

* revert opinionated sub-tab implementation

* additional reverts

* Add decimal validation

* Make gas price single source of truth, dont save any transaction state other than gas price

* Get rid of old wallet.spec reducer tests

* Add decimal validation when re-validating gasCost

* remove utilities view

* Remove cost breakdown

* Remove local gas estimation warning

* Create getShownTokenBalances selector; use in UnitDropDown and Equivalent Values

* Convert reducers to switch case

* Clean tsc errors

* Fix failing test

* fix tscheck error

* Add number validation to gas field

* Fix misaligned input dropdown

* Revert "Fix misaligned input dropdown"

This reverts commit a40a4c0e8d52471dea01e6727f741a737b798695.

* Set window timeout long enough for node switch to be persisted to state

* Transaction Refactor Style Fixes (#615)

* Fix unit dropdown alignment by rendering it in AmountField, and fixing a missed bootstrap case.

* Fix modal amount and gas text.

* Fix misaligned dropdown

* Update conditions for NavLink is-active class
This commit is contained in:
HenryNguyen5 2017-12-18 16:23:31 -05:00 committed by Daniel Ternyak
parent d98389b201
commit 08d4ccbdae
319 changed files with 9232 additions and 4439 deletions

View File

@ -8,10 +8,8 @@ import GenerateWallet from 'containers/Tabs/GenerateWallet';
import Help from 'containers/Tabs/Help';
import SendTransaction from 'containers/Tabs/SendTransaction';
import Swap from 'containers/Tabs/Swap';
import ViewWallet from 'containers/Tabs/ViewWallet';
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
import BroadcastTx from 'containers/Tabs/BroadcastTx';
import RestoreKeystore from 'containers/Tabs/RestoreKeystore';
import ErrorScreen from 'components/ErrorScreen';
// TODO: fix this
@ -47,13 +45,15 @@ export default class Root extends Component<Props, State> {
<Router history={history} key={Math.random()}>
<div>
<Route exact={true} path="/" component={GenerateWallet} />
<Route path="/view-wallet" component={ViewWallet} />
<Route path="/help" component={Help} />
<Route path="/swap" component={Swap} />
<Route path="/account" component={SendTransaction}>
<Route path="send" component={SendTransaction} />
<Route path="info" component={SendTransaction} />
</Route>
<Route path="/send-transaction" component={SendTransaction} />
<Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} />
<Route path="/utilities" component={RestoreKeystore} />
<Route path="/sign-and-verify-message" component={SignAndVerifyMessage} />
<Route path="/pushTx" component={BroadcastTx} />
<LegacyRoutes />
@ -87,7 +87,7 @@ const LegacyRoutes = withRouter(props => {
history.push('/ens');
break;
case '#view-wallet-info':
history.push('/view-wallet');
history.push('/account/info');
break;
case '#check-tx-status':
history.push('/check-tx-status');
@ -99,6 +99,7 @@ const LegacyRoutes = withRouter(props => {
<Switch>
<Redirect from="/signmsg.html" to="/sign-and-verify-message" />
<Redirect from="/helpers.html" to="/helpers" />
<Redirect from="/send-transaction" to="/account/send" />
</Switch>
);
});

View File

@ -25,24 +25,13 @@ export function changeLanguage(sign: string): interfaces.ChangeLanguageAction {
}
export type TChangeNode = typeof changeNode;
export function changeNode(
nodeSelection: string,
node: NodeConfig
): interfaces.ChangeNodeAction {
export function changeNode(nodeSelection: string, node: NodeConfig): interfaces.ChangeNodeAction {
return {
type: TypeKeys.CONFIG_NODE_CHANGE,
payload: { nodeSelection, node }
};
}
export type TChangeGasPrice = typeof changeGasPrice;
export function changeGasPrice(value: number): interfaces.ChangeGasPriceAction {
return {
type: TypeKeys.CONFIG_GAS_PRICE,
payload: value
};
}
export type TPollOfflineStatus = typeof pollOfflineStatus;
export function pollOfflineStatus(): interfaces.PollOfflineStatus {
return {
@ -51,9 +40,7 @@ export function pollOfflineStatus(): interfaces.PollOfflineStatus {
}
export type TChangeNodeIntent = typeof changeNodeIntent;
export function changeNodeIntent(
payload: string
): interfaces.ChangeNodeIntentAction {
export function changeNodeIntent(payload: string): interfaces.ChangeNodeIntentAction {
return {
type: TypeKeys.CONFIG_NODE_CHANGE_INTENT,
payload
@ -61,9 +48,7 @@ export function changeNodeIntent(
}
export type TAddCustomNode = typeof addCustomNode;
export function addCustomNode(
payload: CustomNodeConfig
): interfaces.AddCustomNodeAction {
export function addCustomNode(payload: CustomNodeConfig): interfaces.AddCustomNodeAction {
return {
type: TypeKeys.CONFIG_ADD_CUSTOM_NODE,
payload
@ -71,9 +56,7 @@ export function addCustomNode(
}
export type TRemoveCustomNode = typeof removeCustomNode;
export function removeCustomNode(
payload: CustomNodeConfig
): interfaces.RemoveCustomNodeAction {
export function removeCustomNode(payload: CustomNodeConfig): interfaces.RemoveCustomNodeAction {
return {
type: TypeKeys.CONFIG_REMOVE_CUSTOM_NODE,
payload
@ -81,9 +64,7 @@ export function removeCustomNode(
}
export type TAddCustomNetwork = typeof addCustomNetwork;
export function addCustomNetwork(
payload: CustomNetworkConfig
): interfaces.AddCustomNetworkAction {
export function addCustomNetwork(payload: CustomNetworkConfig): interfaces.AddCustomNetworkAction {
return {
type: TypeKeys.CONFIG_ADD_CUSTOM_NETWORK,
payload
@ -101,9 +82,7 @@ export function removeCustomNetwork(
}
export type TSetLatestBlock = typeof setLatestBlock;
export function setLatestBlock(
payload: string
): interfaces.SetLatestBlockAction {
export function setLatestBlock(payload: string): interfaces.SetLatestBlockAction {
return {
type: TypeKeys.CONFIG_SET_LATEST_BLOCK,
payload

View File

@ -27,12 +27,6 @@ export interface ChangeNodeAction {
};
}
/*** Change gas price ***/
export interface ChangeGasPriceAction {
type: TypeKeys.CONFIG_GAS_PRICE;
payload: number;
}
/*** Poll offline status ***/
export interface PollOfflineStatus {
type: TypeKeys.CONFIG_POLL_OFFLINE_STATUS;
@ -83,7 +77,6 @@ export interface Web3UnsetNodeAction {
export type ConfigAction =
| ChangeNodeAction
| ChangeLanguageAction
| ChangeGasPriceAction
| ToggleOfflineAction
| PollOfflineStatus
| ForceOfflineAction

View File

@ -2,7 +2,6 @@ export enum TypeKeys {
CONFIG_LANGUAGE_CHANGE = 'CONFIG_LANGUAGE_CHANGE',
CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE',
CONFIG_NODE_CHANGE_INTENT = 'CONFIG_NODE_CHANGE_INTENT',
CONFIG_GAS_PRICE = 'CONFIG_GAS_PRICE',
CONFIG_TOGGLE_OFFLINE = 'CONFIG_TOGGLE_OFFLINE',
CONFIG_FORCE_OFFLINE = 'CONFIG_FORCE_OFFLINE',
CONFIG_POLL_OFFLINE_STATUS = 'CONFIG_POLL_OFFLINE_STATUS',

View File

@ -1,2 +1,3 @@
export * from './actionCreators';
export * from './actionTypes';
export * from './constants';

View File

@ -1,2 +1,3 @@
export * from './actionCreators';
export * from './actionTypes';
export * from './constants';

View File

@ -0,0 +1,55 @@
import {
BroadcastLocalTransactionRequestedAction,
BroadcastWeb3TransactionRequestedAction,
BroadcastTransactionFailedAction,
BroadcastTransactionSucceededAction,
BroadcastTransactionQueuedAction
} from '../actionTypes';
import { TypeKeys } from '../constants';
type TBroadcastLocalTransactionRequested = typeof broadcastLocalTransactionRequested;
const broadcastLocalTransactionRequested = (): BroadcastLocalTransactionRequestedAction => ({
type: TypeKeys.BROADCAST_LOCAL_TRANSACTION_REQUESTED
});
type TBroadcastWeb3TransactionRequested = typeof broadcastWeb3TransactionRequested;
const broadcastWeb3TransactionRequested = (): BroadcastWeb3TransactionRequestedAction => ({
type: TypeKeys.BROADCAST_WEB3_TRANSACTION_REQUESTED
});
type TBroadcastTransactionSucceeded = typeof broadcastTransactionSucceeded;
const broadcastTransactionSucceeded = (
payload: BroadcastTransactionSucceededAction['payload']
): BroadcastTransactionSucceededAction => ({
type: TypeKeys.BROADCAST_TRANSACTION_SUCCEEDED,
payload
});
type TBroadcastTransactionFailed = typeof broadcastTransactionFailed;
const broadcastTransactionFailed = (
payload: BroadcastTransactionFailedAction['payload']
): BroadcastTransactionFailedAction => ({
type: TypeKeys.BROADCAST_TRASACTION_FAILED,
payload
});
type TBroadcastTransactionQueued = typeof broadcastTransactionQueued;
const broadcastTransactionQueued = (
payload: BroadcastTransactionQueuedAction['payload']
): BroadcastTransactionQueuedAction => ({
type: TypeKeys.BROADCAST_TRANSACTION_QUEUED,
payload
});
export {
broadcastLocalTransactionRequested,
broadcastWeb3TransactionRequested,
broadcastTransactionSucceeded,
broadcastTransactionFailed,
broadcastTransactionQueued,
TBroadcastLocalTransactionRequested,
TBroadcastWeb3TransactionRequested,
TBroadcastTransactionSucceeded,
TBroadcastTransactionFailed,
TBroadcastTransactionQueued
};

View File

@ -0,0 +1,16 @@
import { SetCurrentToAction, SetCurrentValueAction } from '../actionTypes/current';
import { TypeKeys } from '../';
type TSetCurrentValue = typeof setCurrentValue;
const setCurrentValue = (payload: SetCurrentValueAction['payload']): SetCurrentValueAction => ({
type: TypeKeys.CURRENT_VALUE_SET,
payload
});
type TSetCurrentTo = typeof setCurrentTo;
const setCurrentTo = (payload: SetCurrentToAction['payload']): SetCurrentToAction => ({
type: TypeKeys.CURRENT_TO_SET,
payload
});
export { setCurrentValue, setCurrentTo, TSetCurrentTo, TSetCurrentValue };

View File

@ -0,0 +1,93 @@
import {
SetGasLimitFieldAction,
SetDataFieldAction,
SetToFieldAction,
SetNonceFieldAction,
SetValueFieldAction,
InputGasLimitAction,
InputDataAction,
InputNonceAction,
ResetAction,
SetGasPriceFieldAction
} from '../actionTypes';
import { TypeKeys } from 'actions/transaction/constants';
type TInputGasLimit = typeof inputGasLimit;
const inputGasLimit = (payload: InputGasLimitAction['payload']) => ({
type: TypeKeys.GAS_LIMIT_INPUT,
payload
});
type TInputNonce = typeof inputNonce;
const inputNonce = (payload: InputNonceAction['payload']) => ({
type: TypeKeys.NONCE_INPUT,
payload
});
type TInputData = typeof inputData;
const inputData = (payload: InputDataAction['payload']) => ({
type: TypeKeys.DATA_FIELD_INPUT,
payload
});
type TSetGasLimitField = typeof setGasLimitField;
const setGasLimitField = (payload: SetGasLimitFieldAction['payload']): SetGasLimitFieldAction => ({
type: TypeKeys.GAS_LIMIT_FIELD_SET,
payload
});
type TSetDataField = typeof setDataField;
const setDataField = (payload: SetDataFieldAction['payload']): SetDataFieldAction => ({
type: TypeKeys.DATA_FIELD_SET,
payload
});
type TSetToField = typeof setToField;
const setToField = (payload: SetToFieldAction['payload']): SetToFieldAction => ({
type: TypeKeys.TO_FIELD_SET,
payload
});
type TSetNonceField = typeof setNonceField;
const setNonceField = (payload: SetNonceFieldAction['payload']): SetNonceFieldAction => ({
type: TypeKeys.NONCE_FIELD_SET,
payload
});
type TSetValueField = typeof setValueField;
const setValueField = (payload: SetValueFieldAction['payload']): SetValueFieldAction => ({
type: TypeKeys.VALUE_FIELD_SET,
payload
});
type TSetGasPriceField = typeof setGasPriceField;
const setGasPriceField = (payload: SetGasPriceFieldAction['payload']): SetGasPriceFieldAction => ({
type: TypeKeys.GAS_PRICE_FIELD_SET,
payload
});
type TReset = typeof reset;
const reset = (): ResetAction => ({ type: TypeKeys.RESET });
export {
TInputGasLimit,
TInputNonce,
TInputData,
TSetGasLimitField,
TSetDataField,
TSetToField,
TSetNonceField,
TSetValueField,
TSetGasPriceField,
TReset,
inputGasLimit,
inputNonce,
inputData,
setGasLimitField,
setDataField,
setToField,
setNonceField,
setValueField,
setGasPriceField,
reset
};

View File

@ -0,0 +1,7 @@
export * from './fields';
export * from './meta';
export * from './network';
export * from './sign';
export * from './broadcast';
export * from './current';
export * from './sendEverything';

View File

@ -0,0 +1,27 @@
import {
TypeKeys,
SetUnitMetaAction,
SetTokenValueMetaAction,
SetTokenToMetaAction
} from 'actions/transaction';
type TSetTokenBalance = typeof setTokenValue;
type TSetUnitMeta = typeof setUnitMeta;
type TSetTokenTo = typeof setTokenTo;
const setTokenTo = (payload: SetTokenToMetaAction['payload']): SetTokenToMetaAction => ({
type: TypeKeys.TOKEN_TO_META_SET,
payload
});
const setTokenValue = (payload: SetTokenValueMetaAction['payload']): SetTokenValueMetaAction => ({
type: TypeKeys.TOKEN_VALUE_META_SET,
payload
});
const setUnitMeta = (payload: SetUnitMetaAction['payload']): SetUnitMetaAction => ({
type: TypeKeys.UNIT_META_SET,
payload
});
export { TSetUnitMeta, TSetTokenBalance, TSetTokenTo, setUnitMeta, setTokenValue, setTokenTo };

View File

@ -0,0 +1,82 @@
import {
EstimateGasFailedAction,
EstimateGasRequestedAction,
TypeKeys,
EstimateGasSucceededAction,
GetFromRequestedAction,
GetFromSucceededAction,
GetFromFailedAction,
GetNonceRequestedAction,
GetNonceSucceededAction,
GetNonceFailedAction
} from 'actions/transaction';
type TEstimateGasRequested = typeof estimateGasRequested;
const estimateGasRequested = (
payload: EstimateGasRequestedAction['payload']
): EstimateGasRequestedAction => ({
type: TypeKeys.ESTIMATE_GAS_REQUESTED,
payload
});
type TEstimateGasSucceeded = typeof estimateGasSucceeded;
const estimateGasSucceeded = (): EstimateGasSucceededAction => ({
type: TypeKeys.ESTIMATE_GAS_SUCCEEDED
});
type TEstimateGasFailed = typeof estimateGasFailed;
const estimateGasFailed = (): EstimateGasFailedAction => ({
type: TypeKeys.ESTIMATE_GAS_FAILED
});
type TGetFromRequested = typeof getFromRequested;
const getFromRequested = (): GetFromRequestedAction => ({
type: TypeKeys.GET_FROM_REQUESTED
});
type TGetFromSucceeded = typeof getFromSucceeded;
const getFromSucceeded = (payload: GetFromSucceededAction['payload']): GetFromSucceededAction => ({
type: TypeKeys.GET_FROM_SUCCEEDED,
payload
});
type TGetFromFailed = typeof getFromFailed;
const getFromFailed = (): GetFromFailedAction => ({
type: TypeKeys.GET_FROM_FAILED
});
type TGetNonceRequested = typeof getNonceRequested;
const getNonceRequested = (): GetNonceRequestedAction => ({
type: TypeKeys.GET_NONCE_REQUESTED
});
type TGetNonceSucceeded = typeof getNonceSucceeded;
const getNonceSucceeded = (
payload: GetNonceSucceededAction['payload']
): GetNonceSucceededAction => ({ type: TypeKeys.GET_NONCE_SUCCEEDED, payload });
type TGetNonceFailed = typeof getNonceFailed;
const getNonceFailed = (): GetNonceFailedAction => ({
type: TypeKeys.GET_NONCE_FAILED
});
export {
estimateGasRequested,
estimateGasFailed,
estimateGasSucceeded,
getFromRequested,
getFromSucceeded,
getFromFailed,
getNonceRequested,
getNonceFailed,
getNonceSucceeded,
TEstimateGasRequested,
TEstimateGasFailed,
TEstimateGasSucceeded,
TGetFromRequested,
TGetFromSucceeded,
TGetNonceRequested,
TGetNonceSucceeded,
TGetNonceFailed,
TGetFromFailed
};

View File

@ -0,0 +1,30 @@
import {
SendEverythingFailedAction,
SendEverythingRequestedAction,
SendEverythingSucceededAction
} from '../actionTypes/sendEverything';
import { TypeKeys } from 'actions/transaction';
type TSendEverythingRequested = typeof sendEverythingRequested;
const sendEverythingRequested = (): SendEverythingRequestedAction => ({
type: TypeKeys.SEND_EVERYTHING_REQUESTED
});
type TSendEverythingFailed = typeof sendEverythingFailed;
const sendEverythingFailed = (): SendEverythingFailedAction => ({
type: TypeKeys.SEND_EVERYTHING_FAILED
});
type TSendEverythingSucceeded = typeof sendEverythingSucceeded;
const sendEverythingSucceeded = (): SendEverythingSucceededAction => ({
type: TypeKeys.SEND_EVERYTHING_SUCCEEDED
});
export {
TSendEverythingRequested,
TSendEverythingFailed,
TSendEverythingSucceeded,
sendEverythingRequested,
sendEverythingFailed,
sendEverythingSucceeded
};

View File

@ -0,0 +1,58 @@
import {
SignTransactionFailedAction,
SignLocalTransactionRequestedAction,
SignWeb3TransactionRequestedAction,
SignLocalTransactionSucceededAction,
SignWeb3TransactionSucceededAction
} from '../actionTypes';
import { TypeKeys } from '../constants';
type TSignTransactionFailed = typeof signTransactionFailed;
const signTransactionFailed = (): SignTransactionFailedAction => ({
type: TypeKeys.SIGN_TRANSACTION_FAILED
});
type TSignLocalTransactionSucceeded = typeof signLocalTransactionSucceeded;
const signLocalTransactionSucceeded = (
payload: SignLocalTransactionSucceededAction['payload']
): SignLocalTransactionSucceededAction => ({
type: TypeKeys.SIGN_LOCAL_TRANSACTION_SUCCEEDED,
payload
});
type TSignLocalTransactionRequested = typeof signLocalTransactionRequested;
const signLocalTransactionRequested = (
payload: SignLocalTransactionRequestedAction['payload']
): SignLocalTransactionRequestedAction => ({
type: TypeKeys.SIGN_LOCAL_TRANSACTION_REQUESTED,
payload
});
type TSignWeb3TransactionSucceeded = typeof signWeb3TransactionSucceeded;
const signWeb3TransactionSucceeded = (
payload: SignWeb3TransactionSucceededAction['payload']
): SignWeb3TransactionSucceededAction => ({
type: TypeKeys.SIGN_WEB3_TRANSACTION_SUCCEEDED,
payload
});
type TSignWeb3TransactionRequested = typeof signWeb3TransactionRequested;
const signWeb3TransactionRequested = (
payload: SignWeb3TransactionRequestedAction['payload']
): SignWeb3TransactionRequestedAction => ({
type: TypeKeys.SIGN_WEB3_TRANSACTION_REQUESTED,
payload
});
export {
signTransactionFailed,
signLocalTransactionSucceeded,
signLocalTransactionRequested,
signWeb3TransactionSucceeded,
signWeb3TransactionRequested,
TSignLocalTransactionSucceeded,
TSignLocalTransactionRequested,
TSignWeb3TransactionSucceeded,
TSignWeb3TransactionRequested,
TSignTransactionFailed
};

View File

@ -0,0 +1,33 @@
import {
SwapEtherToTokenAction,
SwapTokenToEtherAction,
SwapTokenToTokenAction
} from '../actionTypes';
import { TypeKeys } from '../constants';
type TSwapTokenToEther = typeof swapTokenToEther;
const swapTokenToEther = (payload: SwapTokenToEtherAction['payload']): SwapTokenToEtherAction => ({
type: TypeKeys.TOKEN_TO_ETHER_SWAP,
payload
});
type TSwapEtherToToken = typeof swapEtherToToken;
const swapEtherToToken = (payload: SwapEtherToTokenAction['payload']): SwapEtherToTokenAction => ({
payload,
type: TypeKeys.ETHER_TO_TOKEN_SWAP
});
type TSwapTokenToToken = typeof swapTokenToToken;
const swapTokenToToken = (payload: SwapTokenToTokenAction['payload']): SwapTokenToTokenAction => ({
payload,
type: TypeKeys.TOKEN_TO_TOKEN_SWAP
});
export {
swapEtherToToken,
swapTokenToEther,
swapTokenToToken,
TSwapTokenToEther,
TSwapEtherToToken,
TSwapTokenToToken
};

View File

@ -0,0 +1,34 @@
import { TypeKeys } from '../constants';
import { BroadcastAction } from './broadcast';
import { FieldAction, InputFieldAction } from './fields';
import { MetaAction } from './meta';
import { NetworkAction } from './network';
import { SignAction } from './sign';
import { SwapAction } from './swap';
import { CurrentAction } from './current';
import { SendEverythingAction } from './sendEverything';
export * from './broadcast';
export * from './fields';
export * from './meta';
export * from './network';
export * from './sign';
export * from './swap';
export * from './current';
export * from './sendEverything';
export interface ResetAction {
type: TypeKeys.RESET;
}
export type TransactionAction =
| InputFieldAction
| BroadcastAction
| FieldAction
| MetaAction
| NetworkAction
| SignAction
| SwapAction
| ResetAction
| CurrentAction
| SendEverythingAction;

View File

@ -0,0 +1,35 @@
import { TypeKeys } from 'actions/transaction';
/* Broadcasting actions */
interface BroadcastLocalTransactionRequestedAction {
type: TypeKeys.BROADCAST_LOCAL_TRANSACTION_REQUESTED;
}
interface BroadcastWeb3TransactionRequestedAction {
type: TypeKeys.BROADCAST_WEB3_TRANSACTION_REQUESTED;
}
interface BroadcastTransactionSucceededAction {
type: TypeKeys.BROADCAST_TRANSACTION_SUCCEEDED;
payload: { indexingHash: string; broadcastedHash: string };
}
interface BroadcastTransactionQueuedAction {
type: TypeKeys.BROADCAST_TRANSACTION_QUEUED;
payload: { indexingHash: string; serializedTransaction: Buffer };
}
interface BroadcastTransactionFailedAction {
type: TypeKeys.BROADCAST_TRASACTION_FAILED;
payload: { indexingHash: string };
}
type BroadcastAction =
| BroadcastLocalTransactionRequestedAction
| BroadcastTransactionSucceededAction
| BroadcastWeb3TransactionRequestedAction
| BroadcastTransactionQueuedAction
| BroadcastTransactionFailedAction;
export {
BroadcastLocalTransactionRequestedAction,
BroadcastTransactionSucceededAction,
BroadcastWeb3TransactionRequestedAction,
BroadcastTransactionQueuedAction,
BroadcastTransactionFailedAction,
BroadcastAction
};

View File

@ -0,0 +1,17 @@
import { TypeKeys } from '../constants';
/* user input */
interface SetCurrentValueAction {
type: TypeKeys.CURRENT_VALUE_SET;
payload: string;
}
interface SetCurrentToAction {
type: TypeKeys.CURRENT_TO_SET;
payload: string;
}
type CurrentAction = SetCurrentValueAction | SetCurrentToAction;
export { SetCurrentValueAction, SetCurrentToAction, CurrentAction };

View File

@ -0,0 +1,92 @@
import { TypeKeys } from 'actions/transaction/constants';
import { Wei, Data, Address, Nonce } from 'libs/units';
/* User Input */
interface InputGasLimitAction {
type: TypeKeys.GAS_LIMIT_INPUT;
payload: string;
}
interface InputDataAction {
type: TypeKeys.DATA_FIELD_INPUT;
payload: string;
}
interface InputNonceAction {
type: TypeKeys.NONCE_INPUT;
payload: string;
}
/*Field Actions -- Reducer input*/
// We can compute field validity by checking if the value is null
interface SetGasLimitFieldAction {
type: TypeKeys.GAS_LIMIT_FIELD_SET;
payload: {
raw: string;
value: Wei | null;
};
}
interface SetGasPriceFieldAction {
type: TypeKeys.GAS_PRICE_FIELD_SET;
payload: {
raw: string;
value: Wei | null;
};
}
interface SetDataFieldAction {
type: TypeKeys.DATA_FIELD_SET;
payload: {
raw: string;
value: Data | null;
};
}
interface SetToFieldAction {
type: TypeKeys.TO_FIELD_SET;
payload: {
raw: string;
value: Address | null;
};
}
interface SetNonceFieldAction {
type: TypeKeys.NONCE_FIELD_SET;
payload: {
raw: string;
value: Nonce | null;
};
}
interface SetValueFieldAction {
type: TypeKeys.VALUE_FIELD_SET;
payload: {
raw: string;
value: Wei | null;
};
}
type InputFieldAction = InputNonceAction | InputGasLimitAction | InputDataAction;
type FieldAction =
| SetGasLimitFieldAction
| SetDataFieldAction
| SetToFieldAction
| SetNonceFieldAction
| SetValueFieldAction
| SetGasPriceFieldAction;
export {
InputGasLimitAction,
InputDataAction,
InputNonceAction,
SetGasLimitFieldAction,
SetDataFieldAction,
SetToFieldAction,
SetNonceFieldAction,
SetValueFieldAction,
FieldAction,
InputFieldAction,
SetGasPriceFieldAction
};

View File

@ -0,0 +1 @@
export * from './actionTypes';

View File

@ -0,0 +1,28 @@
import { TypeKeys } from 'actions/transaction/constants';
import { Address, TokenValue } from 'libs/units';
/*Meta Actions*/
interface SetTokenToMetaAction {
type: TypeKeys.TOKEN_TO_META_SET;
payload: {
raw: string;
value: Address | null;
};
}
interface SetUnitMetaAction {
type: TypeKeys.UNIT_META_SET;
payload: string;
}
interface SetTokenValueMetaAction {
type: TypeKeys.TOKEN_VALUE_META_SET;
payload: {
raw: string;
value: TokenValue | null;
};
}
type MetaAction = SetUnitMetaAction | SetTokenValueMetaAction | SetTokenToMetaAction;
export { MetaAction, SetUnitMetaAction, SetTokenToMetaAction, SetTokenValueMetaAction };

View File

@ -0,0 +1,57 @@
import { TypeKeys } from 'actions/transaction/constants';
import { IHexStrTransaction } from 'libs/transaction';
/* Network actions */
interface EstimateGasRequestedAction {
type: TypeKeys.ESTIMATE_GAS_REQUESTED;
payload: Partial<IHexStrTransaction>;
}
interface EstimateGasSucceededAction {
type: TypeKeys.ESTIMATE_GAS_SUCCEEDED;
}
interface EstimateGasFailedAction {
type: TypeKeys.ESTIMATE_GAS_FAILED;
}
interface GetFromRequestedAction {
type: TypeKeys.GET_FROM_REQUESTED;
}
interface GetFromSucceededAction {
type: TypeKeys.GET_FROM_SUCCEEDED;
payload: string;
}
interface GetFromFailedAction {
type: TypeKeys.GET_FROM_FAILED;
}
interface GetNonceRequestedAction {
type: TypeKeys.GET_NONCE_REQUESTED;
}
interface GetNonceSucceededAction {
type: TypeKeys.GET_NONCE_SUCCEEDED;
payload: string;
}
interface GetNonceFailedAction {
type: TypeKeys.GET_NONCE_FAILED;
}
type NetworkAction =
| EstimateGasFailedAction
| EstimateGasRequestedAction
| EstimateGasSucceededAction
| GetFromRequestedAction
| GetFromSucceededAction
| GetFromFailedAction
| GetNonceRequestedAction
| GetNonceSucceededAction
| GetNonceFailedAction;
export {
EstimateGasRequestedAction,
EstimateGasSucceededAction,
EstimateGasFailedAction,
GetFromRequestedAction,
GetFromSucceededAction,
GetFromFailedAction,
GetNonceRequestedAction,
GetNonceSucceededAction,
GetNonceFailedAction,
NetworkAction
};

View File

@ -0,0 +1,23 @@
import { TypeKeys } from 'actions/transaction';
interface SendEverythingRequestedAction {
type: TypeKeys.SEND_EVERYTHING_REQUESTED;
}
interface SendEverythingSucceededAction {
type: TypeKeys.SEND_EVERYTHING_SUCCEEDED;
}
interface SendEverythingFailedAction {
type: TypeKeys.SEND_EVERYTHING_FAILED;
}
type SendEverythingAction =
| SendEverythingRequestedAction
| SendEverythingSucceededAction
| SendEverythingFailedAction;
export {
SendEverythingAction,
SendEverythingSucceededAction,
SendEverythingFailedAction,
SendEverythingRequestedAction
};

View File

@ -0,0 +1,46 @@
import EthTx from 'ethereumjs-tx';
import { TypeKeys } from 'actions/transaction/constants';
/*
* Difference between the web3/local is that a local sign will actually sign the tx
* While a web3 sign just gathers the rest of the nessesary parameters of the ethereum tx
* to do the sign + broadcast in 1 step later on
*/
/* Signing / Async actions */
interface SignLocalTransactionRequestedAction {
type: TypeKeys.SIGN_LOCAL_TRANSACTION_REQUESTED;
payload: EthTx;
}
interface SignLocalTransactionSucceededAction {
type: TypeKeys.SIGN_LOCAL_TRANSACTION_SUCCEEDED;
payload: { signedTransaction: Buffer; indexingHash: string; noVerify?: boolean }; // dont verify against fields, for pushTx
}
interface SignWeb3TransactionRequestedAction {
type: TypeKeys.SIGN_WEB3_TRANSACTION_REQUESTED;
payload: EthTx;
}
interface SignWeb3TransactionSucceededAction {
type: TypeKeys.SIGN_WEB3_TRANSACTION_SUCCEEDED;
payload: { transaction: Buffer; indexingHash: string; noVerify?: boolean };
}
interface SignTransactionFailedAction {
type: TypeKeys.SIGN_TRANSACTION_FAILED;
}
type SignAction =
| SignLocalTransactionRequestedAction
| SignLocalTransactionSucceededAction
| SignWeb3TransactionRequestedAction
| SignWeb3TransactionSucceededAction
| SignTransactionFailedAction;
export {
SignLocalTransactionRequestedAction,
SignLocalTransactionSucceededAction,
SignWeb3TransactionRequestedAction,
SignWeb3TransactionSucceededAction,
SignTransactionFailedAction,
SignAction
};

View File

@ -0,0 +1,40 @@
import { TypeKeys } from 'actions/transaction/constants';
import {
SetToFieldAction,
SetValueFieldAction,
SetTokenToMetaAction,
SetTokenValueMetaAction,
SetDataFieldAction
} from 'actions/transaction';
/* Swapping actions */
interface SwapTokenToEtherAction {
type: TypeKeys.TOKEN_TO_ETHER_SWAP;
payload: {
to: SetToFieldAction['payload'];
value: SetValueFieldAction['payload'];
decimal: number;
};
}
interface SwapEtherToTokenAction {
type: TypeKeys.ETHER_TO_TOKEN_SWAP;
payload: {
to: SetToFieldAction['payload'];
data: SetDataFieldAction['payload'];
tokenTo: SetTokenToMetaAction['payload'];
tokenValue: SetTokenValueMetaAction['payload'];
decimal: number;
};
}
interface SwapTokenToTokenAction {
type: TypeKeys.TOKEN_TO_TOKEN_SWAP;
payload: {
to: SetToFieldAction['payload'];
data: SetDataFieldAction['payload'];
tokenValue: SetTokenValueMetaAction['payload'];
decimal: number;
};
}
type SwapAction = SwapEtherToTokenAction | SwapTokenToEtherAction | SwapTokenToTokenAction;
export { SwapTokenToEtherAction, SwapEtherToTokenAction, SwapAction, SwapTokenToTokenAction };

View File

@ -0,0 +1,53 @@
export enum TypeKeys {
ESTIMATE_GAS_REQUESTED = 'ESTIMATE_GAS_REQUESTED',
ESTIMATE_GAS_SUCCEEDED = 'ESTIMATE_GAS_SUCCEEDED',
ESTIMATE_GAS_FAILED = 'ESTIMATE_GAS_FAILED',
GET_FROM_REQUESTED = 'GET_FROM_REQUESTED',
GET_FROM_SUCCEEDED = 'GET_FROM_SUCCEEDED',
GET_FROM_FAILED = 'GET_FROM_FAILED',
GET_NONCE_REQUESTED = 'GET_NONCE_REQUESTED',
GET_NONCE_SUCCEEDED = 'GET_NONCE_SUCCEEDED',
GET_NONCE_FAILED = 'GET_NONCE_FAILED',
SIGN_WEB3_TRANSACTION_REQUESTED = 'SIGN_WEB3_TRANSACTION_REQUESTED',
SIGN_WEB3_TRANSACTION_SUCCEEDED = 'SIGN_WEB3_TRANSACTION_SUCCEEDED',
SIGN_LOCAL_TRANSACTION_REQUESTED = 'SIGN_LOCAL_TRANSACTION_REQUESTED',
SIGN_LOCAL_TRANSACTION_SUCCEEDED = 'SIGN_LOCAL_TRANSACTION_SUCCEEDED',
SIGN_TRANSACTION_FAILED = 'SIGN_TRANSACTION_FAILED',
BROADCAST_WEB3_TRANSACTION_REQUESTED = 'BROADCAST_WEB3_TRANSACTION_REQUESTED',
BROADCAST_TRANSACTION_SUCCEEDED = 'BROADCAST_TRANSACTION_SUCCEEDED',
BROADCAST_LOCAL_TRANSACTION_REQUESTED = 'BROADCAST_LOCAL_TRANSACTION_REQUESTED',
BROADCAST_TRANSACTION_QUEUED = 'BROADCAST_TRANSACTION_QUEUED',
BROADCAST_TRASACTION_FAILED = 'BROADCAST_TRASACTION_FAILED',
CURRENT_VALUE_SET = 'CURRENT_VALUE_SET',
CURRENT_TO_SET = 'CURRENT_TO_SET',
DATA_FIELD_INPUT = 'DATA_FIELD_INPUT',
GAS_LIMIT_INPUT = 'GAS_LIMIT_INPUT',
NONCE_INPUT = 'NONCE_INPUT',
DATA_FIELD_SET = 'DATA_FIELD_SET',
GAS_LIMIT_FIELD_SET = 'GAS_LIMIT_FIELD_SET',
TO_FIELD_SET = 'TO_FIELD_SET',
VALUE_FIELD_SET = 'VALUE_FIELD_SET',
NONCE_FIELD_SET = 'NONCE_FIELD_SET',
GAS_PRICE_FIELD_SET = 'GAS_PRICE_FIELD_SET',
TOKEN_TO_META_SET = 'TOKEN_TO_META_SET',
UNIT_META_SET = 'UNIT_META_SET',
TOKEN_VALUE_META_SET = 'TOKEN_VALUE_META_SET',
TOKEN_TO_ETHER_SWAP = 'TOKEN_TO_ETHER_SWAP',
ETHER_TO_TOKEN_SWAP = 'ETHER_TO_TOKEN_SWAP',
TOKEN_TO_TOKEN_SWAP = 'TOKEN_TO_TOKEN_SWAP',
SEND_EVERYTHING_REQUESTED = 'SEND_EVERYTHING_REQUESTED',
SEND_EVERYTHING_SUCCEEDED = 'SEND_EVERYTHING_SUCCEEDED',
SEND_EVERYTHING_FAILED = 'SEND_EVERYTHING_FAILED',
RESET = 'RESET'
}

View File

@ -0,0 +1,3 @@
export * from './actionCreators';
export * from './constants';
export * from './actionTypes';

View File

@ -1,5 +1,5 @@
import { Wei, TokenValue } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet';
import { IWallet, WalletConfig } from 'libs/wallet';
import * as types from './actionTypes';
import { TypeKeys } from './constants';
export type TUnlockPrivateKey = typeof unlockPrivateKey;
@ -13,9 +13,7 @@ export function unlockPrivateKey(
}
export type TUnlockKeystore = typeof unlockKeystore;
export function unlockKeystore(
value: types.KeystoreUnlockParams
): types.UnlockKeystoreAction {
export function unlockKeystore(value: types.KeystoreUnlockParams): types.UnlockKeystoreAction {
return {
type: TypeKeys.WALLET_UNLOCK_KEYSTORE,
payload: value
@ -23,9 +21,7 @@ export function unlockKeystore(
}
export type TUnlockMnemonic = typeof unlockMnemonic;
export function unlockMnemonic(
value: types.MnemonicUnlockParams
): types.UnlockMnemonicAction {
export function unlockMnemonic(value: types.MnemonicUnlockParams): types.UnlockMnemonicAction {
return {
type: TypeKeys.WALLET_UNLOCK_MNEMONIC,
payload: value
@ -54,9 +50,7 @@ export function setBalancePending(): types.SetBalancePendingAction {
}
export type TSetBalance = typeof setBalanceFullfilled;
export function setBalanceFullfilled(
value: Wei
): types.SetBalanceFullfilledAction {
export function setBalanceFullfilled(value: Wei): types.SetBalanceFullfilledAction {
return {
type: TypeKeys.WALLET_SET_BALANCE_FULFILLED,
payload: value
@ -69,56 +63,44 @@ export function setBalanceRejected(): types.SetBalanceRejectedAction {
};
}
export type TSetTokenBalances = typeof setTokenBalances;
export function setTokenBalances(payload: {
export function setTokenBalancesPending(): types.SetTokenBalancesPendingAction {
return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES_PENDING
};
}
export type TSetTokenBalancesFulfilled = typeof setTokenBalancesFulfilled;
export function setTokenBalancesFulfilled(payload: {
[key: string]: {
balance: TokenValue;
error: string | null;
};
}): types.SetTokenBalancesAction {
}): types.SetTokenBalancesFulfilledAction {
return {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES,
type: TypeKeys.WALLET_SET_TOKEN_BALANCES_FULFILLED,
payload
};
}
export type TBroadcastTx = typeof broadcastTx;
export function broadcastTx(
signedTx: string
): types.BroadcastTxRequestedAction {
export function setTokenBalancesRejected(): types.SetTokenBalancesRejectedAction {
return {
type: TypeKeys.WALLET_BROADCAST_TX_REQUESTED,
payload: {
signedTx
}
type: TypeKeys.WALLET_SET_TOKEN_BALANCES_REJECTED
};
}
export type TBroadcastTxSucceded = typeof broadcastTxSucceded;
export function broadcastTxSucceded(
txHash: string,
signedTx: string
): types.BroadcastTxSuccededAction {
export type TScanWalletForTokens = typeof scanWalletForTokens;
export function scanWalletForTokens(wallet: IWallet): types.ScanWalletForTokensAction {
return {
type: TypeKeys.WALLET_BROADCAST_TX_SUCCEEDED,
payload: {
txHash,
signedTx
}
type: TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS,
payload: wallet
};
}
export type TBroadCastTxFailed = typeof broadCastTxFailed;
export function broadCastTxFailed(
signedTx: string,
errorMsg: string
): types.BroadcastTxFailedAction {
export type TSetWalletTokens = typeof setWalletTokens;
export function setWalletTokens(tokens: string[]): types.SetWalletTokensAction {
return {
type: TypeKeys.WALLET_BROADCAST_TX_FAILED,
payload: {
signedTx,
error: errorMsg
}
type: TypeKeys.WALLET_SET_WALLET_TOKENS,
payload: tokens
};
}
@ -128,3 +110,11 @@ export function resetWallet(): types.ResetWalletAction {
type: TypeKeys.WALLET_RESET
};
}
export type TSetWalletConfig = typeof setWalletConfig;
export function setWalletConfig(config: WalletConfig): types.SetWalletConfigAction {
return {
type: TypeKeys.WALLET_SET_CONFIG,
payload: config
};
}

View File

@ -1,5 +1,5 @@
import { Wei, TokenValue } from 'libs/units';
import { IWallet } from 'libs/wallet/IWallet';
import { IWallet, WalletConfig } from 'libs/wallet';
import { TypeKeys } from './constants';
/*** Unlock Private Key ***/
@ -45,8 +45,12 @@ export interface SetBalanceRejectedAction {
}
/*** Set Token Balance ***/
export interface SetTokenBalancesAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES;
export interface SetTokenBalancesPendingAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES_PENDING;
}
export interface SetTokenBalancesFulfilledAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES_FULFILLED;
payload: {
[key: string]: {
balance: TokenValue;
@ -55,12 +59,18 @@ export interface SetTokenBalancesAction {
};
}
/*** Broadcast Tx ***/
export interface BroadcastTxRequestedAction {
type: TypeKeys.WALLET_BROADCAST_TX_REQUESTED;
payload: {
signedTx: string;
};
export interface SetTokenBalancesRejectedAction {
type: TypeKeys.WALLET_SET_TOKEN_BALANCES_REJECTED;
}
export interface ScanWalletForTokensAction {
type: TypeKeys.WALLET_SCAN_WALLET_FOR_TOKENS;
payload: IWallet;
}
export interface SetWalletTokensAction {
type: TypeKeys.WALLET_SET_WALLET_TOKENS;
payload: string[];
}
/*** Unlock Mnemonic ***/
@ -82,20 +92,9 @@ export interface UnlockKeystoreAction {
payload: KeystoreUnlockParams;
}
export interface BroadcastTxSuccededAction {
type: TypeKeys.WALLET_BROADCAST_TX_SUCCEEDED;
payload: {
txHash: string;
signedTx: string;
};
}
export interface BroadcastTxFailedAction {
type: TypeKeys.WALLET_BROADCAST_TX_FAILED;
payload: {
signedTx: string;
error: string;
};
export interface SetWalletConfigAction {
type: TypeKeys.WALLET_SET_CONFIG;
payload: WalletConfig;
}
/*** Union Type ***/
@ -106,7 +105,9 @@ export type WalletAction =
| SetBalancePendingAction
| SetBalanceFullfilledAction
| SetBalanceRejectedAction
| SetTokenBalancesAction
| BroadcastTxRequestedAction
| BroadcastTxFailedAction
| BroadcastTxSuccededAction;
| SetTokenBalancesPendingAction
| SetTokenBalancesFulfilledAction
| SetTokenBalancesRejectedAction
| ScanWalletForTokensAction
| SetWalletTokensAction
| SetWalletConfigAction;

View File

@ -7,9 +7,11 @@ export enum TypeKeys {
WALLET_SET_BALANCE_PENDING = 'WALLET_SET_BALANCE_PENDING',
WALLET_SET_BALANCE_FULFILLED = 'WALLET_SET_BALANCE_FULFILLED',
WALLET_SET_BALANCE_REJECTED = 'WALLET_SET_BALANCE_REJECTED',
WALLET_SET_TOKEN_BALANCES = 'WALLET_SET_TOKEN_BALANCES',
WALLET_BROADCAST_TX_REQUESTED = 'WALLET_BROADCAST_TX_REQUESTED',
WALLET_BROADCAST_TX_FAILED = 'WALLET_BROADCAST_TX_FAILED',
WALLET_BROADCAST_TX_SUCCEEDED = 'WALLET_BROADCAST_TX_SUCCEEDED',
WALLET_SET_TOKEN_BALANCES_PENDING = 'WALLET_SET_TOKEN_BALANCES_PENDING',
WALLET_SET_TOKEN_BALANCES_FULFILLED = 'WALLET_SET_TOKEN_BALANCES_FULFILLED',
WALLET_SET_TOKEN_BALANCES_REJECTED = 'WALLET_SET_TOKEN_BALANCES_REJECTED',
WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS',
WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS',
WALLET_SET_CONFIG = 'WALLET_SET_CONFIG',
WALLET_RESET = 'WALLET_RESET'
}

View File

@ -0,0 +1,43 @@
import { Query } from 'components/renderCbs';
import { setCurrentTo, TSetCurrentTo } from 'actions/transaction';
import { AddressInput } from './AddressInput';
import React from 'react';
import { connect } from 'react-redux';
interface DispatchProps {
setCurrentTo: TSetCurrentTo;
}
interface OwnProps {
to: string | null;
}
type Props = DispatchProps & DispatchProps & OwnProps;
//TODO: add ens resolving
class AddressFieldClass extends React.Component<Props, {}> {
public componentDidMount() {
// this 'to' parameter can be either token or actual field related
const { to } = this.props;
if (to) {
this.props.setCurrentTo(to);
}
}
public render() {
return <AddressInput onChange={this.setAddress} />;
}
private setAddress = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.setCurrentTo(value);
};
}
const AddressField = connect(null, { setCurrentTo })(AddressFieldClass);
const DefaultAddressField: React.SFC<{}> = () => (
<Query params={['to']} withQuery={({ to }) => <AddressField to={to} />} />
);
export { DefaultAddressField as AddressField };

View File

@ -0,0 +1,56 @@
import React, { Component } from 'react';
import { Identicon } from 'components/ui';
import translate from 'translations';
//import { EnsAddress } from './components';
import { Query } from 'components/renderCbs';
import { donationAddressMap } from 'config/data';
import { ICurrentTo, getCurrentTo, isValidCurrentTo } from 'selectors/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
interface StateProps {
currentTo: ICurrentTo;
isValid: boolean;
}
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
type Props = OwnProps & StateProps;
//TODO: ENS handling
class AddressInputClass extends Component<Props> {
public render() {
const { currentTo, onChange, isValid } = this.props;
const { raw } = currentTo;
return (
<div className="row form-group">
<div className="col-xs-11">
<label>{translate('SEND_addr')}:</label>
<Query
params={['readOnly']}
withQuery={({ readOnly }) => (
<input
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
type="text"
value={raw}
placeholder={donationAddressMap.ETH}
readOnly={!!readOnly}
onChange={onChange}
/>
)}
/>
{/*<EnsAddress ensAddress={ensAddress} />*/}
</div>
<div className="col-xs-1" style={{ padding: 0 }}>
<Identicon address={/*ensAddress ||*/ raw} />
</div>
</div>
);
}
}
export const AddressInput = connect((state: AppState) => ({
currentTo: getCurrentTo(state),
isValid: isValidCurrentTo(state)
}))(AddressInputClass);

View File

@ -0,0 +1,36 @@
import React from 'react';
/*
public onChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
const newValue = (e.target as HTMLInputElement).value;
const { onChange } = this.props;
if (!onChange) {
return;
}
// FIXME debounce?
if (isValidENSAddress(newValue)) {
this.props.resolveEnsName(newValue);
}
onChange(newValue);
};
}
function mapStateToProps(state: AppState, props: PublicProps) {
return {
ensAddress: getEnsAddress(state, props.value)
};
}
export default connect(mapStateToProps, { resolveEnsName })(AddressField);
*/
interface EnsAddressProps {
ensAddress: string | null;
}
export const EnsAddress: React.SFC<EnsAddressProps> = ({ ensAddress }) =>
(!!ensAddress && (
<p className="ens-response">
<span className="mono">{ensAddress}</span>
</p>
)) ||
null;

View File

@ -0,0 +1 @@
export * from './EnsAddress';

View File

@ -0,0 +1 @@
export * from './AddressField';

View File

@ -0,0 +1,31 @@
import React from 'react';
import { AmountFieldFactory } from './AmountFieldFactory';
import { Aux } from 'components/ui';
import { UnitDropDown } from 'components';
import translate, { translateRaw } from 'translations';
interface Props {
hasUnitDropdown?: boolean;
}
export const AmountField: React.SFC<Props> = ({ hasUnitDropdown }) => (
<AmountFieldFactory
withProps={({ currentValue: { raw }, isValid, onChange, readOnly }) => (
<Aux>
<label>{translate('SEND_amount')}</label>
<div className="input-group">
<input
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
type="number"
placeholder={translateRaw('SEND_amount_short')}
value={raw}
readOnly={!!readOnly}
onChange={onChange}
/>
{hasUnitDropdown && <UnitDropDown />}
</div>
</Aux>
)}
/>
);

View File

@ -0,0 +1,59 @@
import React, { Component } from 'react';
import { Query } from 'components/renderCbs';
import { setCurrentValue, TSetCurrentValue } from 'actions/transaction';
import { connect } from 'react-redux';
import { AmountInput } from './AmountInputFactory';
import { AppState } from 'reducers';
export interface CallbackProps {
isValid: boolean;
readOnly: boolean;
currentValue:
| AppState['transaction']['fields']['value']
| AppState['transaction']['meta']['tokenValue'];
onChange(ev: React.FormEvent<HTMLInputElement>);
}
interface DispatchProps {
setCurrentValue: TSetCurrentValue;
}
interface OwnProps {
value: string | null;
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
type Props = DispatchProps & OwnProps;
class AmountFieldClass extends Component<Props, {}> {
public componentDidMount() {
const { value } = this.props;
if (value) {
this.props.setCurrentValue(value);
}
}
public render() {
return <AmountInput onChange={this.setValue} withProps={this.props.withProps} />;
}
private setValue = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.setCurrentValue(value);
};
}
const AmountField = connect(null, { setCurrentValue })(AmountFieldClass);
interface DefaultAmountFieldProps {
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
const DefaultAmountField: React.SFC<DefaultAmountFieldProps> = ({ withProps }) => (
<Query
params={['value']}
withQuery={({ value }) => <AmountField value={value} withProps={withProps} />}
/>
);
export { DefaultAmountField as AmountFieldFactory };

View File

@ -0,0 +1,43 @@
import React, { Component } from 'react';
import { Query } from 'components/renderCbs';
import { ICurrentValue, getCurrentValue, isValidAmount } from 'selectors/transaction';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { CallbackProps } from 'components/AmountFieldFactory';
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>);
withProps(props: CallbackProps): React.ReactElement<any> | null;
}
interface StateProps {
currentValue: ICurrentValue;
validAmount: boolean;
}
type Props = OwnProps & StateProps;
class AmountInputClass extends Component<Props> {
public render() {
const { currentValue, onChange, withProps, validAmount } = this.props;
return (
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>
withProps({
currentValue,
isValid: validAmount,
readOnly: !!readOnly,
onChange
})
}
/>
);
}
}
export const AmountInput = connect((state: AppState) => ({
currentValue: getCurrentValue(state),
validAmount: isValidAmount(state)
}))(AmountInputClass);

View File

@ -0,0 +1 @@
export * from './AmountFieldFactory';

View File

@ -37,8 +37,7 @@ export default class AccountInfo extends React.Component<Props, State> {
this.setAddressFromWallet();
}
// TODO: don't use any;
public toggleShowLongBalance = (e: React.SyntheticEvent<HTMLSpanElement>) => {
public toggleShowLongBalance = (e: React.FormEvent<HTMLSpanElement>) => {
e.preventDefault();
this.setState(state => {
return {
@ -79,6 +78,7 @@ export default class AccountInfo extends React.Component<Props, State> {
value={balance.wei}
unit={'ether'}
displayShortBalance={!showLongBalance}
checkOffline={true}
/>
)}
</span>

View File

@ -70,7 +70,12 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
<li className="EquivalentValues-values-currency" key={key}>
<span className="EquivalentValues-values-currency-label">{key}:</span>{' '}
<span className="EquivalentValues-values-currency-value">
<UnitDisplay unit={'ether'} value={values[key]} displayShortBalance={3} />
<UnitDisplay
unit={'ether'}
value={values[key]}
displayShortBalance={3}
checkOffline={true}
/>
</span>
</li>
);

View File

@ -0,0 +1,33 @@
@import "common/sass/variables";
.AddCustom {
&-field {
&-error {
font-size: 13px;
font-weight: normal;
color: $brand-danger;
}
}
&-buttons {
padding-top: 10px;
&-help {
display: block;
font-size: 13px;
margin-bottom: 10px;
}
&-btn {
margin-right: 10px;
&.btn-primary {
width: 120px;
}
&.btn-default {
width: 110px;
}
}
}
}

View File

@ -1,14 +1,19 @@
import React from 'react';
import classnames from 'classnames';
import { Token } from 'config/data';
import { isPositiveIntegerOrZero, isValidETHAddress } from 'libs/validators';
import React from 'react';
import translate from 'translations';
import NewTabLink from 'components/ui/NewTabLink';
import './AddCustomTokenForm.scss';
interface Props {
allTokens: Token[];
onSave(params: Token): void;
toggleForm(): void;
}
interface State {
tokenSymbolLookup: { [symbol: string]: boolean };
address: string;
symbol: string;
decimal: string;
@ -16,27 +21,36 @@ interface State {
export default class AddCustomTokenForm extends React.Component<Props, State> {
public state = {
tokenSymbolLookup: {},
address: '',
symbol: '',
decimal: ''
};
constructor(props: Props) {
super(props);
this.state = {
...this.state,
tokenSymbolLookup: this.generateSymbolLookup(props.allTokens)
};
}
public render() {
const { address, symbol, decimal } = this.state;
const inputClasses = 'AddCustom-field-input form-control input-sm';
const errors = this.getErrors();
const fields = [
{
name: 'address',
value: address,
label: translate('TOKEN_Addr')
},
{
name: 'symbol',
value: symbol,
label: translate('TOKEN_Symbol')
},
{
name: 'address',
value: address,
label: translate('TOKEN_Addr')
},
{
name: 'decimal',
value: decimal,
@ -53,50 +67,67 @@ export default class AddCustomTokenForm extends React.Component<Props, State> {
<input
className={classnames(
inputClasses,
errors[field.name] ? 'is-invalid' : 'is-valid'
errors[field.name] ? 'is-invalid' : field.value ? 'is-valid' : ''
)}
type="text"
name={field.name}
value={field.value}
onChange={this.onFieldChange}
/>
{typeof errors[field.name] === 'string' && (
<div className="AddCustom-field-error">{errors[field.name]}</div>
)}
</label>
);
})}
<button
className="btn btn-primary btn-sm btn-block"
disabled={!this.isValid()}
>
{translate('x_Save')}
</button>
<div className="AddCustom-buttons">
<NewTabLink
href="https://myetherwallet.github.io/knowledge-base/send/adding-new-token-and-sending-custom-tokens.html"
className="AddCustom-buttons-help"
>
{translate('Need help? Learn how to add custom tokens.')}
</NewTabLink>
<button
className="AddCustom-buttons-btn btn btn-primary btn-sm"
disabled={!this.isValid()}
>
{translate('x_Save')}
</button>
<button
className="AddCustom-buttons-btn btn btn-sm btn-default"
onClick={this.props.toggleForm}
>
{translate('x_Cancel')}
</button>
</div>
</form>
);
}
public getErrors() {
const { address, symbol, decimal } = this.state;
const errors = {
decimal: false,
address: false,
symbol: false
};
const errors: { [key: string]: boolean | string } = {};
if (!isPositiveIntegerOrZero(parseInt(decimal, 10))) {
// Formatting errors
if (decimal && !isPositiveIntegerOrZero(parseInt(decimal, 10))) {
errors.decimal = true;
}
if (!isValidETHAddress(address)) {
if (address && !isValidETHAddress(address)) {
errors.address = true;
}
if (!symbol) {
errors.symbol = true;
// Message errors
if (symbol && this.state.tokenSymbolLookup[symbol]) {
errors.symbol = 'A token with this symbol already exists';
}
return errors;
}
public isValid() {
return !Object.keys(this.getErrors()).length;
const { address, symbol, decimal } = this.state;
return !Object.keys(this.getErrors()).length && address && symbol && decimal;
}
public onFieldChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
@ -115,4 +146,11 @@ export default class AddCustomTokenForm extends React.Component<Props, State> {
const { address, symbol, decimal } = this.state;
this.props.onSave({ address, symbol, decimal: parseInt(decimal, 10) });
};
private generateSymbolLookup(tokens: Token[]) {
return tokens.reduce((prev, tk) => {
prev[tk.symbol] = true;
return prev;
}, {});
}
}

View File

@ -0,0 +1,131 @@
import React from 'react';
import translate from 'translations';
import { Token } from 'config/data';
import { TokenBalance } from 'selectors/wallet';
import AddCustomTokenForm from './AddCustomTokenForm';
import TokenRow from './TokenRow';
interface Props {
allTokens: Token[];
tokenBalances: TokenBalance[];
hasSavedWalletTokens: boolean;
scanWalletForTokens(): any;
setWalletTokens(tokens: string[]): any;
onAddCustomToken(token: Token): any;
onRemoveCustomToken(symbol: string): any;
}
interface State {
trackedTokens: { [symbol: string]: boolean };
showCustomTokenForm: boolean;
}
export default class TokenBalances extends React.Component<Props, State> {
public state = {
trackedTokens: {},
showCustomTokenForm: false
};
public componentWillReceiveProps(nextProps: Props) {
if (nextProps.tokenBalances !== this.props.tokenBalances) {
const trackedTokens = nextProps.tokenBalances.reduce((prev, t) => {
prev[t.symbol] = !t.balance.isZero();
return prev;
}, {});
this.setState({ trackedTokens });
}
}
public render() {
const { allTokens, tokenBalances, hasSavedWalletTokens } = this.props;
const { showCustomTokenForm, trackedTokens } = this.state;
let bottom;
if (!hasSavedWalletTokens) {
bottom = (
<div className="TokenBalances-buttons">
<button className="btn btn-primary btn-block" onClick={this.handleSetWalletTokens}>
<span>{translate('x_Save')}</span>
</button>
<p className="TokenBalances-buttons-help">
{translate('Missing tokens? You can add custom tokens next.')}
</p>
</div>
);
} else if (showCustomTokenForm) {
bottom = (
<div className="TokenBalances-form">
<AddCustomTokenForm
allTokens={allTokens}
onSave={this.addCustomToken}
toggleForm={this.toggleShowCustomTokenForm}
/>
</div>
);
} else {
bottom = (
<div className="TokenBalances-buttons">
<button className="btn btn-default btn-xs" onClick={this.toggleShowCustomTokenForm}>
<span>{translate('SEND_custom')}</span>
</button>{' '}
<button className="btn btn-default btn-xs" onClick={this.props.scanWalletForTokens}>
<span>Scan for New Tokens</span>
</button>
</div>
);
}
return (
<div>
{!hasSavedWalletTokens && (
<p className="TokenBalances-help">Select which tokens you would like to keep track of</p>
)}
<table className="TokenBalances-rows">
<tbody>
{tokenBalances.map(
token =>
token ? (
<TokenRow
key={token.symbol}
balance={token.balance}
symbol={token.symbol}
custom={token.custom}
decimal={token.decimal}
tracked={trackedTokens[token.symbol]}
toggleTracked={!hasSavedWalletTokens && this.toggleTrack}
onRemove={this.props.onRemoveCustomToken}
/>
) : null
)}
</tbody>
</table>
{bottom}
</div>
);
}
private toggleTrack = (symbol: string) => {
this.setState({
trackedTokens: {
...this.state.trackedTokens,
[symbol]: !this.state.trackedTokens[symbol]
}
});
};
private toggleShowCustomTokenForm = () => {
this.setState({
showCustomTokenForm: !this.state.showCustomTokenForm
});
};
private addCustomToken = (token: Token) => {
this.props.onAddCustomToken(token);
this.setState({ showCustomTokenForm: false });
};
private handleSetWalletTokens = () => {
const { trackedTokens } = this.state;
const desiredTokens = Object.keys(trackedTokens).filter(t => trackedTokens[t]);
this.props.setWalletTokens(desiredTokens);
};
}

View File

@ -11,21 +11,25 @@
&-balance {
@include mono;
}
&-symbol {
position: relative;
font-weight: 300;
&-remove {
margin-left: -32px;
margin-right: 20px;
height: 12px;
position: absolute;
top: 50%;
right: 4px;
float: right;
font-size: 18px;
cursor: pointer;
opacity: 0.4;
transform: translateY(-50%);
&:hover {
opacity: 1;
}
}
}
&-symbol {
font-weight: 300;
}
}

View File

@ -4,11 +4,15 @@ import { TokenValue } from 'libs/units';
import { UnitDisplay } from 'components/ui';
import './TokenRow.scss';
type ToggleTrackedFn = (symbol: string) => void;
interface Props {
balance: TokenValue;
symbol: string;
custom?: boolean;
decimal: number;
tracked: boolean;
toggleTracked: ToggleTrackedFn | false;
onRemove(symbol: string): void;
}
interface State {
@ -21,16 +25,32 @@ export default class TokenRow extends React.Component<Props, State> {
};
public render() {
const { balance, symbol, custom, decimal } = this.props;
const { balance, symbol, custom, decimal, tracked } = this.props;
const { showLongBalance } = this.state;
return (
<tr className="TokenRow">
<tr className="TokenRow" onClick={this.handleToggleTracked}>
{this.props.toggleTracked && (
<td className="TokenRow-toggled">
<input type="checkbox" checked={tracked} />
</td>
)}
<td
className="TokenRow-balance"
title={`${balance.toString()} (Double-Click)`}
onDoubleClick={this.toggleShowLongBalance}
>
<span>
<UnitDisplay
value={balance}
decimal={decimal}
displayShortBalance={!showLongBalance}
checkOffline={true}
/>
</span>
</td>
<td className="TokenRow-symbol">
{symbol}
{!!custom && (
<img
src={removeIcon}
@ -40,11 +60,7 @@ export default class TokenRow extends React.Component<Props, State> {
tabIndex={0}
/>
)}
<span>
<UnitDisplay value={balance} decimal={decimal} displayShortBalance={!showLongBalance} />
</span>
</td>
<td className="TokenRow-symbol">{symbol}</td>
</tr>
);
}
@ -58,7 +74,13 @@ export default class TokenRow extends React.Component<Props, State> {
});
};
public onRemove = () => {
private onRemove = () => {
this.props.onRemove(this.props.symbol);
};
private handleToggleTracked = () => {
if (this.props.toggleTracked) {
this.props.toggleTracked(this.props.symbol);
}
};
}

View File

@ -5,14 +5,35 @@
margin-top: 0;
}
&-help {
font-size: $font-size-small;
margin-bottom: 15px;
}
&-scan {
margin-top: 10px;
}
&-loader {
padding: 25px 0;
text-align: center;
}
&-rows {
width: 100%;
margin-bottom: $space;
}
&-form {
margin-top: $space * 2;
padding-top: $space;
border-top: 1px solid $gray-lighter;
margin-top: $space;
}
&-buttons {
&-help {
padding-top: 10px;
text-align: center;
font-size: $font-size-xs;
color: $gray;
}
}
}

View File

@ -1,93 +1,120 @@
import { Token } from 'config/data';
import React from 'react';
import { TokenBalance } from 'selectors/wallet';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import {
addCustomToken,
removeCustomToken,
TAddCustomToken,
TRemoveCustomToken
} from 'actions/customTokens';
import {
scanWalletForTokens,
TScanWalletForTokens,
setWalletTokens,
TSetWalletTokens
} from 'actions/wallet';
import { getAllTokens } from 'selectors/config';
import { getTokenBalances, getWalletInst, getWalletConfig, TokenBalance } from 'selectors/wallet';
import { Token } from 'config/data';
import translate from 'translations';
import AddCustomTokenForm from './AddCustomTokenForm';
import Balances from './Balances';
import Spinner from 'components/ui/Spinner';
import './index.scss';
import TokenRow from './TokenRow';
interface Props {
tokens: TokenBalance[];
onAddCustomToken(token: Token): any;
onRemoveCustomToken(symbol: string): any;
interface StateProps {
wallet: AppState['wallet']['inst'];
walletConfig: AppState['wallet']['config'];
tokens: Token[];
tokenBalances: TokenBalance[];
tokensError: AppState['wallet']['tokensError'];
isTokensLoading: AppState['wallet']['isTokensLoading'];
hasSavedWalletTokens: AppState['wallet']['hasSavedWalletTokens'];
}
interface State {
showAllTokens: boolean;
showCustomTokenForm: boolean;
interface ActionProps {
addCustomToken: TAddCustomToken;
removeCustomToken: TRemoveCustomToken;
scanWalletForTokens: TScanWalletForTokens;
setWalletTokens: TSetWalletTokens;
}
export default class TokenBalances extends React.Component<Props, State> {
public state = {
showAllTokens: false,
showCustomTokenForm: false
};
type Props = StateProps & ActionProps;
class TokenBalances extends React.Component<Props, {}> {
public render() {
const { tokens } = this.props;
const shownTokens = tokens.filter(
token => !token.balance.eqn(0) || token.custom || this.state.showAllTokens
);
const {
tokens,
walletConfig,
tokenBalances,
hasSavedWalletTokens,
isTokensLoading,
tokensError
} = this.props;
const walletTokens = walletConfig ? walletConfig.tokens : [];
let content;
if (tokensError) {
content = <h5>{tokensError}</h5>;
} else if (isTokensLoading) {
content = (
<div className="TokenBalances-loader">
<Spinner size="x3" />
</div>
);
} else if (!walletTokens) {
content = (
<button
className="TokenBalances-scan btn btn-primary btn-block"
onClick={this.scanWalletForTokens}
>
{translate('Scan for my Tokens')}
</button>
);
} else {
const shownBalances = tokenBalances.filter(t => walletTokens.includes(t.symbol));
content = (
<Balances
allTokens={tokens}
tokenBalances={shownBalances}
hasSavedWalletTokens={hasSavedWalletTokens}
scanWalletForTokens={this.scanWalletForTokens}
setWalletTokens={this.props.setWalletTokens}
onAddCustomToken={this.props.addCustomToken}
onRemoveCustomToken={this.props.removeCustomToken}
/>
);
}
return (
<section className="TokenBalances">
<h5 className="TokenBalances-title">{translate('sidebar_TokenBal')}</h5>
<table className="TokenBalances-rows">
<tbody>
{shownTokens.map(token => (
<TokenRow
key={token.symbol}
balance={token.balance}
symbol={token.symbol}
custom={token.custom}
decimal={token.decimal}
onRemove={this.props.onRemoveCustomToken}
/>
))}
</tbody>
</table>
<div className="TokenBalances-buttons">
<button
className="btn btn-default btn-xs"
onClick={this.toggleShowAllTokens}
>
{!this.state.showAllTokens ? 'Show All Tokens' : 'Hide Tokens'}
</button>{' '}
<button
className="btn btn-default btn-xs"
onClick={this.toggleShowCustomTokenForm}
>
<span>{translate('SEND_custom')}</span>
</button>
</div>
{this.state.showCustomTokenForm && (
<div className="TokenBalances-form">
<AddCustomTokenForm onSave={this.addCustomToken} />
</div>
)}
{content}
</section>
);
}
public toggleShowAllTokens = () => {
this.setState(state => {
return {
showAllTokens: !state.showAllTokens
};
});
};
public toggleShowCustomTokenForm = () => {
this.setState(state => {
return {
showCustomTokenForm: !state.showCustomTokenForm
};
});
};
public addCustomToken = (token: Token) => {
this.props.onAddCustomToken(token);
this.setState({ showCustomTokenForm: false });
private scanWalletForTokens = () => {
if (this.props.wallet) {
this.props.scanWalletForTokens(this.props.wallet);
}
};
}
function mapStateToProps(state: AppState): StateProps {
return {
wallet: getWalletInst(state),
walletConfig: getWalletConfig(state),
tokens: getAllTokens(state),
tokenBalances: getTokenBalances(state),
tokensError: state.wallet.tokensError,
isTokensLoading: state.wallet.isTokensLoading,
hasSavedWalletTokens: state.wallet.hasSavedWalletTokens
};
}
export default connect(mapStateToProps, {
addCustomToken,
removeCustomToken,
scanWalletForTokens,
setWalletTokens
})(TokenBalances);

View File

@ -1,22 +1,11 @@
import {
addCustomToken,
removeCustomToken,
TAddCustomToken,
TRemoveCustomToken
} from 'actions/customTokens';
import { showNotification, TShowNotification } from 'actions/notifications';
import { fetchCCRates as dFetchCCRates, TFetchCCRates } from 'actions/rates';
import { fetchCCRates, TFetchCCRates } from 'actions/rates';
import { NetworkConfig } from 'config/data';
import { IWallet, Balance } from 'libs/wallet';
import React from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getNetworkConfig } from 'selectors/config';
import {
getTokenBalances,
getWalletInst,
TokenBalance
} from 'selectors/wallet';
import { getShownTokenBalances, getWalletInst, TokenBalance } from 'selectors/wallet';
import AccountInfo from './AccountInfo';
import EquivalentValues from './EquivalentValues';
import Promos from './Promos';
@ -30,9 +19,6 @@ interface Props {
tokenBalances: TokenBalance[];
rates: AppState['rates']['rates'];
ratesError: AppState['rates']['ratesError'];
showNotification: TShowNotification;
addCustomToken: TAddCustomToken;
removeCustomToken: TRemoveCustomToken;
fetchCCRates: TFetchCCRates;
}
@ -44,15 +30,8 @@ interface Block {
export class BalanceSidebar extends React.Component<Props, {}> {
public render() {
const {
wallet,
balance,
network,
tokenBalances,
rates,
ratesError,
fetchCCRates
} = this.props;
const { wallet, balance, network, tokenBalances, rates, ratesError } = this.props;
if (!wallet) {
return null;
}
@ -64,9 +43,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
},
{
name: 'Account Info',
content: (
<AccountInfo wallet={wallet} balance={balance} network={network} />
)
content: <AccountInfo wallet={wallet} balance={balance} network={network} />
},
{
name: 'Promos',
@ -75,13 +52,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
},
{
name: 'Token Balances',
content: (
<TokenBalances
tokens={tokenBalances}
onAddCustomToken={this.props.addCustomToken}
onRemoveCustomToken={this.props.removeCustomToken}
/>
)
content: <TokenBalances />
},
{
name: 'Equivalent Values',
@ -91,7 +62,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
tokenBalances={tokenBalances}
rates={rates}
ratesError={ratesError}
fetchCCRates={fetchCCRates}
fetchCCRates={this.props.fetchCCRates}
/>
)
}
@ -100,10 +71,7 @@ export class BalanceSidebar extends React.Component<Props, {}> {
return (
<aside>
{blocks.map(block => (
<section
className={`Block ${block.isFullWidth ? 'is-full-width' : ''}`}
key={block.name}
>
<section className={`Block ${block.isFullWidth ? 'is-full-width' : ''}`} key={block.name}>
{block.content}
</section>
))}
@ -116,7 +84,7 @@ function mapStateToProps(state: AppState) {
return {
wallet: getWalletInst(state),
balance: state.wallet.balance,
tokenBalances: getTokenBalances(state),
tokenBalances: getShownTokenBalances(state, true),
network: getNetworkConfig(state),
rates: state.rates.rates,
ratesError: state.rates.ratesError
@ -124,8 +92,5 @@ function mapStateToProps(state: AppState) {
}
export default connect(mapStateToProps, {
addCustomToken,
removeCustomToken,
showNotification,
fetchCCRates: dFetchCCRates
fetchCCRates
})(BalanceSidebar);

View File

@ -0,0 +1,142 @@
import Modal, { IButton } from 'components/ui/Modal';
import Spinner from 'components/ui/Spinner';
import { Details, Summary } from './components';
import React from 'react';
import { connect } from 'react-redux';
import { getWalletType, IWalletType } from 'selectors/wallet';
import { getLanguageSelection } from 'selectors/config';
import {
broadcastLocalTransactionRequested,
TBroadcastLocalTransactionRequested,
broadcastWeb3TransactionRequested,
TBroadcastWeb3TransactionRequested
} from 'actions/transaction';
import {
currentTransactionBroadcasting,
currentTransactionBroadcasted,
currentTransactionFailed
} from 'selectors/transaction';
import translate, { translateRaw } from 'translations';
import './ConfirmationModal.scss';
import { AppState } from 'reducers';
interface DispatchProps {
broadcastLocalTransactionRequested: TBroadcastLocalTransactionRequested;
broadcastWeb3TransactionRequested: TBroadcastWeb3TransactionRequested;
}
interface StateProps {
lang: string;
walletTypes: IWalletType;
transactionBroadcasting: boolean;
transactionBroadcasted: boolean;
transactionFailed: boolean;
}
interface OwnProps {
onClose(): void;
}
interface State {
retryingFailedBroadcast: boolean;
timeToRead: number;
}
type Props = DispatchProps & StateProps & OwnProps;
class ConfirmationModalClass extends React.Component<Props, State> {
private readTimer = 0;
public constructor(props: Props) {
super(props);
const { transactionFailed } = props;
this.state = {
timeToRead: 5,
retryingFailedBroadcast: transactionFailed
};
}
public componentDidUpdate() {
if (this.props.transactionBroadcasted && !this.state.retryingFailedBroadcast) {
this.props.onClose();
}
}
// Count down 5 seconds before allowing them to confirm
public componentDidMount() {
this.readTimer = window.setInterval(() => {
if (this.state.timeToRead > 0) {
this.setState({ timeToRead: this.state.timeToRead - 1 });
} else {
window.clearInterval(this.readTimer);
}
}, 1000);
}
public render() {
const { onClose, transactionBroadcasting } = this.props;
const { timeToRead } = this.state;
const buttonPrefix = timeToRead > 0 ? `(${timeToRead}) ` : '';
const buttons: IButton[] = [
{
text: buttonPrefix + translateRaw('SENDModal_Yes'),
type: 'primary',
disabled: timeToRead > 0,
onClick: this.confirm
},
{
text: translateRaw('SENDModal_No'),
type: 'default',
onClick: onClose
}
];
return (
<div className="ConfModalWrap">
<Modal
title="Confirm Your Transaction"
buttons={buttons}
handleClose={onClose}
disableButtons={transactionBroadcasting}
isOpen={true}
>
<div className="ConfModal">
{transactionBroadcasting ? (
<div className="ConfModal-loading">
<Spinner size="x5" />
</div>
) : (
<div>
<Summary />
<Details />
<div className="ConfModal-confirm">{translate('SENDModal_Content_3')}</div>
</div>
)}
</div>
</Modal>
</div>
);
}
public componentWillUnmount() {
window.clearInterval(this.readTimer);
}
private confirm = () => {
if (this.state.timeToRead < 1) {
this.props.walletTypes.isWeb3Wallet
? this.props.broadcastWeb3TransactionRequested()
: this.props.broadcastLocalTransactionRequested();
this.setState({ retryingFailedBroadcast: false });
}
};
}
export const ConfirmationModal = connect(
(state: AppState) => ({
transactionBroadcasting: currentTransactionBroadcasting(state),
transactionBroadcasted: currentTransactionBroadcasted(state),
transactionFailed: currentTransactionFailed(state),
lang: getLanguageSelection(state),
walletTypes: getWalletType(state)
}),
{ broadcastLocalTransactionRequested, broadcastWeb3TransactionRequested }
)(ConfirmationModalClass);

View File

@ -0,0 +1,43 @@
import { getTransactionFields, makeTransaction } from 'libs/transaction';
import { SerializedTransaction } from 'components/renderCbs';
import { UnitDisplay } from 'components/ui';
import { Wei, TokenValue } from 'libs/units';
import ERC20 from 'libs/erc20';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getDecimal, getUnit } from 'selectors/transaction';
interface StateProps {
unit: string;
decimal: number;
}
class AmountClass extends Component<StateProps> {
public render() {
return (
<SerializedTransaction
withSerializedTransaction={serializedTransaction => {
const transactionInstance = makeTransaction(serializedTransaction);
const { value, data } = getTransactionFields(transactionInstance);
const { decimal, unit } = this.props;
return (
<UnitDisplay
decimal={decimal}
value={
unit === 'ether' ? Wei(value) : TokenValue(ERC20.transfer.decodeInput(data)._value)
}
symbol={unit}
checkOffline={false}
/>
);
}}
/>
);
}
}
export const Amount = connect((state: AppState) => ({
decimal: getDecimal(state),
unit: getUnit(state)
}))(AmountClass);

View File

@ -0,0 +1,45 @@
import { getTransactionFields, makeTransaction } from 'libs/transaction';
import { SerializedTransaction } from 'components/renderCbs';
import { Aux } from 'components/ui';
import ERC20 from 'libs/erc20';
import { From } from '../From';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getUnit } from 'selectors/transaction';
import { AppState } from 'reducers';
interface StateProps {
unit: string;
}
class AddressesClass extends Component<StateProps> {
public render() {
return (
<SerializedTransaction
withSerializedTransaction={serializedTransaction => {
const transactionInstance = makeTransaction(serializedTransaction);
const { to, data } = getTransactionFields(transactionInstance);
return (
<Aux>
<li className="ConfModal-details-detail">
You are sending from <From withFrom={from => <code>{from}</code>} />
</li>
<li className="ConfModal-details-detail">
You are sending to{' '}
<code>
{this.props.unit === 'ether' ? to : ERC20.transfer.decodeInput(data)._to}
</code>
</li>
</Aux>
);
}}
/>
);
}
}
export const Addresses = connect((state: AppState) => ({ unit: getUnit(state) }))(AddressesClass);
//got duplication here

View File

@ -0,0 +1,18 @@
import { GasPrice } from './components';
import { Amount } from '../../Amount';
import React from 'react';
export const AmountAndGasPrice: React.SFC<{}> = () => (
<li className="ConfModal-details-detail">
<p>
You are sending{' '}
<strong>
<Amount />
</strong>{' '}
with a gas price of{' '}
<strong>
<GasPrice />
</strong>
</p>
</li>
);

View File

@ -0,0 +1,18 @@
import React from 'react';
import { getTransactionFields, makeTransaction } from 'libs/transaction';
import { SerializedTransaction } from 'components/renderCbs';
import { UnitDisplay } from 'components/ui';
import { Wei } from 'libs/units';
export const GasPrice: React.SFC<{}> = () => (
<SerializedTransaction
withSerializedTransaction={serializedTransaction => {
const transactionInstance = makeTransaction(serializedTransaction);
const { gasPrice } = getTransactionFields(transactionInstance);
return (
<UnitDisplay unit={'gwei'} value={Wei(gasPrice)} symbol={'gwei'} checkOffline={false} />
);
}}
/>
);

View File

@ -0,0 +1 @@
export * from './GasPrice';

View File

@ -0,0 +1 @@
export * from './AmountAndGasPrice';

View File

@ -0,0 +1,44 @@
import { getTransactionFields, makeTransaction } from 'libs/transaction';
import { SerializedTransaction } from 'components/renderCbs';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { isEtherTransaction } from 'selectors/transaction';
interface StateProps {
showData: boolean;
}
class ShowDataWhenNoTokenClass extends Component<StateProps> {
public render() {
return this.props.showData ? <Data /> : null;
}
}
const ShowDataWhenNoToken = connect((state: AppState) => ({ showData: isEtherTransaction(state) }))(
ShowDataWhenNoTokenClass
);
const Data: React.SFC<{}> = () => (
<SerializedTransaction
withSerializedTransaction={serializedTransaction => {
const transactionInstance = makeTransaction(serializedTransaction);
const { data } = getTransactionFields(transactionInstance);
const dataBox = (
<span>
You are sending the following data:{' '}
<textarea className="form-control" value={data} rows={3} disabled={true} />
</span>
);
return (
<li className="ConfModal-details-detail">
{!emptyData(data) ? dataBox : 'There is no data attached to this transaction'}
</li>
);
}}
/>
);
const emptyData = (data: string) => data === '0x';
export { ShowDataWhenNoToken as Data };

View File

@ -0,0 +1,16 @@
import { Addresses } from './Addresses';
import { Data } from './Data';
import { Node } from './Node';
import { AmountAndGasPrice } from './AmountAndGasPrice';
import { Nonce } from './Nonce';
import React from 'react';
export const Details: React.SFC<{}> = () => (
<ul className="ConfModal-details">
<Addresses />
<Nonce />
<AmountAndGasPrice />
<Node />
<Data />
</ul>
);

View File

@ -0,0 +1,24 @@
import { NodeConfig } from 'config/data';
import React, { Component } from 'react';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
import { getNodeConfig } from 'selectors/config';
interface StateProps {
node: NodeConfig;
}
class NodeClass extends Component<StateProps, {}> {
public render() {
return (
<li className="ConfModal-details-detail">
You are interacting with the <strong>{this.props.node.network}</strong> network provided by{' '}
<strong>{this.props.node.service}</strong>
</li>
);
}
}
export const Node = connect((state: AppState) => ({
node: getNodeConfig(state)
}))(NodeClass);

View File

@ -0,0 +1,19 @@
import React from 'react';
import { getTransactionFields, makeTransaction } from 'libs/transaction';
import { SerializedTransaction } from 'components/renderCbs';
import { Nonce as makeNonce } from 'libs/units';
export const Nonce: React.SFC<{}> = () => (
<SerializedTransaction
withSerializedTransaction={serializedTransaction => {
const transactionInstance = makeTransaction(serializedTransaction);
const { nonce } = getTransactionFields(transactionInstance);
return (
<li className="ConfModal-details-detail">
You are sending with a nonce of <code>{makeNonce(nonce).toString()}</code>
</li>
);
}}
/>
);

View File

@ -0,0 +1 @@
export * from './Details';

View File

@ -0,0 +1,21 @@
import { AppState } from 'reducers';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getFrom } from 'selectors/transaction';
type From = AppState['transaction']['meta']['from'];
interface StateProps {
from: From;
}
interface OwnProps {
withFrom(from: string): React.ReactElement<any> | null;
}
class FromClass extends Component<StateProps & OwnProps, {}> {
public render() {
const { from, withFrom } = this.props;
return from ? withFrom(from) : null;
}
}
export const From = connect((state: AppState) => ({ from: getFrom(state) }))(FromClass);

View File

@ -0,0 +1,10 @@
import { SummaryAmount, SummaryFrom, SummaryTo } from './components';
import React from 'react';
export const Summary: React.SFC<{}> = () => (
<div className="ConfModal-summary">
<SummaryFrom />
<SummaryAmount />
<SummaryTo />
</div>
);

View File

@ -0,0 +1,11 @@
import React from 'react';
import { Amount } from '../../Amount';
export const SummaryAmount: React.SFC<{}> = () => (
<div className="ConfModal-summary-amount">
<div className="ConfModal-summary-amount-arrow" />
<div className="ConfModal-summary-amount-currency">
<Amount />
</div>
</div>
);

View File

@ -0,0 +1,9 @@
import React from 'react';
import { From } from '../../From';
import { Identicon } from 'components/ui';
export const SummaryFrom: React.SFC<{}> = () => (
<div className="ConfModal-summary-icon ConfModal-summary-icon--from">
<From withFrom={from => <Identicon size="100%" address={from} />} />
</div>
);

View File

@ -0,0 +1,37 @@
import { Identicon } from 'components/ui';
import { SerializedTransaction } from 'components/renderCbs';
import { makeTransaction, getTransactionFields } from 'libs/transaction';
import ERC20 from 'libs/erc20';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getUnit } from 'selectors/transaction';
interface StateProps {
unit: string;
}
//got duplication here
class SummaryToClass extends Component<StateProps> {
public render() {
return (
<SerializedTransaction
withSerializedTransaction={serializedTransaction => {
const transactionInstance = makeTransaction(serializedTransaction);
const { to, data } = getTransactionFields(transactionInstance);
return (
<div className="ConfModal-summary-icon ConfModal-summary-icon--to">
<Identicon
size="100%"
address={this.props.unit === 'ether' ? to : ERC20.transfer.decodeInput(data)._to}
/>
</div>
);
}}
/>
);
}
}
export const SummaryTo = connect((state: AppState) => ({ unit: getUnit(state) }))(SummaryToClass);

View File

@ -0,0 +1,3 @@
export * from './SummaryTo';
export * from './SummaryFrom';
export * from './SummaryAmount';

View File

@ -0,0 +1 @@
export * from './Summary';

View File

@ -0,0 +1,2 @@
export * from './Details';
export * from './Summary';

View File

@ -0,0 +1 @@
export * from './ConfirmationModal';

View File

@ -0,0 +1,18 @@
import { CustomMessage, messages } from './components';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getCurrentTo, ICurrentTo } from 'selectors/transaction';
interface StateProps {
currentTo: ICurrentTo;
}
class CurrentCustomMessageClass extends Component<StateProps> {
public render() {
return <CustomMessage message={messages.find(m => m.to === this.props.currentTo.raw)} />;
}
}
export const CurrentCustomMessage = connect((state: AppState) => ({
currentTo: getCurrentTo(state)
}))(CurrentCustomMessageClass);

View File

@ -7,22 +7,19 @@ interface Props {
};
}
export default function CustomMessage(props: Props) {
export const CustomMessage = (props: Props) => {
return (
<div className="clearfix form-group">
{!!props.message &&
{!!props.message && (
<div className="alert alert-info col-xs-12 clearfix">
<p>
<small>
A message from {props.message.to}
</small>
<small>A message from {props.message.to}</small>
</p>
<p>
<strong>
{props.message.msg}
</strong>
<strong>{props.message.msg}</strong>
</p>
</div>}
</div>
)}
</div>
);
}
};

View File

@ -0,0 +1,2 @@
export * from './CustomMessage';
export * from './messages';

View File

@ -1,4 +1,4 @@
export default [
export const messages = [
{
// donation address example
to: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8',

View File

@ -0,0 +1 @@
export * from './CurrentCustomMessage';

View File

@ -0,0 +1,32 @@
import { DataFieldFactory } from './DataFieldFactory';
import React from 'react';
import { Expandable, ExpandHandler } from 'components/ui';
import translate from 'translations';
import { donationAddressMap } from 'config/data';
const expander = (expandHandler: ExpandHandler) => (
<a onClick={expandHandler}>
<p className="strong">{translate('TRANS_advanced')}</p>
</a>
);
export const DataField: React.SFC<{}> = () => (
<DataFieldFactory
withProps={({ data: { raw }, dataExists, onChange, readOnly }) => (
<Expandable expandLabel={expander}>
<div className="form-group">
<label>{translate('TRANS_data')}</label>
<input
className={`form-control ${dataExists ? 'is-valid' : 'is-invalid'}`}
type="text"
placeholder={donationAddressMap.ETH}
value={raw}
readOnly={!!readOnly}
onChange={onChange}
/>
</div>
</Expandable>
)}
/>
);

View File

@ -0,0 +1,65 @@
import { DataInput } from './DataInputFactory';
import { Query } from 'components/renderCbs';
import { inputData, TInputData } from 'actions/transaction';
import React from 'react';
import { connect } from 'react-redux';
import { isEtherTransaction } from 'selectors/transaction';
import { AppState } from 'reducers';
export interface CallBackProps {
data: AppState['transaction']['fields']['data'];
dataExists: boolean;
readOnly: boolean;
onChange(ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>);
}
interface DispatchProps {
isEtherTransaction;
inputData: TInputData;
}
interface OwnProps {
data: string | null;
withProps(props: CallBackProps): React.ReactElement<any> | null;
}
interface StateProps {
isEtherTransaction: boolean;
}
type Props = DispatchProps & OwnProps & StateProps;
class DataFieldClass extends React.Component<Props> {
public componentDidMount() {
const { data } = this.props;
if (data) {
this.props.inputData(data);
}
}
public render() {
return this.props.isEtherTransaction ? (
<DataInput onChange={this.setData} withProps={this.props.withProps} />
) : null;
}
private setData = (ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { value } = ev.currentTarget;
this.props.inputData(value);
};
}
const DataField = connect(
(state: AppState) => ({ isEtherTransaction: isEtherTransaction(state) }),
{ inputData }
)(DataFieldClass);
interface DefaultDataFieldProps {
withProps(props: CallBackProps): React.ReactElement<any> | null;
}
const DefaultDataField: React.SFC<DefaultDataFieldProps> = ({ withProps }) => (
/* TODO: check query param of tokens too */
<Query
params={['data']}
withQuery={({ data }) => <DataField data={data} withProps={withProps} />}
/>
);
export { DefaultDataField as DataFieldFactory };

View File

@ -0,0 +1,36 @@
import React, { Component } from 'react';
import { Query } from 'components/renderCbs';
import { getData, getDataExists } from 'selectors/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { CallBackProps } from 'components/DataFieldFactory';
interface OwnProps {
withProps(props: CallBackProps): React.ReactElement<any> | null;
onChange(ev: React.FormEvent<HTMLInputElement>);
}
interface StateProps {
data: AppState['transaction']['fields']['data'];
dataExists: boolean;
}
type Props = OwnProps & StateProps;
class DataInputClass extends Component<Props> {
public render() {
const { data, onChange, dataExists } = this.props;
return (
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>
this.props.withProps({ data, onChange, readOnly: !!readOnly, dataExists })
}
/>
);
}
}
export const DataInput = connect((state: AppState) => ({
data: getData(state),
dataExists: getDataExists(state)
}))(DataInputClass);

View File

@ -0,0 +1 @@
export * from './DataFieldFactory';

View File

@ -0,0 +1,21 @@
import React from 'react';
import { GasFieldFactory } from './GasFieldFactory';
import translate from 'translations';
import { Aux } from 'components/ui';
export const GasField: React.SFC<{}> = () => (
<Aux>
<label>{translate('TRANS_gas')} </label>
<GasFieldFactory
withProps={({ gasLimit: { raw, value }, onChange, readOnly }) => (
<input
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}
type="text"
readOnly={!!readOnly}
value={raw}
onChange={onChange}
/>
)}
/>
</Aux>
);

View File

@ -0,0 +1,57 @@
import React, { Component } from 'react';
import { GasQuery } from 'components/renderCbs';
import { GasInput } from './GasInputFactory';
import { inputGasLimit, TInputGasLimit } from 'actions/transaction';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
const defaultGasLimit = '21000';
export interface CallBackProps {
readOnly: boolean;
gasLimit: AppState['transaction']['fields']['gasLimit'];
onChange(value: React.FormEvent<HTMLInputElement>): void;
}
interface DispatchProps {
inputGasLimit: TInputGasLimit;
}
interface OwnProps {
gasLimit: string | null;
withProps(props: CallBackProps);
}
type Props = DispatchProps & OwnProps;
class GasLimitFieldClass extends Component<Props, {}> {
public componentDidMount() {
const { gasLimit } = this.props;
if (gasLimit) {
this.props.inputGasLimit(gasLimit);
} else {
this.props.inputGasLimit(defaultGasLimit);
}
}
public render() {
return <GasInput onChange={this.setGas} withProps={this.props.withProps} />;
}
private setGas = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.inputGasLimit(value);
};
}
const GasLimitField = connect(null, { inputGasLimit })(GasLimitFieldClass);
interface DefaultGasFieldProps {
withProps(props: CallBackProps);
}
const DefaultGasField: React.SFC<DefaultGasFieldProps> = ({ withProps }) => (
<GasQuery
withQuery={({ gasLimit }) => <GasLimitField gasLimit={gasLimit} withProps={withProps} />}
/>
);
export { DefaultGasField as GasFieldFactory };

View File

@ -0,0 +1,34 @@
import React, { Component } from 'react';
import { Query } from 'components/renderCbs';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getGasLimit } from 'selectors/transaction';
import { CallBackProps } from 'components/GasFieldFactory';
interface StateProps {
gasLimit: AppState['transaction']['fields']['gasLimit'];
}
interface OwnProps {
withProps(props: CallBackProps);
onChange(value: React.FormEvent<HTMLInputElement>): void;
}
type Props = StateProps & OwnProps;
class GasInputClass extends Component<Props> {
public render() {
const { gasLimit, onChange } = this.props;
return (
<Query
params={['readOnly']}
withQuery={({ readOnly }) =>
this.props.withProps({ gasLimit, onChange, readOnly: !!readOnly })
}
/>
);
}
}
export const GasInput = connect((state: AppState) => ({ gasLimit: getGasLimit(state) }))(
GasInputClass
);

View File

@ -0,0 +1 @@
export * from './GasFieldFactory';

View File

@ -0,0 +1,39 @@
import React from 'react';
import classnames from 'classnames';
interface Props {
isValid: boolean;
isVisible: boolean;
name: string;
value: string;
placeholder: string;
disabled?: boolean;
handleInput(e: React.FormEvent<HTMLInputElement>): void;
handleToggle(): void;
}
const KeystoreInput: React.SFC<Props> = ({
isValid,
isVisible,
handleInput,
name,
value,
placeholder,
disabled,
handleToggle
}) => (
<div className="input-group">
<input
className={classnames('form-control', isValid ? 'is-valid' : 'is-invalid')}
type={isVisible ? 'text' : 'password'}
name={name}
placeholder={placeholder}
value={value}
onChange={handleInput}
disabled={disabled}
/>
<span onClick={handleToggle} role="button" className="input-group-addon eye" />
</div>
);
export default KeystoreInput;

View File

@ -0,0 +1,18 @@
@import 'common/sass/variables';
.GenKeystore {
max-width: 40rem;
&-field {
margin-bottom: $space;
&-label {
margin: 0 0 $space * 0.5;
}
}
&-button {
max-width: 16rem;
margin: $space auto 0;
}
}

View File

@ -0,0 +1,177 @@
import React from 'react';
import { generateKeystoreFileInfo, KeystoreFile } from 'utils/keystore';
import Modal from 'components/ui/Modal';
import Input from './Input';
import translate, { translateRaw } from 'translations';
import { MINIMUM_PASSWORD_LENGTH } from 'config/data';
import { isValidPrivKey } from 'libs/validators';
import './index.scss';
interface Props {
isOpen: boolean;
privateKey?: string;
handleClose(): void;
}
interface State {
privateKey: string;
password: string;
isPrivateKeyVisible: boolean;
isPasswordVisible: boolean;
keystoreFile: KeystoreFile | null;
hasError: boolean;
}
const initialState: State = {
privateKey: '',
password: '',
isPrivateKeyVisible: false,
isPasswordVisible: false,
keystoreFile: null,
hasError: false
};
export default class GenerateKeystoreModal extends React.Component<Props, State> {
public state: State = initialState;
constructor(props: Props) {
super(props);
if (props.privateKey) {
this.state = {
...this.state,
privateKey: props.privateKey
};
}
}
public componentWillReceiveProps(nextProps) {
if (nextProps.privateKey !== this.props.privateKey) {
this.setState({ privateKey: nextProps.privateKey });
}
}
public render() {
const {
privateKey,
password,
isPrivateKeyVisible,
isPasswordVisible,
keystoreFile,
hasError
} = this.state;
const isPrivateKeyValid = isValidPrivKey(privateKey);
const isPasswordValid = password.length >= MINIMUM_PASSWORD_LENGTH;
return (
<Modal
title={translate('Generate Keystore File')}
isOpen={this.props.isOpen}
handleClose={this.handleClose}
>
<form className="GenKeystore" onSubmit={this.handleSubmit}>
<label className="GenKeystore-field">
<h4 className="GenKeystore-field-label">Private Key</h4>
<Input
isValid={isPrivateKeyValid}
isVisible={isPrivateKeyVisible}
name="privateKey"
value={privateKey}
handleInput={this.handleInput}
placeholder="f1d0e0789c6d40f39..."
handleToggle={this.togglePrivateKey}
disabled={!!this.props.privateKey}
/>
</label>
<label className="GenKeystore-field">
<h4 className="GenKeystore-field-label">Password</h4>
<Input
isValid={isPasswordValid}
isVisible={isPasswordVisible}
name="password"
value={password}
placeholder={translateRaw('Minimum 9 characters')}
handleInput={this.handleInput}
handleToggle={this.togglePassword}
/>
</label>
{!keystoreFile ? (
<button
className="GenKeystore-button btn btn-primary btn-block"
disabled={!isPrivateKeyValid || !isPasswordValid}
>
{translate('Generate Keystore File')}
</button>
) : hasError ? (
<p className="alert alert-danger">
Keystore generation failed or was invalid. In order to prevent loss of funds, we
cannot provide you with a keystore file that may be corrupted. Refresh the page or use
a different browser, and try again.
</p>
) : (
<a
onClick={this.handleClose}
href={keystoreFile.blob}
className="GenKeystore-button btn btn-success btn-block"
aria-label={translateRaw('x_Keystore')}
aria-describedby={translateRaw('x_KeystoreDesc')}
download={keystoreFile.filename}
>
{translate('Download Keystore File')}
</a>
)}
</form>
</Modal>
);
}
private togglePrivateKey = () => {
this.setState({
isPrivateKeyVisible: !this.state.isPrivateKeyVisible
});
};
private togglePassword = () => {
this.setState({
isPasswordVisible: !this.state.isPasswordVisible
});
};
private handleInput = (e: React.FormEvent<HTMLInputElement>) => {
const { name, value } = e.currentTarget;
let { keystoreFile } = this.state;
if (name === 'privateKey') {
keystoreFile = null;
}
this.setState({ [name as any]: value, keystoreFile });
};
private handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!this.state.keystoreFile) {
this.generateKeystoreFileInfo();
}
};
private generateKeystoreFileInfo = async () => {
try {
const { privateKey, password } = this.state;
const keystoreFile = await generateKeystoreFileInfo(privateKey, password);
if (keystoreFile) {
this.setState({ keystoreFile });
}
} catch (err) {
this.setState({ hasError: true });
}
};
private handleClose = () => {
this.setState({
...initialState,
privateKey: this.props.privateKey || initialState.privateKey
});
this.props.handleClose();
};
}

View File

@ -0,0 +1,43 @@
import {
signLocalTransactionRequested,
signWeb3TransactionRequested,
TSignLocalTransactionRequested,
TSignWeb3TransactionRequested,
SignLocalTransactionRequestedAction,
SignWeb3TransactionRequestedAction
} from 'actions/transaction';
import React, { Component } from 'react';
import { connect, Dispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { AppState } from 'reducers';
type Payload =
| SignLocalTransactionRequestedAction['payload']
| SignWeb3TransactionRequestedAction['payload'];
type Signer = (
payload: Payload
) => () => SignLocalTransactionRequestedAction | SignWeb3TransactionRequestedAction;
interface DispatchProps {
signer: TSignLocalTransactionRequested | TSignWeb3TransactionRequested;
}
interface Props {
isWeb3: boolean;
withSigner(signer: Signer): React.ReactElement<any> | null;
}
class Container extends Component<DispatchProps & Props, {}> {
public render() {
return this.props.withSigner(this.sign);
}
private sign = (payload: Payload) => () => this.props.signer(payload);
}
export const WithSigner = connect(null, (dispatch: Dispatch<AppState>, ownProps: Props) => {
return bindActionCreators(
{
signer: ownProps.isWeb3 ? signWeb3TransactionRequested : signLocalTransactionRequested
},
dispatch
);
})(Container);

View File

@ -0,0 +1,41 @@
import translate from 'translations';
import { WithSigner } from './Container';
import EthTx from 'ethereumjs-tx';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getTransaction, isNetworkRequestPending } from 'selectors/transaction';
import { getWalletType } from 'selectors/wallet';
interface StateProps {
transaction: EthTx;
networkRequestPending: boolean;
isFullTransaction: boolean;
isWeb3Wallet: boolean;
}
class GenerateTransactionClass extends Component<StateProps> {
public render() {
const { isFullTransaction, isWeb3Wallet, transaction, networkRequestPending } = this.props;
return (
<WithSigner
isWeb3={isWeb3Wallet}
withSigner={signer => (
<button
disabled={!isFullTransaction || networkRequestPending}
className="btn btn-info btn-block"
onClick={signer(transaction)}
>
{isWeb3Wallet ? translate('Send to MetaMask / Mist') : translate('DEP_signtx')}
</button>
)}
/>
);
}
}
export const GenerateTransaction = connect((state: AppState) => ({
...getTransaction(state),
networkRequestPending: isNetworkRequestPending(state),
isWeb3Wallet: getWalletType(state).isWeb3Wallet
}))(GenerateTransactionClass);

View File

@ -0,0 +1 @@
export * from './GenerateTransaction';

View File

@ -3,13 +3,15 @@ import throttle from 'lodash/throttle';
import React, { Component } from 'react';
import DropdownShell from 'components/ui/DropdownShell';
import './GasPriceDropdown.scss';
import { SetGasLimitFieldAction } from 'actions/transaction';
import { gasPricetoBase } from 'libs/units';
interface Props {
value: number;
onChange(gasPrice: number): void;
value: string;
onChange(payload: SetGasLimitFieldAction['payload']): void;
}
export default class GasPriceDropdown extends Component<Props, {}> {
export default class GasPriceDropdown extends Component<Props> {
constructor(props: Props) {
super(props);
this.updateGasPrice = throttle(this.updateGasPrice, 50);
@ -69,10 +71,10 @@ export default class GasPriceDropdown extends Component<Props, {}> {
};
private updateGasPrice = (value: string) => {
this.props.onChange(parseInt(value, 10));
this.props.onChange({ raw: value, value: gasPricetoBase(parseInt(value, 10)) });
};
private handleGasPriceChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
this.updateGasPrice((e.target as HTMLInputElement).value);
private handleGasPriceChange = (e: React.FormEvent<HTMLInputElement>) => {
this.updateGasPrice(e.currentTarget.value);
};
}

View File

@ -11,36 +11,28 @@ const tabs = [
},
{
name: 'NAV_SendEther',
to: 'send-transaction'
name: 'Account View & Send',
to: '/account'
},
{
name: 'NAV_Swap',
to: 'swap'
},
{
name: 'NAV_ViewWallet'
// to: 'view-wallet'
to: '/swap'
},
{
name: 'NAV_Contracts',
to: 'contracts'
to: '/contracts'
},
{
name: 'NAV_ENS',
to: 'ens'
to: '/ens'
},
{
name: 'Sign & Verify Message',
to: 'sign-and-verify-message'
to: '/sign-and-verify-message'
},
{
name: 'Broadcast Transaction',
to: 'pushTx'
},
{
name: 'NAV_Utilities',
to: 'utilities'
to: '/pushTx'
},
{
name: 'NAV_Help',

View File

@ -26,28 +26,17 @@ class NavigationLink extends React.Component<Props, {}> {
const linkClasses = classnames({
'NavigationLink-link': true,
'is-disabled': !link.to,
'is-active':
location.pathname === link.to ||
location.pathname.substring(1) === link.to
'is-active': location.pathname === link.to
});
const linkLabel = `nav item: ${translateRaw(link.name)}`;
const linkEl =
link.external || !link.to ? (
<a
className={linkClasses}
href={link.to}
aria-label={linkLabel}
target="_blank"
>
<a className={linkClasses} href={link.to} aria-label={linkLabel} target="_blank">
{translate(link.name)}
</a>
) : (
<Link
className={linkClasses}
to={(link as any).to}
aria-label={linkLabel}
>
<Link className={linkClasses} to={(link as any).to} aria-label={linkLabel}>
{translate(link.name)}
</Link>
);

View File

@ -1,5 +1,4 @@
import {
TChangeGasPrice,
TChangeLanguage,
TChangeNodeIntent,
TAddCustomNode,
@ -11,6 +10,7 @@ import { Dropdown, ColorDropdown } from 'components/ui';
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { TSetGasPriceField } from 'actions/transaction';
import {
ANNOUNCEMENT_MESSAGE,
ANNOUNCEMENT_TYPE,
@ -28,19 +28,19 @@ import { getKeyByValue } from 'utils/helpers';
import { makeCustomNodeId } from 'utils/node';
import { getNetworkConfigFromId } from 'utils/network';
import './index.scss';
import { AppState } from 'reducers';
interface Props {
languageSelection: string;
node: NodeConfig;
nodeSelection: string;
isChangingNode: boolean;
gasPriceGwei: number;
gasPrice: AppState['transaction']['fields']['gasPrice'];
customNodes: CustomNodeConfig[];
customNetworks: CustomNetworkConfig[];
changeLanguage: TChangeLanguage;
changeNodeIntent: TChangeNodeIntent;
changeGasPrice: TChangeGasPrice;
setGasPriceField: TSetGasPriceField;
addCustomNode: TAddCustomNode;
removeCustomNode: TRemoveCustomNode;
addCustomNetwork: TAddCustomNetwork;
@ -129,8 +129,8 @@ export default class Header extends Component<Props, State> {
<div className="Header-branding-right-dropdown">
<GasPriceDropdown
value={this.props.gasPriceGwei}
onChange={this.props.changeGasPrice}
value={this.props.gasPrice.raw}
onChange={this.props.setGasPriceField}
/>
</div>

View File

@ -0,0 +1,23 @@
import { NonceInput } from './NonceInput';
import { inputNonce, TInputNonce } from 'actions/transaction';
import React, { Component } from 'react';
import { connect } from 'react-redux';
interface DispatchProps {
inputNonce: TInputNonce;
}
class NonceFieldClass extends Component<DispatchProps> {
public render() {
return <NonceInput onChange={this.setNonce} />;
}
private setNonce = (ev: React.FormEvent<HTMLInputElement>) => {
const { value } = ev.currentTarget;
this.props.inputNonce(value);
};
}
export const NonceField = connect(null, {
inputNonce
})(NonceFieldClass);

View File

@ -0,0 +1,55 @@
import React, { Component } from 'react';
import { Aux } from 'components/ui';
import { Query } from 'components/renderCbs';
import Help from 'components/ui/Help';
import { getNonce, nonceRequestFailed } from 'selectors/transaction';
import { isAnyOffline } from 'selectors/config';
import { AppState } from 'reducers';
import { connect } from 'react-redux';
const nonceHelp = (
<Help
size={'x1'}
link={'https://myetherwallet.github.io/knowledge-base/transactions/what-is-nonce.html'}
/>
);
interface OwnProps {
onChange(ev: React.FormEvent<HTMLInputElement>): void;
}
interface StateProps {
shouldDisplay: boolean;
nonce: AppState['transaction']['fields']['nonce'];
}
type Props = OwnProps & StateProps;
class NonceInputClass extends Component<Props> {
public render() {
const { nonce: { raw, value }, onChange, shouldDisplay } = this.props;
const content = (
<Aux>
{nonceHelp}
<label>Nonce</label>
<Query
params={['readOnly']}
withQuery={({ readOnly }) => (
<input
className={`form-control ${!!value ? 'is-valid' : 'is-invalid'}`}
type="text"
value={raw}
readOnly={!!readOnly}
onChange={onChange}
/>
)}
/>
</Aux>
);
return shouldDisplay ? content : null;
}
}
export const NonceInput = connect((state: AppState) => ({
shouldDisplay: isAnyOffline(state) || nonceRequestFailed(state),
nonce: getNonce(state)
}))(NonceInputClass);

View File

@ -0,0 +1 @@
export * from './NonceField';

View File

@ -0,0 +1,36 @@
import { UnlockHeader } from 'components/ui';
import React, { Component } from 'react';
import translate from 'translations';
import { isAnyOffline } from 'selectors/config';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
interface Props {
allowReadOnly: boolean;
}
export const OfflineAwareUnlockHeader: React.SFC<Props> = ({ allowReadOnly }) => (
<UnlockHeader title={<Title />} allowReadOnly={allowReadOnly} />
);
interface StateProps {
shouldDisplayOffline: boolean;
}
class TitleClass extends Component<StateProps> {
public render() {
const { shouldDisplayOffline } = this.props;
const offlineTitle = shouldDisplayOffline ? (
<span style={{ color: 'red' }}> (Offline)</span>
) : null;
return (
<div>
{translate('Account')}
{offlineTitle}
</div>
);
}
}
const Title = connect((state: AppState) => ({
shouldDisplayOffline: isAnyOffline(state)
}))(TitleClass);

View File

@ -5,7 +5,7 @@ import translate from 'translations';
import printElement from 'utils/printElement';
import { stripHexPrefix } from 'libs/values';
const print = (address: string, privateKey: string) => () =>
export const print = (address: string, privateKey: string) => () =>
address &&
privateKey &&
printElement(<PaperWallet address={address} privateKey={privateKey} />, {

View File

@ -0,0 +1,17 @@
import React from 'react';
import { SendButtonFactory } from './SendButtonFactory';
import translate from 'translations';
export const SendButton: React.SFC<{}> = () => (
<SendButtonFactory
withProps={({ onClick }) => (
<div className="row form-group">
<div className="col-xs-12">
<button className="btn btn-primary btn-block" onClick={onClick}>
{translate('SEND_trans')}
</button>
</div>
</div>
)}
/>
);

Some files were not shown because too many files have changed in this diff Show More