Save entire swap info instead of just the address.

- Users should be able to see the entered and promised amount, otherwise they might forget how much to send over.
- This change also prepares for the future upgrade to support multiple coins.
This commit is contained in:
infinite-persistence 2021-04-04 15:10:55 +08:00 committed by Sean Yesmunt
parent 9c808e2b5e
commit b2630f6f21
10 changed files with 97 additions and 77 deletions

View file

@ -1,10 +1,27 @@
declare type CoinSwapInfo = {
coin: string,
sendAddress: string,
sendAmount: number,
lbcAmount: number,
}
declare type CoinSwapState = { declare type CoinSwapState = {
btcAddresses: Array<string> coinSwaps: Array<CoinSwapInfo>
}; };
declare type CoinSwapAction = { declare type CoinSwapAction = {
type: string, type: string,
data: { data: {
btcAddress: string, coin: string,
sendAddress: string,
sendAmount: number,
lbcAmount: number,
},
};
declare type CoinSwapRemoveAction = {
type: string,
data: {
sendAddress: string,
}, },
}; };

View file

@ -2,20 +2,20 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import WalletSwap from './view'; import WalletSwap from './view';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { doAddBtcAddress } from 'redux/actions/coinSwap'; import { doAddCoinSwap } from 'redux/actions/coinSwap';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { selectBtcAddresses } from 'redux/selectors/coinSwap'; import { selectCoinSwaps } from 'redux/selectors/coinSwap';
import { selectReceiveAddress, doGetNewAddress, doCheckAddressIsMine } from 'lbry-redux'; import { selectReceiveAddress, doGetNewAddress, doCheckAddressIsMine } from 'lbry-redux';
const select = (state, props) => ({ const select = (state, props) => ({
receiveAddress: selectReceiveAddress(state), receiveAddress: selectReceiveAddress(state),
btcAddresses: selectBtcAddresses(state), coinSwaps: selectCoinSwaps(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
doToast: (options) => dispatch(doToast(options)), doToast: (options) => dispatch(doToast(options)),
doAddBtcAddress: (address) => dispatch(doAddBtcAddress(address)), addCoinSwap: (coinSwap) => dispatch(doAddCoinSwap(coinSwap)),
getNewAddress: () => dispatch(doGetNewAddress()), getNewAddress: () => dispatch(doGetNewAddress()),
checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)), checkAddressIsMine: (address) => dispatch(doCheckAddressIsMine(address)),
}); });

View file

@ -47,24 +47,16 @@ const NAG_RATE_CALL_FAILED = 'Unable to obtain exchange rate. Try again later.';
type Props = { type Props = {
receiveAddress: string, receiveAddress: string,
btcAddresses: Array<string>, coinSwaps: Array<CoinSwapInfo>,
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
doAddBtcAddress: (string) => void, addCoinSwap: (CoinSwapInfo) => void,
getNewAddress: () => void, getNewAddress: () => void,
checkAddressIsMine: (string) => void, checkAddressIsMine: (string) => void,
openModal: (string, {}) => void, openModal: (string, {}) => void,
}; };
function WalletSwap(props: Props) { function WalletSwap(props: Props) {
const { const { receiveAddress, doToast, coinSwaps, addCoinSwap, getNewAddress, checkAddressIsMine, openModal } = props;
receiveAddress,
doToast,
btcAddresses,
doAddBtcAddress,
getNewAddress,
checkAddressIsMine,
openModal,
} = props;
const [btc, setBtc] = usePersistedState('swap-btc-amount', 0.001); const [btc, setBtc] = usePersistedState('swap-btc-amount', 0.001);
const [btcError, setBtcError] = React.useState(); const [btcError, setBtcError] = React.useState();
@ -92,9 +84,9 @@ function WalletSwap(props: Props) {
setBtcAddress(null); setBtcAddress(null);
} }
function removeBtcAddress(btcAddress) { function removeCoinSwap(sendAddress) {
openModal(MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS, { openModal(MODALS.CONFIRM_REMOVE_BTC_SWAP_ADDRESS, {
btcAddress: btcAddress, sendAddress: sendAddress,
}); });
} }
@ -236,7 +228,12 @@ function WalletSwap(props: Props) {
}) })
.then((result) => { .then((result) => {
setBtcAddress(result); setBtcAddress(result);
doAddBtcAddress(result); addCoinSwap({
coin: 'btc',
sendAddress: result,
sendAmount: btc,
lbcAmount: lbc,
});
}) })
.catch((err) => { .catch((err) => {
setNag({ msg: err === INTERNAL_APIS_DOWN ? NAG_SWAP_CALL_FAILED : err.message, type: 'error' }); setNag({ msg: err === INTERNAL_APIS_DOWN ? NAG_SWAP_CALL_FAILED : err.message, type: 'error' });
@ -259,8 +256,8 @@ function WalletSwap(props: Props) {
setNag(null); setNag(null);
setIsRefreshingStatus(true); setIsRefreshingStatus(true);
btcAddresses.forEach((x) => { coinSwaps.forEach((x) => {
queryStatus(x, null, null); queryStatus(x.sendAddress, null, null);
}); });
} }
@ -357,9 +354,7 @@ function WalletSwap(props: Props) {
disabled={isSwapping || isNaN(btc) || btc === 0 || lbc === 0 || btcError} disabled={isSwapping || isNaN(btc) || btc === 0 || lbc === 0 || btcError}
label={isSwapping ? __('Processing...') : __('Start Swap')} label={isSwapping ? __('Processing...') : __('Start Swap')}
/> />
{btcAddresses.length !== 0 && ( {coinSwaps.length !== 0 && <Button button="link" label={__('View Past Swaps')} onClick={handleViewPastSwaps} />}
<Button button="link" label={__('View Past Swaps')} onClick={handleViewPastSwaps} />
)}
</div> </div>
</> </>
); );
@ -424,37 +419,36 @@ function WalletSwap(props: Props) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{btcAddresses.length === 0 && ( {coinSwaps.length === 0 && (
<tr> <tr>
<td>{'---'}</td> <td>{'---'}</td>
</tr> </tr>
)} )}
{btcAddresses.length !== 0 && {coinSwaps.length !== 0 &&
btcAddresses.map((x) => { coinSwaps.map((x) => {
const shortBtcAddress = x.substring(0, 7);
return ( return (
<tr key={x}> <tr key={x.sendAddress}>
<td> <td>
<Button <Button
button="link" button="link"
className="button--hash-id" className="button--hash-id"
title={x} title={x.sendAddress}
label={shortBtcAddress} label={x.sendAddress.substring(0, 7)}
onClick={() => { onClick={() => {
clipboard.writeText(x); clipboard.writeText(x.sendAddress);
doToast({ doToast({
message: __('Address copied.'), message: __('Address copied.'),
}); });
}} }}
/> />
</td> </td>
<td>{isRefreshingStatus ? '...' : getStatusStr(x)}</td> <td>{isRefreshingStatus ? '...' : getStatusStr(x.sendAddress)}</td>
<td> <td>
<Button <Button
button="link" button="link"
icon={ICONS.REMOVE} icon={ICONS.REMOVE}
title={__('Remove address')} title={__('Remove address')}
onClick={() => removeBtcAddress(x)} onClick={() => removeCoinSwap(x.sendAddress)}
/> />
</td> </td>
</tr> </tr>
@ -467,7 +461,7 @@ function WalletSwap(props: Props) {
</div> </div>
<div className="section__actions"> <div className="section__actions">
<Button autoFocus onClick={handleCancelPending} button="primary" label={__('Go Back')} /> <Button autoFocus onClick={handleCancelPending} button="primary" label={__('Go Back')} />
{btcAddresses.length !== 0 && !isRefreshingStatus && ( {coinSwaps.length !== 0 && !isRefreshingStatus && (
<Button button="link" label={__('Refresh')} onClick={handleViewPastSwaps} /> <Button button="link" label={__('Refresh')} onClick={handleViewPastSwaps} />
)} )}
{isRefreshingStatus && <Spinner type="small" />} {isRefreshingStatus && <Spinner type="small" />}

View file

@ -283,8 +283,8 @@ export const COMMENT_RECEIVED = 'COMMENT_RECEIVED';
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL'; export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
// Coin swap // Coin swap
export const ADD_BTC_ADDRESS = 'ADD_BTC_ADDRESS'; export const ADD_COIN_SWAP = 'ADD_COIN_SWAP';
export const REMOVE_BTC_ADDRESS = 'REMOVE_BTC_ADDRESS'; export const REMOVE_COIN_SWAP = 'REMOVE_COIN_SWAP';
// Tags // Tags
export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW'; export const TOGGLE_TAG_FOLLOW = 'TOGGLE_TAG_FOLLOW';

View file

@ -1,12 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import ModalRemoveBtcSwapAddress from './view'; import ModalRemoveBtcSwapAddress from './view';
import { doRemoveBtcAddress } from 'redux/actions/coinSwap'; import { doRemoveCoinSwap } from 'redux/actions/coinSwap';
const select = (state, props) => ({}); const select = (state, props) => ({});
const perform = (dispatch) => ({ const perform = (dispatch) => ({
doRemoveBtcAddress: (btcAddress) => dispatch(doRemoveBtcAddress(btcAddress)), removeCoinSwap: (sendAddress) => dispatch(doRemoveCoinSwap(sendAddress)),
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
}); });

View file

@ -6,19 +6,19 @@ import Card from 'component/common/card';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
type Props = { type Props = {
btcAddress: string, sendAddress: string,
doRemoveBtcAddress: (string) => void, removeCoinSwap: (string) => void,
closeModal: () => void, closeModal: () => void,
}; };
function ModalRemoveBtcSwapAddress(props: Props) { function ModalRemoveBtcSwapAddress(props: Props) {
const { btcAddress, doRemoveBtcAddress, closeModal } = props; const { sendAddress, removeCoinSwap, closeModal } = props;
return ( return (
<Modal isOpen contentLabel={__('Confirm Address Removal')} type="card" onAborted={closeModal}> <Modal isOpen contentLabel={__('Confirm Address Removal')} type="card" onAborted={closeModal}>
<Card <Card
title={__('Remove BTC Swap Address')} title={__('Remove BTC Swap Address')}
subtitle={<I18nMessage tokens={{ btc_address: <em>{`${btcAddress}`}</em> }}>Remove %btc_address%?</I18nMessage>} subtitle={<I18nMessage tokens={{ address: <em>{`${sendAddress}`}</em> }}>Remove %address%?</I18nMessage>}
body={<p className="help--warning">{__('This process cannot be reversed.')}</p>} body={<p className="help--warning">{__('This process cannot be reversed.')}</p>}
actions={ actions={
<> <>
@ -27,7 +27,7 @@ function ModalRemoveBtcSwapAddress(props: Props) {
button="primary" button="primary"
label={__('OK')} label={__('OK')}
onClick={() => { onClick={() => {
doRemoveBtcAddress(btcAddress); removeCoinSwap(sendAddress);
closeModal(); closeModal();
}} }}
/> />

View file

@ -3,7 +3,7 @@ import * as ACTIONS from 'constants/action_types';
import { selectPrefsReady } from 'redux/selectors/sync'; import { selectPrefsReady } from 'redux/selectors/sync';
import { doAlertWaitingForSync } from 'redux/actions/app'; import { doAlertWaitingForSync } from 'redux/actions/app';
export const doAddBtcAddress = (btcAddress: string) => (dispatch: Dispatch, getState: GetState) => { export const doAddCoinSwap = (coinSwap: CoinSwapInfo) => (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const ready = selectPrefsReady(state); const ready = selectPrefsReady(state);
@ -12,14 +12,17 @@ export const doAddBtcAddress = (btcAddress: string) => (dispatch: Dispatch, getS
} }
dispatch({ dispatch({
type: ACTIONS.ADD_BTC_ADDRESS, type: ACTIONS.ADD_COIN_SWAP,
data: { data: {
btcAddress, coin: coinSwap.coin,
sendAddress: coinSwap.sendAddress,
sendAmount: coinSwap.sendAmount,
lbcAmount: coinSwap.lbcAmount,
}, },
}); });
}; };
export const doRemoveBtcAddress = (btcAddress: string) => (dispatch: Dispatch, getState: GetState) => { export const doRemoveCoinSwap = (sendAddress: string) => (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const ready = selectPrefsReady(state); const ready = selectPrefsReady(state);
@ -28,9 +31,9 @@ export const doRemoveBtcAddress = (btcAddress: string) => (dispatch: Dispatch, g
} }
dispatch({ dispatch({
type: ACTIONS.REMOVE_BTC_ADDRESS, type: ACTIONS.REMOVE_COIN_SWAP,
data: { data: {
btcAddress, sendAddress,
}, },
}); });
}; };

View file

@ -4,41 +4,47 @@ import { ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
import { handleActions } from 'util/redux-utils'; import { handleActions } from 'util/redux-utils';
const defaultState: CoinSwapState = { const defaultState: CoinSwapState = {
btcAddresses: [], coinSwaps: [],
}; };
export default handleActions( export default handleActions(
{ {
[ACTIONS.ADD_BTC_ADDRESS]: (state: CoinSwapState, action: CoinSwapAction): CoinSwapState => { [ACTIONS.ADD_COIN_SWAP]: (state: CoinSwapState, action: CoinSwapAction): CoinSwapState => {
const { btcAddresses } = state; const { coinSwaps } = state;
const { btcAddress } = action.data; const { coin, sendAddress, sendAmount, lbcAmount } = action.data;
let newBtcAddresses = btcAddresses.slice();
if (!newBtcAddresses.includes(btcAddress)) { let newCoinSwaps = coinSwaps.slice();
newBtcAddresses.push(btcAddress); if (!newCoinSwaps.find((x) => x.sendAddress === sendAddress)) {
newCoinSwaps.push({
coin: coin,
sendAddress: sendAddress,
sendAmount: sendAmount,
lbcAmount: lbcAmount,
});
} }
return { return {
btcAddresses: newBtcAddresses, coinSwaps: newCoinSwaps,
}; };
}, },
[ACTIONS.REMOVE_BTC_ADDRESS]: (state: CoinSwapState, action: CoinSwapAction): CoinSwapState => { [ACTIONS.REMOVE_COIN_SWAP]: (state: CoinSwapState, action: CoinSwapRemoveAction): CoinSwapState => {
const { btcAddresses } = state; const { coinSwaps } = state;
const { btcAddress } = action.data; const { sendAddress } = action.data;
let newBtcAddresses = btcAddresses.slice(); let newCoinSwaps = coinSwaps.slice();
newBtcAddresses = newBtcAddresses.filter((x) => x !== btcAddress); newCoinSwaps = newCoinSwaps.filter((x) => x.sendAddress !== sendAddress);
return { return {
btcAddresses: newBtcAddresses, coinSwaps: newCoinSwaps,
}; };
}, },
[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: ( [LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: (
state: CoinSwapState, state: CoinSwapState,
action: { data: { btcAddresses: ?Array<string> } } action: { data: { coinSwaps: ?Array<CoinSwapInfo> } }
) => { ) => {
const { btcAddresses } = action.data; const { coinSwaps } = action.data;
const sanitizedBtcAddresses = btcAddresses && btcAddresses.filter((e) => typeof e === 'string'); const sanitizedCoinSwaps = coinSwaps && coinSwaps.filter((x) => typeof x.sendAddress === 'string');
return { return {
...state, ...state,
btcAddresses: coinSwaps: sanitizedCoinSwaps && sanitizedCoinSwaps.length ? sanitizedCoinSwaps : state.coinSwaps,
sanitizedBtcAddresses && sanitizedBtcAddresses.length ? sanitizedBtcAddresses : state.btcAddresses,
}; };
}, },
}, },

View file

@ -3,6 +3,6 @@ import { createSelector } from 'reselect';
const selectState = (state: { coinSwap: CoinSwapState }) => state.coinSwap || {}; const selectState = (state: { coinSwap: CoinSwapState }) => state.coinSwap || {};
export const selectBtcAddresses = createSelector(selectState, (state: CoinSwapState) => { export const selectCoinSwaps = createSelector(selectState, (state: CoinSwapState) => {
return state.btcAddresses.filter((x) => typeof x === 'string'); return state.coinSwaps.filter((x) => typeof x.sendAddress === 'string');
}); });

View file

@ -66,7 +66,7 @@ const searchFilter = createFilter('search', ['options']);
const tagsFilter = createFilter('tags', ['followedTags']); const tagsFilter = createFilter('tags', ['followedTags']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']); const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
const blockedFilter = createFilter('blocked', ['blockedChannels']); const blockedFilter = createFilter('blocked', ['blockedChannels']);
const btcAddressesFilter = createFilter('coinSwap', ['btcAddresses']); const coinSwapsFilter = createFilter('coinSwap', ['coinSwaps']);
const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']); const settingsFilter = createBlacklistFilter('settings', ['loadedLanguages', 'language']);
const whiteListedReducers = [ const whiteListedReducers = [
'fileInfo', 'fileInfo',
@ -86,7 +86,7 @@ const transforms = [
fileInfoFilter, fileInfoFilter,
walletFilter, walletFilter,
blockedFilter, blockedFilter,
btcAddressesFilter, coinSwapsFilter,
tagsFilter, tagsFilter,
appFilter, appFilter,
searchFilter, searchFilter,
@ -122,8 +122,8 @@ const triggerSharedStateActions = [
ACTIONS.CHANNEL_SUBSCRIBE, ACTIONS.CHANNEL_SUBSCRIBE,
ACTIONS.CHANNEL_UNSUBSCRIBE, ACTIONS.CHANNEL_UNSUBSCRIBE,
ACTIONS.TOGGLE_BLOCK_CHANNEL, ACTIONS.TOGGLE_BLOCK_CHANNEL,
ACTIONS.ADD_BTC_ADDRESS, ACTIONS.ADD_COIN_SWAP,
ACTIONS.REMOVE_BTC_ADDRESS, ACTIONS.REMOVE_COIN_SWAP,
ACTIONS.TOGGLE_TAG_FOLLOW, ACTIONS.TOGGLE_TAG_FOLLOW,
LBRY_REDUX_ACTIONS.CREATE_CHANNEL_COMPLETED, LBRY_REDUX_ACTIONS.CREATE_CHANNEL_COMPLETED,
ACTIONS.SYNC_CLIENT_SETTINGS, ACTIONS.SYNC_CLIENT_SETTINGS,
@ -156,7 +156,7 @@ const sharedStateFilters = {
property: 'following', property: 'following',
}, },
blocked: { source: 'blocked', property: 'blockedChannels' }, blocked: { source: 'blocked', property: 'blockedChannels' },
btc_addresses: { source: 'coinSwap', property: 'btcAddresses' }, coin_swaps: { source: 'coinSwap', property: 'coinSwaps' },
settings: { source: 'settings', property: 'sharedPreferences' }, settings: { source: 'settings', property: 'sharedPreferences' },
app_welcome_version: { source: 'app', property: 'welcomeVersion' }, app_welcome_version: { source: 'app', property: 'welcomeVersion' },
sharing_3P: { source: 'app', property: 'allowAnalytics' }, sharing_3P: { source: 'app', property: 'allowAnalytics' },