diff --git a/common/actions/transaction/actionCreators/fields.ts b/common/actions/transaction/actionCreators/fields.ts index e64c15aa..ab431a8a 100644 --- a/common/actions/transaction/actionCreators/fields.ts +++ b/common/actions/transaction/actionCreators/fields.ts @@ -81,7 +81,10 @@ const setGasPriceField = (payload: SetGasPriceFieldAction['payload']): SetGasPri }); type TReset = typeof reset; -const reset = (): ResetAction => ({ type: TypeKeys.RESET }); +const reset = (payload: ResetAction['payload'] = { include: {}, exclude: {} }): ResetAction => ({ + type: TypeKeys.RESET, + payload +}); export { TInputGasLimit, diff --git a/common/actions/transaction/actionTypes/actionTypes.ts b/common/actions/transaction/actionTypes/actionTypes.ts index 2bc7200b..67a3914e 100644 --- a/common/actions/transaction/actionTypes/actionTypes.ts +++ b/common/actions/transaction/actionTypes/actionTypes.ts @@ -7,6 +7,9 @@ import { SignAction } from './sign'; import { SwapAction } from './swap'; import { CurrentAction } from './current'; import { SendEverythingAction } from './sendEverything'; +import { State as FieldState } from 'reducers/transaction/fields'; +import { State as MetaState } from 'reducers/transaction/meta'; +import { State as SignState } from 'reducers/transaction/sign'; export * from './broadcast'; export * from './fields'; @@ -19,6 +22,18 @@ export * from './sendEverything'; export interface ResetAction { type: TypeKeys.RESET; + payload: { + include: { + fields?: (keyof FieldState)[]; + meta?: (keyof MetaState)[]; + sign?: (keyof SignState)[]; + }; + exclude: { + fields?: (keyof FieldState)[]; + meta?: (keyof MetaState)[]; + sign?: (keyof SignState)[]; + }; + }; } export type TransactionAction = diff --git a/common/reducers/transaction/fields/fields.ts b/common/reducers/transaction/fields/fields.ts index a4e065a2..461254ad 100644 --- a/common/reducers/transaction/fields/fields.ts +++ b/common/reducers/transaction/fields/fields.ts @@ -11,6 +11,7 @@ import { import { Reducer } from 'redux'; import { State } from './typings'; import { gasPricetoBase } from 'libs/units'; +import { resetHOF } from 'reducers/transaction/shared'; const INITIAL_STATE: State = { to: { raw: '', value: null }, @@ -49,7 +50,7 @@ const tokenToToken = ( { payload: { decimal: _, tokenValue: __, ...rest } }: SwapTokenToTokenAction ): State => ({ ...state, ...rest }); -const reset = (state: State): State => ({ ...INITIAL_STATE, gasPrice: state.gasPrice }); +const reset = resetHOF('fields', INITIAL_STATE); export const fields = ( state: State = INITIAL_STATE, @@ -75,7 +76,7 @@ export const fields = ( case TK.TOKEN_TO_TOKEN_SWAP: return tokenToToken(state, action); case TK.RESET: - return reset(state); + return reset(state, action); default: return state; } diff --git a/common/reducers/transaction/meta/meta.ts b/common/reducers/transaction/meta/meta.ts index ee52b491..0c4612ee 100644 --- a/common/reducers/transaction/meta/meta.ts +++ b/common/reducers/transaction/meta/meta.ts @@ -12,6 +12,7 @@ import { NetworkAction } from 'actions/transaction'; import { Reducer } from 'redux'; +import { resetHOF } from 'reducers/transaction/shared'; const INITIAL_STATE: State = { unit: '', @@ -53,7 +54,7 @@ const tokenToToken = ( { payload: { data: _, to: __, ...rest } }: SwapTokenToTokenAction ): State => ({ ...state, ...rest }); -const reset = () => INITIAL_STATE; +const reset = resetHOF('meta', INITIAL_STATE); const unitMeta = (state: State, { payload }: SetUnitMetaAction): State => ({ ...state, @@ -81,7 +82,7 @@ export const meta = ( case TK.TOKEN_TO_TOKEN_SWAP: return tokenToToken(state, action); case TK.RESET: - return reset(); + return reset(state, action); default: return state; } diff --git a/common/reducers/transaction/shared.ts b/common/reducers/transaction/shared.ts new file mode 100644 index 00000000..077c2a3a --- /dev/null +++ b/common/reducers/transaction/shared.ts @@ -0,0 +1,37 @@ +import { State } from './transaction'; +import { Omit } from 'react-redux'; +import { ResetAction } from 'actions/transaction'; + +export const resetHOF = ( + reducerName: keyof (Omit), + initialState: State[typeof reducerName], + returnCb?: ( + state: State[typeof reducerName], + returnedState: State[typeof reducerName] + ) => State[typeof reducerName] +) => (state: State[typeof reducerName], { payload: { exclude, include } }: ResetAction) => { + const excludeFields = exclude[reducerName]; + const includeFields = include[reducerName]; + + // sanity check + if (includeFields && excludeFields) { + throw Error('Cant have include and exclude fields at the same time'); + } + const returnState = { ...initialState }; + const stateCopy = { ...state }; + + if (includeFields) { + (includeFields as any[]).forEach(fieldName => { + stateCopy[fieldName] = returnState[fieldName]; + }); + return returnCb ? returnCb(state, returnState) : { ...stateCopy }; + } + + if (excludeFields) { + (excludeFields as any[]).forEach(fieldName => { + returnState[fieldName] = state[fieldName]; + }); + } + + return returnCb ? returnCb(state, returnState) : returnState; +}; diff --git a/common/reducers/transaction/sign/sign.ts b/common/reducers/transaction/sign/sign.ts index cb42ca0a..7b625d48 100644 --- a/common/reducers/transaction/sign/sign.ts +++ b/common/reducers/transaction/sign/sign.ts @@ -6,6 +6,7 @@ import { SignAction, ResetAction } from 'actions/transaction'; +import { resetHOF } from 'reducers/transaction/shared'; const INITIAL_STATE: State = { local: { signedTransaction: null }, @@ -43,7 +44,7 @@ const signWeb3TranscationSucceeded = ( const signTransactionFailed = () => INITIAL_STATE; -const reset = () => INITIAL_STATE; +const reset = resetHOF('sign', INITIAL_STATE); export const sign = (state: State = INITIAL_STATE, action: SignAction | ResetAction) => { switch (action.type) { @@ -56,7 +57,7 @@ export const sign = (state: State = INITIAL_STATE, action: SignAction | ResetAct case TK.SIGN_TRANSACTION_FAILED: return signTransactionFailed(); case TK.RESET: - return reset(); + return reset(state, action); default: return state; } diff --git a/common/sagas/transaction/index.ts b/common/sagas/transaction/index.ts index bacc1a0b..0e40c05b 100644 --- a/common/sagas/transaction/index.ts +++ b/common/sagas/transaction/index.ts @@ -7,7 +7,7 @@ import { meta } from './meta'; import { network } from './network'; import { signing } from './signing'; import { sendEverything } from './sendEverything'; -import { reset, setDefaultUnit } from './reset'; +import { reset } from './reset'; export function* transaction(): SagaIterator { yield all([ @@ -18,7 +18,6 @@ export function* transaction(): SagaIterator { ...network, ...signing, ...sendEverything, - ...reset, - setDefaultUnit + ...reset ]); } diff --git a/common/sagas/transaction/reset.ts b/common/sagas/transaction/reset.ts index e96fabf6..1eab8d40 100644 --- a/common/sagas/transaction/reset.ts +++ b/common/sagas/transaction/reset.ts @@ -1,10 +1,11 @@ import { SagaIterator } from 'redux-saga'; -import { TypeKeys } from 'actions/wallet'; -import { takeEvery, put, select } from 'redux-saga/effects'; +import { TypeKeys as WalletTypeKeys } from 'actions/wallet'; +import { takeEvery, put, take, race, fork, select } from 'redux-saga/effects'; import { - reset as resetActionCreator, setUnitMeta, - TypeKeys as Constants + TypeKeys as TransactionTypeKeys, + reset as resetActionCreator, + ResetAction } from 'actions/transaction'; import { getNetworkUnit } from 'selectors/config'; @@ -12,11 +13,68 @@ export function* resetTransactionState(): SagaIterator { yield put(resetActionCreator()); } -export function* setNetworkUnit(): SagaIterator { +/** + * After a transaction is signed, wait for any action that would result in the transaction state changing then fire off + * a handler that will remove the current serialized transaction so the user does not send a stale transaction + */ +export function* watchTransactionState(): SagaIterator { + while (true) { + // wait for transaction to be signed + yield take([ + TransactionTypeKeys.SIGN_LOCAL_TRANSACTION_SUCCEEDED, + TransactionTypeKeys.SIGN_WEB3_TRANSACTION_SUCCEEDED + ]); + + const { bail } = yield race({ + bail: take([ + TransactionTypeKeys.RESET, + WalletTypeKeys.WALLET_RESET, + WalletTypeKeys.WALLET_RESET + ]), // bail on actions that would wipe state + wipeState: take([ + TransactionTypeKeys.CURRENT_TO_SET, + TransactionTypeKeys.CURRENT_VALUE_SET, + TransactionTypeKeys.GAS_LIMIT_FIELD_SET, + TransactionTypeKeys.GAS_PRICE_FIELD_SET, + TransactionTypeKeys.VALUE_FIELD_SET, + TransactionTypeKeys.DATA_FIELD_SET, + TransactionTypeKeys.NONCE_FIELD_SET, + TransactionTypeKeys.TO_FIELD_SET, + TransactionTypeKeys.TOKEN_TO_META_SET, + TransactionTypeKeys.TOKEN_VALUE_META_SET, + TransactionTypeKeys.UNIT_META_SET + ]) // watch for any actions that would change transaction state + }); + + if (bail) { + continue; + } + + yield put( + resetActionCreator({ + exclude: { + fields: ['data', 'gasLimit', 'gasPrice', 'nonce', 'to', 'value'], + meta: ['decimal', 'from', 'previousUnit', 'tokenTo', 'tokenValue', 'unit'] + }, + include: {} + }) + ); + } +} + +export function* setNetworkUnit({ payload: { exclude, include } }: ResetAction): SagaIterator { + if (exclude.meta && exclude.meta.includes('unit')) { + return; + } + if (include.meta && !include.meta.includes('unit')) { + return; + } const networkUnit = yield select(getNetworkUnit); yield put(setUnitMeta(networkUnit)); } -export const setDefaultUnit = takeEvery(Constants.RESET, setNetworkUnit); - -export const reset = [takeEvery([TypeKeys.WALLET_RESET], resetTransactionState)]; +export const reset = [ + takeEvery([WalletTypeKeys.WALLET_RESET], resetTransactionState), + fork(watchTransactionState), + takeEvery(TransactionTypeKeys.RESET, setNetworkUnit) +];