From 0432db8f6bdd2b5572deb20e800c532ff2337d62 Mon Sep 17 00:00:00 2001 From: Milan Date: Tue, 30 Aug 2022 12:45:27 +0200 Subject: [PATCH 1/5] feat(send flow): implemented UI changes on flow added a feature flag and some initial work with real sending amount --- config/mocks/wallet-options-v4.json | 1 + .../Form/CoinAccountOption/index.tsx | 119 ++++ .../src/data/coins/model/send.ts | 4 +- .../src/data/components/sagaRegister.ts | 2 +- .../src/data/components/sagas.ts | 2 +- .../components/sendCrypto/sagaRegister.ts | 4 +- .../src/data/components/sendCrypto/sagas.ts | 158 +++++- .../data/components/sendCrypto/selectors.ts | 2 + .../src/data/components/sendCrypto/types.ts | 5 +- .../modals/SendCrypto/AccountSelect/index.tsx | 139 +++++ .../SendCrypto/AccountSelect/selectors.ts | 38 ++ .../src/modals/SendCrypto/Confirm/index.tsx | 300 ++++++----- .../SendCrypto/EnterAmountNew/index.tsx | 510 ++++++++++++++++++ .../SendCrypto/EnterAmountNew/validation.tsx | 91 ++++ .../src/modals/SendCrypto/Status/index.tsx | 35 +- .../SendCrypto/TransactionOverview/index.tsx | 191 +++++++ .../src/modals/SendCrypto/index.tsx | 25 +- .../src/modals/SendCrypto/selectors.ts | 8 +- .../src/modals/SendCrypto/types.ts | 1 + 19 files changed, 1472 insertions(+), 163 deletions(-) create mode 100644 packages/blockchain-wallet-v4-frontend/src/components/Form/CoinAccountOption/index.tsx create mode 100644 packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/index.tsx create mode 100644 packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/selectors.ts create mode 100644 packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/EnterAmountNew/index.tsx create mode 100644 packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/EnterAmountNew/validation.tsx create mode 100644 packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/TransactionOverview/index.tsx diff --git a/config/mocks/wallet-options-v4.json b/config/mocks/wallet-options-v4.json index 097449b5862..1062db5a7c9 100644 --- a/config/mocks/wallet-options-v4.json +++ b/config/mocks/wallet-options-v4.json @@ -63,6 +63,7 @@ "isStakingEnabled": true, "mergeAndUpgrade": false, "nftExplorer": true, + "newSendFlow": true, "recurringBuys": true, "rewardsFlowUnderSwapEnabled": true, "rewardsPromoBanner": true, diff --git a/packages/blockchain-wallet-v4-frontend/src/components/Form/CoinAccountOption/index.tsx b/packages/blockchain-wallet-v4-frontend/src/components/Form/CoinAccountOption/index.tsx new file mode 100644 index 00000000000..ae3c07b8a41 --- /dev/null +++ b/packages/blockchain-wallet-v4-frontend/src/components/Form/CoinAccountOption/index.tsx @@ -0,0 +1,119 @@ +import React from 'react' +import styled, { css, DefaultTheme } from 'styled-components' + +import { FiatType } from '@core/types' +import { CoinAccountIcon, Text } from 'blockchain-info-components' +import CoinDisplay from 'components/Display/CoinDisplay' +import FiatDisplay from 'components/Display/FiatDisplay' +import { SwapAccountType } from 'data/types' + +const Option = styled.div<{ displayOnly?: boolean }>` + display: flex; + align-items: center; + justify-content: space-between; + border-top: ${(props) => `1px solid ${props.theme.grey000}`}; + padding: 16px 40px; + &:first-child { + border-top: 0; + } + + ${(props) => + !props.displayOnly && + css` + cursor: pointer; + &:hover { + background-color: ${(props) => props.theme.blue000}; + } + `} +` + +const OptionTitle = styled(Text)` + color: ${(props) => props.theme.grey800}; + font-weight: 600; + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +` +const OptionValue = styled(Text)<{ + color?: keyof DefaultTheme + weight?: number +}>` + color: ${(props) => props.color || props.theme.grey600}; + margin-top: 4px; + font-weight: ${(props) => (props.weight ? props.weight : 600)}; + font-size: 14px; +` +const BalanceRow = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; +` +const FlexStartRow = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; +` + +const CoinAccountOption: React.FC = (props) => { + const { account, coin, displayOnly, walletCurrency } = props + + return ( + + ) +} + +type Props = { + account: SwapAccountType + coin: string + displayOnly?: boolean + onClick?: () => void + walletCurrency: FiatType +} + +export default CoinAccountOption diff --git a/packages/blockchain-wallet-v4-frontend/src/data/coins/model/send.ts b/packages/blockchain-wallet-v4-frontend/src/data/coins/model/send.ts index 86623cf13fc..60ad53929cd 100644 --- a/packages/blockchain-wallet-v4-frontend/src/data/coins/model/send.ts +++ b/packages/blockchain-wallet-v4-frontend/src/data/coins/model/send.ts @@ -2,7 +2,7 @@ import { CoinAccountSelectorType } from 'data/coins/types' // used in the coin/account selector in send flyout (modals/SendCrypto) export const SEND_ACCOUNTS_SELECTOR: CoinAccountSelectorType = { - importedAddresses: false, - nonCustodialAccounts: false, + importedAddresses: true, + nonCustodialAccounts: true, tradingAccounts: true } diff --git a/packages/blockchain-wallet-v4-frontend/src/data/components/sagaRegister.ts b/packages/blockchain-wallet-v4-frontend/src/data/components/sagaRegister.ts index c7a413bfe03..c332bdcdbe2 100644 --- a/packages/blockchain-wallet-v4-frontend/src/data/components/sagaRegister.ts +++ b/packages/blockchain-wallet-v4-frontend/src/data/components/sagaRegister.ts @@ -66,7 +66,7 @@ export default ({ api, coreSagas, networks }) => yield fork(recurringBuy({ api })) yield fork(resetWallet2fa({ api })) yield fork(send({ api, coreSagas, networks })) - yield fork(sendCrypto({ api })) + yield fork(sendCrypto({ api, coreSagas, networks })) yield fork(sendBch({ api, coreSagas, networks })) yield fork(sendBtc({ api, coreSagas, networks })) yield fork(sendEth({ api, coreSagas, networks })) diff --git a/packages/blockchain-wallet-v4-frontend/src/data/components/sagas.ts b/packages/blockchain-wallet-v4-frontend/src/data/components/sagas.ts index b8dee394cb4..2bfa6ff72c7 100644 --- a/packages/blockchain-wallet-v4-frontend/src/data/components/sagas.ts +++ b/packages/blockchain-wallet-v4-frontend/src/data/components/sagas.ts @@ -64,7 +64,7 @@ export default ({ api, coreSagas, networks }) => ({ send: send({ api, coreSagas, networks }), sendBch: sendBch({ api, coreSagas, networks }), sendBtc: sendBtc({ api, coreSagas, networks }), - sendCrypto: sendCrypto({ api }), + sendCrypto: sendCrypto({ api, coreSagas, networks }), sendEth: sendEth({ api, coreSagas, networks }), sendXlm: sendXlm({ api, coreSagas, networks }), settings: settings({ api, coreSagas }), diff --git a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagaRegister.ts b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagaRegister.ts index 70dbc22aeef..a082a0802e2 100644 --- a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagaRegister.ts +++ b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagaRegister.ts @@ -5,8 +5,8 @@ import { actionTypes } from 'data/form/actionTypes' import sagas from './sagas' import { actions as A } from './slice' -export default ({ api }) => { - const sendCryptoSagas = sagas({ api }) +export default ({ api, coreSagas, networks }) => { + const sendCryptoSagas = sagas({ api, coreSagas, networks }) return function* brokerageSaga() { yield takeLatest(A.buildTx.type, sendCryptoSagas.buildTx) diff --git a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts index 04d2d8fa6a9..8ec684a8a3b 100644 --- a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts +++ b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts @@ -1,13 +1,16 @@ +import BigNumber from 'bignumber.js' import { SEND_FORM } from 'blockchain-wallet-v4-frontend/src/modals/SendCrypto/model' import { SendFormType } from 'blockchain-wallet-v4-frontend/src/modals/SendCrypto/types' import { call, delay, put, select } from 'redux-saga/effects' import secp256k1 from 'secp256k1' +import { Exchange } from '@core' import { convertCoinToCoin, convertFiatToCoin } from '@core/exchange' import { APIType } from '@core/network/api' import { BuildTxIntentType, BuildTxResponseType } from '@core/network/api/coin/types' import { getPrivKey, getPubKey } from '@core/redux/data/self-custody/sagas' -import { FiatType, WalletAccountEnum } from '@core/types' +import { ADDRESS_TYPES } from '@core/redux/payment/btc/utils' +import { BtcPaymentType, FiatType, PaymentValue, WalletAccountEnum } from '@core/types' import { errorHandler } from '@core/utils' import { actions, selectors } from 'data' import { SwapBaseCounterTypes } from 'data/components/swap/types' @@ -16,11 +19,26 @@ import { Analytics } from 'data/types' import { AccountType } from 'middleware/analyticsMiddleware/types' import { promptForSecondPassword } from 'services/sagas' +import sendSagas from '../send/sagas' import * as S from './selectors' import { actions as A } from './slice' import { SendCryptoStepType } from './types' -export default ({ api }: { api: APIType }) => { +const nonMigratedCoins = [ + 'BTC', + 'ETH' + // , 'BCH', 'ETH', 'XLM' +] + +export const logLocation = 'components/sendCrypto/sagas' + +export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; networks: any }) => { + const { showWithdrawalLockAlert } = sendSagas({ + api, + coreSagas, + networks + }) + const initializeSend = function* () { const totalBalanceR = yield select(selectors.balances.getTotalWalletBalanceNotFormatted) const totalBalance = totalBalanceR.getOrElse({ total: 0 }) @@ -32,6 +50,7 @@ export default ({ api }: { api: APIType }) => { } } + // TODO add a new if case for trading accounts BTC const buildTx = function* (action: ReturnType) { let coin let fee @@ -41,6 +60,11 @@ export default ({ api }: { api: APIType }) => { const { account, baseCryptoAmt, destination, fee, memo } = action.payload const { coin } = account const feesR = S.getWithdrawalFees(yield select(), coin) + if (nonMigratedCoins.includes(coin)) { + // console.log('coin to be processed differently - build TX', coin) + // TODO simulate buildTx logic for non migrated coins + return + } if (account.type === SwapBaseCounterTypes.ACCOUNT) { const password = yield call(promptForSecondPassword) @@ -124,6 +148,120 @@ export default ({ api }: { api: APIType }) => { } } + // helper function to send BTC amount + const submitBTCTransaction = function* () { + // TODO double check do we need initial part since here we already know a few things + + const p = S.getPayment(yield select())?.getOrElse({} as PaymentValue) + let payment: BtcPaymentType = coreSagas.payment.btc.create({ + network: networks.btc, + payment: p || ({} as PaymentValue) + }) + + const formValues = selectors.form.getFormValues(SEND_FORM)(yield select()) as SendFormType + const { amount, coin, fix, payPro, selectedAccount, to } = formValues + const { fromType } = payment.value() + try { + // Sign payment + let password + if (fromType !== ADDRESS_TYPES.WATCH_ONLY) { + password = yield call(promptForSecondPassword) + } + if (fromType !== ADDRESS_TYPES.CUSTODIAL) { + payment = yield payment.sign(password) + } + // Publish payment + if (payPro) { + // @ts-ignore + const { txHex, weightedSize } = payment.value() + const invoiceId = payPro.paymentUrl.split('/i/')[1] + yield call( + // @ts-ignore + api.verifyPaymentRequest, + invoiceId, + txHex, + weightedSize, + coin + ) + yield delay(3000) + yield call( + // @ts-ignore + api.submitPaymentRequest, + invoiceId, + txHex, + weightedSize, + coin + ) + } else if (fromType === ADDRESS_TYPES.CUSTODIAL) { + const value = payment.value() + if (!value.to) return + if (!value.amount) return + if (!value.selection) return + + yield call( + api.withdrawBSFunds, + value.to[0].address, + coin, + new BigNumber(value.amount[0]).toString(), + value.selection.fee + ) + } else { + const value = payment.value() + // notify backend of incoming non-custodial deposit + if (value.to && value.to[0].type === 'CUSTODIAL') { + yield put( + actions.components.send.notifyNonCustodialToCustodialTransfer(value, 'SIMPLEBUY') + ) + } + payment = yield payment.publish() + } + + yield put(actions.core.data.btc.fetchData()) + // yield put(A.sendBtcPaymentUpdatedSuccess(payment.value())) + // Set tx note + // if (path(['description', 'length'], payment.value())) { + // yield put( + // actions.core.wallet.setTransactionNote(payment.value().txId, payment.value().description) + // ) + // } + // Redirect to tx list, display success + // yield put(actions.router.push('/coins/BTC')) + // yield put( + // actions.alerts.displaySuccess(C.SEND_COIN_SUCCESS, { + // coinName: 'Bitcoin' + // }) + // ) + + const amt = payment.value().amount || [0] + const coinAmount = Exchange.convertCoinToCoin({ + coin, + value: amt.reduce((a, b) => a + b, 0) + }) + + // triggers email notification to user that + // non-custodial funds were sent from the wallet + if (fromType === ADDRESS_TYPES.ACCOUNT) { + yield put(actions.core.wallet.triggerNonCustodialSendAlert(coin, coinAmount)) + } + + // yield put(destroy(FORM)) + } catch (e) { + // yield put(stopSubmit(FORM)) + // Set errors + const error = errorHandler(e) + + if (fromType === ADDRESS_TYPES.CUSTODIAL && error) { + if (error === 'Pending withdrawal locks') { + yield call(showWithdrawalLockAlert) + } else { + yield put(actions.alerts.displayError(error)) + } + } else { + yield put(A.submitTransactionFailure(error)) + } + } + } + const fetchFeesAndMins = function* ({ payload }: ReturnType) { yield put(A.fetchWithdrawalFeesLoading()) try { @@ -177,6 +315,7 @@ export default ({ api }: { api: APIType }) => { if (form.includes('SEND') && field === 'coin') { yield put(actions.modals.closeAllModals()) + if ( (selectors.core.data.coins.getCustodialCoins().includes(payload) || selectors.core.data.coins.getDynamicSelfCustodyCoins().includes(payload)) && @@ -200,6 +339,13 @@ export default ({ api }: { api: APIType }) => { ) } } + + const formValues = selectors.form.getFormValues(SEND_FORM)(yield select()) as SendFormType + if (formValues && nonMigratedCoins.includes(formValues?.selectedAccount?.coin)) { + const { coin } = formValues.selectedAccount + // TODO add part for checking for change based on coin + // console.log('coin to be processed differently', coin) + } } const signTx = function* (prebuildTx: BuildTxResponseType, password: string) { @@ -247,6 +393,14 @@ export default ({ api }: { api: APIType }) => { .getRatesSelector(coin, yield select()) .getOrFail('Failed to get rates') + // check is selected account with coin in non migrated coins list and process it + if (nonMigratedCoins.includes(coin)) { + if (coin === 'BTC') { + call(submitBTCTransaction) + return + } + } + if (selectedAccount.type === SwapBaseCounterTypes.ACCOUNT) { const password = yield call(promptForSecondPassword) const guid = yield select(selectors.core.wallet.getGuid) diff --git a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/selectors.ts b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/selectors.ts index 2fa43d9e777..cf3391c2139 100644 --- a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/selectors.ts +++ b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/selectors.ts @@ -19,3 +19,5 @@ export const getWithdrawalMin = (state: RootState, coin: string) => ) export const getSendLimits = (state: RootState) => state.components.sendCrypto.sendLimits + +export const getPayment = (state: RootState) => state.components.sendCrypto.payment diff --git a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/types.ts b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/types.ts index 5f814be72fd..5594d1c3fab 100644 --- a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/types.ts +++ b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/types.ts @@ -1,6 +1,7 @@ import { BuildTxResponseType } from '@core/network/api/coin/types' import { CrossBorderLimits, + PaymentValue, RemoteDataType, WithdrawalLockResponseType, WithdrawalMinsAndFeesResponse, @@ -12,6 +13,7 @@ import { SwapAccountType } from 'data/types' export type SendCryptoState = { initialCoin?: string isValidAddress: RemoteDataType + payment?: RemoteDataType prebuildTx: RemoteDataType sendLimits: RemoteDataType step: SendCryptoStepType @@ -45,5 +47,6 @@ export enum SendCryptoStepType { 'ENTER_TO', 'ENTER_AMOUNT', 'CONFIRM', - 'STATUS' + 'STATUS', + 'TX_OVERVIEW' } diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/index.tsx b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/index.tsx new file mode 100644 index 00000000000..a75daa6e3ae --- /dev/null +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/index.tsx @@ -0,0 +1,139 @@ +import React from 'react' +import { FormattedMessage } from 'react-intl' +import { connect, ConnectedProps } from 'react-redux' +import { Icon, Padding } from '@blockchain-com/constellation' +import { IconCloseCircleV2 } from '@blockchain-com/icons' +import { bindActionCreators, compose } from 'redux' +import reduxForm, { InjectedFormProps } from 'redux-form/lib/reduxForm' +import styled from 'styled-components' + +import { Text } from 'blockchain-info-components' +import { FlyoutContainer, FlyoutContent } from 'components/Flyout/Layout' +import CoinAccountOption from 'components/Form/CoinAccountOption' +import { actions } from 'data' +import { SendCryptoStepType } from 'data/components/sendCrypto/types' + +import { Title } from '../../components' +import { Props as OwnProps } from '..' +import { SEND_FORM } from '../model' +import { getData } from './selectors' + +const HeaderWrapper = styled.div` + display: flex; + flex-direction: column; + max-width: 480px; + background-color: ${(props) => props.theme.white}; +` + +const HeaderRow = styled.div` + flex-direction: row; + justify-content: space-between; + display: flex; +` +const NoAccountsText = styled.div` + border-top: ${(props) => `1px solid ${props.theme.grey000}`}; + padding: 40px 40px 0; + text-align: center; +` +const InfoRow = styled.div` + background: rgba(240, 242, 247, 0.32); +` + +const CloseIconContainer = styled.div` + cursor: pointer; +` + +class SendAccountSelect extends React.PureComponent & Props> { + render() { + const { close, data, formActions, sendCryptoActions, showNewSendFlow, walletCurrency } = + this.props + return ( + + + + + + <FormattedMessage id='buttons.send' defaultMessage='Send' /> + + close()}> + + + + + + + + + + + + + + + + + + + + + {data.accounts.map((account) => ( + { + formActions.reset(SEND_FORM) + formActions.change(SEND_FORM, 'selectedAccount', account) + sendCryptoActions.setStep({ + step: showNewSendFlow + ? SendCryptoStepType.ENTER_AMOUNT + : SendCryptoStepType.ENTER_TO + }) + sendCryptoActions.fetchSendLimits({ account }) + }} + walletCurrency={walletCurrency} + /> + ))} + {data.accounts.length === 0 && ( + + + + + + )} + + + ) + } +} + +const mapStateToProps = (state, ownProps) => ({ + // coinList: selectors.balances.getTotalWalletBalancesSorted(state), + data: getData(state, ownProps) +}) + +const mapDispatchToProps = (dispatch) => ({ + modalActions: bindActionCreators(actions.modals, dispatch) +}) + +const connector = connect(mapStateToProps, mapDispatchToProps) +const enhance = compose( + reduxForm<{}, Props>({ + destroyOnUnmount: false, + form: SEND_FORM + }), + connector +) + +type Props = ConnectedProps & OwnProps + +export default enhance(SendAccountSelect) as React.ComponentType diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/selectors.ts b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/selectors.ts new file mode 100644 index 00000000000..552683907cd --- /dev/null +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/selectors.ts @@ -0,0 +1,38 @@ +import { map } from 'ramda' + +import { createDeepEqualSelector } from '@core/utils' +import { SEND_ACCOUNTS_SELECTOR } from 'data/coins/model/send' +import { getCoinAccounts } from 'data/coins/selectors' +import { CoinAccountSelectorType } from 'data/coins/types' +import { SwapAccountType } from 'data/components/swap/types' + +import { Props as OwnProps } from '..' + +export const getData = createDeepEqualSelector( + [ + (state, ownProps: OwnProps) => + getCoinAccounts(state, { + coins: ownProps.sendableCoins, + ...SEND_ACCOUNTS_SELECTOR + } as CoinAccountSelectorType), + (state, ownProps) => ({ ownProps, state }) + ], + (accounts, { ownProps }) => { + const prunedAccounts = [] as Array + const coinsToUse = map((coin) => coin.symbol, ownProps.coinList) + + // @ts-ignore + map( + (coin) => + map((acct: any) => { + if (coinsToUse.includes(acct.baseCoin) && Number(acct.balance) > 0) { + prunedAccounts.push(acct) + } + }, coin), + accounts + ) + + return { accounts: prunedAccounts } + } +) +export default getData diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/Confirm/index.tsx b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/Confirm/index.tsx index 08b8b3f4a22..36bfebef4cf 100644 --- a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/Confirm/index.tsx +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/Confirm/index.tsx @@ -58,7 +58,7 @@ const isInsufficientBalance = (e: string) => { } const Confirm: React.FC & Props> = (props) => { - const { formValues, rates, sendCryptoActions, walletCurrency } = props + const { formValues, rates, sendCryptoActions, showNewSendFlow, walletCurrency } = props const { amount, fee, fix, memo, selectedAccount, selectedAccount: account, to } = formValues const { coin } = selectedAccount @@ -96,159 +96,167 @@ const Confirm: React.FC & Props> = (props) => { sendCryptoActions.submitTransaction() }} > -
- - - { - if (isMax) { - props.formActions.change(SEND_FORM, 'amount', '0') - } - sendCryptoActions.setStep({ step: SendCryptoStepType.ENTER_AMOUNT }) - }} - name='arrow-back' - role='button' - color='grey600' - size='24px' - style={{ marginRight: '20px' }} - /> - - - - - - {props.prebuildTxR.cata({ - Failure: (e) => - isInsufficientBalance(e) ? ( - - props.sendCryptoActions.setStep({ step: SendCryptoStepType.ENTER_AMOUNT }) - } - handleMax={() => - sendCryptoActions.buildTx({ - account, - baseCryptoAmt: 'MAX', - destination: to, - fee, - fix, - memo, - rates, - walletCurrency - }) - } - /> + + + { + if (isMax) { + props.formActions.change(SEND_FORM, 'amount', '0') + } + sendCryptoActions.setStep({ step: SendCryptoStepType.ENTER_AMOUNT }) + }} + name='arrow-back' + role='button' + color='grey600' + size='24px' + style={{ marginRight: '20px' }} + /> + + {showNewSendFlow ? ( + ) : ( - - ), - Loading: () => ( -
- -
- ), - NotAsked: () => ( -
- -
+ + )} +
+
+
+ {props.prebuildTxR.cata({ + Failure: (e) => + isInsufficientBalance(e) ? ( + + props.sendCryptoActions.setStep({ step: SendCryptoStepType.ENTER_AMOUNT }) + } + handleMax={() => + sendCryptoActions.buildTx({ + account, + baseCryptoAmt: 'MAX', + destination: to, + fee, + fix, + memo, + rates, + walletCurrency + }) + } + /> + ) : ( + ), - Success: (tx) => ( - <> - - - {new BigNumber(tx.summary.amount).plus(tx.summary.absoluteFeeEstimate)} + Loading: () => ( +
+ +
+ ), + NotAsked: () => ( +
+ +
+ ), + Success: (tx) => ( + <> + + + {new BigNumber(tx.summary.amount).plus(tx.summary.absoluteFeeEstimate)} + + + +
+ + + +
+
+ + {tx.summary.amount} - - -
- - - -
-
- - {tx.summary.amount} - - - {tx.summary.amount} - -
-
- -
- - - -
-
- - {selectedAccount.label} - -
-
- -
- - - -
-
- + + {tx.summary.amount} + +
+
+ +
+ + + +
+
+ + {selectedAccount.label} + +
+
+ +
+ + + +
+
+ + {to && ( - -
-
+ )} + +
+
+ +
+ + + +
+
+ + {tx.summary.absoluteFeeEstimate} + + + {tx.summary.absoluteFeeEstimate} + +
+
+ {tx.rawTx?.payload.payload.memo.content ? (
- +
- - {tx.summary.absoluteFeeEstimate} - - - {tx.summary.absoluteFeeEstimate} - -
-
- {tx.rawTx?.payload.payload.memo.content ? ( - -
- - - -
-
- - {tx.rawTx.payload.payload.memo.content} - -
-
- ) : null} - -
- - + + {tx.rawTx.payload.payload.memo.content}
-
- - {new BigNumber(tx.summary.amount).plus(tx.summary.absoluteFeeEstimate)} - - - {new BigNumber(tx.summary.amount).plus(tx.summary.absoluteFeeEstimate)} - -
- - + ) : null} + +
+ + + +
+
+ + {new BigNumber(tx.summary.amount).plus(tx.summary.absoluteFeeEstimate)} + + + {new BigNumber(tx.summary.amount).plus(tx.summary.absoluteFeeEstimate)} + +
+
+ + + + + {!showNewSendFlow && ( - - - ) - })} -
+ )} + + + ) + })} ) } diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/EnterAmountNew/index.tsx b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/EnterAmountNew/index.tsx new file mode 100644 index 00000000000..36b070a16ee --- /dev/null +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/EnterAmountNew/index.tsx @@ -0,0 +1,510 @@ +import React from 'react' +import { FormattedMessage } from 'react-intl' +import { connect, ConnectedProps } from 'react-redux' +import { compose } from 'redux' +import { Field } from 'redux-form' +import reduxForm, { InjectedFormProps } from 'redux-form/lib/reduxForm' +import styled from 'styled-components' + +import { Remote } from '@core' +import { convertCoinToCoin, convertCoinToFiat, convertFiatToCoin } from '@core/exchange' +import Currencies from '@core/exchange/currencies' +import { formatFiat } from '@core/exchange/utils' +import { getRatesSelector } from '@core/redux/data/misc/selectors' +import { CoinType, RatesType } from '@core/types' +import { Button, CoinAccountIcon, Icon, Text } from 'blockchain-info-components' +import { DisplayContainer } from 'components/BuySell' +import { ErrorCartridge } from 'components/Cartridge' +import CoinDisplay from 'components/Display/CoinDisplay' +import FiatDisplay from 'components/Display/FiatDisplay' +import { FlyoutWrapper } from 'components/Flyout' +import BuyMoreLine from 'components/Flyout/Banners/BuyMoreLine' +import UpgradeToGoldLine, { Flows } from 'components/Flyout/Banners/UpgradeToGoldLine' +import { FlyoutContainer, FlyoutContent, FlyoutFooter } from 'components/Flyout/Layout' +import { StepHeader } from 'components/Flyout/SendRequestCrypto' +import AmountFieldInput from 'components/Form/AmountFieldInput' +import Form from 'components/Form/Form' +import TextBox from 'components/Form/TextBox' +import TextWithQRScanner from 'components/Form/TextWithQRScanner' +import { Padding } from 'components/Padding' +import { selectors } from 'data' +import { convertBaseToStandard } from 'data/components/exchange/services' +import { SendCryptoStepType } from 'data/components/sendCrypto/types' +import { SwapBaseCounterTypes } from 'data/types' +import { getEffectiveLimit, getEffectivePeriod } from 'services/custodial' +import { media } from 'services/styles' +import { debounce } from 'utils/helpers' + +import { AlertButton, MaxButton } from '../../components' +import { TIER_TYPES } from '../../Settings/TradingLimits/model' +import { Props as OwnProps } from '..' +import { FormLabelWithBorder, SEND_FORM } from '../model' +import { validate } from './validation' + +const Wrapper = styled(Form)` + height: 100%; +` + +const CustomErrorCartridge = styled(ErrorCartridge)` + cursor: pointer; +` +const CheckoutDisplayContainer = styled(DisplayContainer)` + justify-content: space-between; + ${media.tablet` + padding: 16px 20px; + `} +` +const QuoteActionContainer = styled.div` + height: 32px; +` +const FieldWrapper = styled(FlyoutWrapper)` + display: flex; + padding-top: 24px; + padding-bottom: 12px; +` +const FieldWrapperTo = styled(FlyoutWrapper)` + display: flex; + padding-top: 8px; + padding-bottom: 12px; +` +const ErrorWrapper = styled(FlyoutWrapper)` + display: flex; + padding-bottom: 0px; + padding-top: 0px; +` +const FlexStartRow = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; +` +const BalanceRow = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; +` +const OptionTitle = styled(Text)` + color: ${(props) => props.theme.grey800}; + font-weight: 600; + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +` +const OptionValue = styled(Text)` + color: ${(props) => props.theme.grey600}; + margin-top: 4px; + font-weight: 600; + font-size: 14px; +` + +const SendEnterAmount: React.FC & Props> = (props) => { + const { + buySellActions, + formActions, + formErrors, + formValues, + isValidAddress, + minR, + rates, + sendCryptoActions, + sendLimits, + verifyIdentity, + walletCurrency + } = props + const amountError = typeof formErrors.amount === 'string' && formErrors.amount + const { amount, fix, selectedAccount, to } = formValues + const { coin, type } = selectedAccount + const isAccount = type === SwapBaseCounterTypes.ACCOUNT + + const { coinfig } = window.coins[selectedAccount.coin] + + const valid = isValidAddress.cata({ + Failure: () => false, + Loading: () => false, + NotAsked: () => false, + Success: (res) => res + }) + + const disabled = !Remote.Success.is(isValidAddress) || !valid + + const max = Number(convertCoinToCoin({ coin, value: selectedAccount.balance })) + const min = minR.getOrElse(0) + const maxMinusFee = Number( + convertCoinToCoin({ + coin, + value: + Number(selectedAccount.balance) - + Number( + convertCoinToCoin({ baseToStandard: false, coin, value: props.feesR.getOrElse(0) || 0 }) + ) + }) + ) + + const cryptoAmt = + fix === 'FIAT' + ? convertFiatToCoin({ + coin, + currency: walletCurrency, + maxPrecision: 8, + rates, + value: amount + }) + : amount + const fiatAmt = + fix === 'CRYPTO' + ? convertCoinToFiat({ + coin, + currency: walletCurrency, + isStandard: true, + rates, + value: amount || 0 + }) + : amount + + const quote = fix === 'CRYPTO' ? fiatAmt : cryptoAmt + + const effectiveLimit = getEffectiveLimit(sendLimits) + const effectivePeriod = getEffectivePeriod(sendLimits) + + const handleMax = () => { + if (isAccount) { + // formActions.change(SEND_FORM, 'amount', 'MAX') + formActions.change(SEND_FORM, 'amount', maxMinusFee) + if (to) { + sendCryptoActions.setStep({ step: SendCryptoStepType.CONFIRM }) + } + } else { + formActions.change(SEND_FORM, 'fix', 'CRYPTO') + formActions.change(SEND_FORM, 'amount', maxMinusFee) + } + } + + return ( + sendCryptoActions.setStep({ step: SendCryptoStepType.CONFIRM })}> + + + + + + sendCryptoActions.setStep({ step: SendCryptoStepType.COIN_SELECTION }) + } + name='arrow-back' + role='button' + color='grey600' + size='24px' + style={{ marginRight: '20px' }} + /> + + + + + + + + +
+ {selectedAccount.label} + {coin} +
+
+ + + + {selectedAccount.balance} + + + + {selectedAccount.balance} + + + +
+ + { + formActions.change(SEND_FORM, 'fix', fix === 'CRYPTO' ? 'FIAT' : 'CRYPTO') + formActions.change(SEND_FORM, 'amount', fix === 'CRYPTO' ? fiatAmt : cryptoAmt) + }} + /> + + {amountError && amountError !== 'ABOVE_MAX_LIMIT' ? ( +
+ { + if (amountError === 'ABOVE_MAX') { + handleMax() + } else { + formActions.change(SEND_FORM, 'fix', 'CRYPTO') + formActions.change(SEND_FORM, 'amount', min) + } + }} + > + {amountError === 'ABOVE_MAX' && ( + + )} + {amountError === 'BELOW_MIN' && ( + + )} + +
+ ) : null} +
+ + + + +
+ +
+ + + + + + + formActions.change(SEND_FORM, 'to', data)} + onChange={debounce((e) => { + sendCryptoActions.validateAddress({ + address: e.currentTarget.value, + coin: selectedAccount.coin + }) + }, 100)} + placeholder={`${coinfig.name} Address`} + /> + + + + + + + + {isValidAddress.cata({ + Failure: () => null, + Loading: () => null, + NotAsked: () => null, + Success: (val) => { + return val ? null : ( + + + + + + ) + } + })} + {coinfig.type.isMemoBased ? ( + <> + + + + + + + + ) : null} +
+
+ + {!amountError && ( + + )} + + {amountError && amountError === 'ABOVE_MAX_LIMIT' && effectiveLimit && ( + <> + + + + + + + + )} + + {amountError === 'ABOVE_MAX' && ( + <> + + + + + + + + )} + {amountError === 'BELOW_MIN' && ( + + + + )} + + {(sendLimits?.suggestedUpgrade?.requiredTier === TIER_TYPES.GOLD || + (amountError && amountError === 'ABOVE_MAX_LIMIT' && effectiveLimit)) && + !isAccount && } + + {amountError === 'ABOVE_MAX' && ( + + buySellActions.showModal({ + cryptoCurrency: coin as CoinType, + orderType: 'BUY', + origin: 'Send' + }) + } + buyAmount={`${coin} ${amount}`} + coin={coin} + /> + )} + +
+
+ ) +} + +const mapStateToProps = (state, ownProps: OwnProps) => { + const { coin } = ownProps.formValues.selectedAccount + + const ratesSelector = getRatesSelector(coin, state) + return { + feesR: selectors.components.sendCrypto.getWithdrawalFees(state, coin), + minR: selectors.components.sendCrypto.getWithdrawalMin(state, coin), + rates: ratesSelector.getOrElse({} as RatesType) + } +} + +const connector = connect(mapStateToProps) +const enhance = compose( + connector, + reduxForm<{}, Props>({ + destroyOnUnmount: false, + form: SEND_FORM, + validate + }) +) + +export type Props = ConnectedProps & + OwnProps & { + formErrors: { + amount?: 'ABOVE_MAX' | 'ABOVE_MAX_LIMIT' | 'BELOW_MIN' | 'NEGATIVE_INCOMING_AMT' | boolean + } + } + +export default enhance(SendEnterAmount) as React.ComponentType diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/EnterAmountNew/validation.tsx b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/EnterAmountNew/validation.tsx new file mode 100644 index 00000000000..7e0de8c18dd --- /dev/null +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/EnterAmountNew/validation.tsx @@ -0,0 +1,91 @@ +import { convertCoinToCoin, convertFiatToCoin } from '@core/exchange' +import { convertBaseToStandard } from 'data/components/exchange/services' +import { SwapBaseCounterTypes } from 'data/types' + +import { SendFormType } from '../types' +import { Props } from '.' + +const maximumAmount = (amount: string, balance: string | number, fee: string): boolean | string => { + if (!amount) return false + + return Number(amount) > Number(balance) - Number(fee) +} + +const maximumAmountWithLimit = ( + amount: string, + balance: string | number, + limit: string | number, + fee: string +): boolean | string => { + if (!amount) return false + + // check current amount vs limit, also check is limit + fee in available balance + return Number(amount) > Number(limit) && Number(limit) + Number(fee) <= Number(balance) +} + +const minimumAmount = (amount: string, min: number) => { + if (!amount) return false + + return Number(amount) < Number(min) +} + +export const validate = (formValues: SendFormType, props: Props) => { + const { feesR, minR, rates, sendLimits, walletCurrency: currency } = props + const { amount, fix, selectedAccount } = formValues + const { coin } = selectedAccount + + const fee = selectedAccount.type === SwapBaseCounterTypes.ACCOUNT ? 0 : feesR.getOrElse(0) || 0 + const min = minR.getOrElse(0) || 0 + + const cryptoStandardAmt = + fix === 'FIAT' + ? convertFiatToCoin({ + coin, + currency, + maxPrecision: 8, + rates, + value: amount + }) + : amount + const cryptoBaseAmt = convertCoinToCoin({ + baseToStandard: false, + coin, + value: cryptoStandardAmt + }) + const feeBaseAmt = convertCoinToCoin({ + baseToStandard: false, + coin, + value: fee + }) + + const isBelowMin = minimumAmount(cryptoStandardAmt, min) + let isAboveMax = maximumAmount(cryptoBaseAmt, selectedAccount.balance, feeBaseAmt) + + // do this only for seamless limits and if amount is below current balance + if (sendLimits?.current?.available && !isAboveMax) { + const { value: limitAmount } = sendLimits.current.available + + const limitAmountInBase = convertBaseToStandard('FIAT', limitAmount) + + const maxLimit = + fix === 'FIAT' + ? limitAmountInBase + : convertFiatToCoin({ + coin, + currency, + maxPrecision: 8, + rates, + value: limitAmountInBase + }) + isAboveMax = maximumAmountWithLimit(amount, selectedAccount.balance, maxLimit, feeBaseAmt) + if (isAboveMax) { + return { + amount: 'ABOVE_MAX_LIMIT' + } + } + } + + return { + amount: isBelowMin ? 'BELOW_MIN' : isAboveMax ? 'ABOVE_MAX' : false + } +} diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/Status/index.tsx b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/Status/index.tsx index ab3c1e7100e..77876c88fa3 100644 --- a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/Status/index.tsx +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/Status/index.tsx @@ -43,6 +43,8 @@ const StatusIcon = styled.div` ` const Status: React.FC = (props) => { + const { coinfig } = window.coins[props.formValues.selectedAccount.coin] + const icon = props.transaction.cata({ Failure: () => , Loading: () => , @@ -64,11 +66,33 @@ const Status: React.FC = (props) => { ) }) + const subMsg = props.transaction.cata({ + Failure: () => null, + Loading: () => null, + NotAsked: () => null, + Success: () => ( + <> + + + ) + }) return ( - + props.close()} + cursor + name='close-circle' + size='20px' + color='grey400' + /> @@ -78,6 +102,9 @@ const Status: React.FC = (props) => { {msg} + + {subMsg} + {props.transaction.cata({ Failure: () => ( @@ -99,7 +126,11 @@ const Status: React.FC = (props) => { data-e2e='viewDetails' fullwidth jumbo - onClick={() => props.routerActions.push(`/coins/${val.amount.symbol}`)} + onClick={() => + props.showNewSendFlow + ? props.sendCryptoActions.setStep({ step: SendCryptoStepType.TX_OVERVIEW }) + : props.routerActions.push(`/coins/${val.amount.symbol}`) + } > diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/TransactionOverview/index.tsx b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/TransactionOverview/index.tsx new file mode 100644 index 00000000000..5014a828238 --- /dev/null +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/TransactionOverview/index.tsx @@ -0,0 +1,191 @@ +import React from 'react' +import { FormattedMessage } from 'react-intl' +import { connect, ConnectedProps } from 'react-redux' +import BigNumber from 'bignumber.js' +import styled from 'styled-components' + +import { Button, SpinningLoader, Text } from 'blockchain-info-components' +import CollapseText from 'components/CollapseText' +import DataError from 'components/DataError' +import CoinDisplay from 'components/Display/CoinDisplay' +import FiatDisplay from 'components/Display/FiatDisplay' +import { FlyoutWrapper, Row } from 'components/Flyout' +import { AmountWrapper, StepHeader } from 'components/Flyout/SendRequestCrypto' +import { selectors } from 'data' +import { RootState } from 'data/rootReducer' + +import { Props as OwnProps } from '..' + +const Wrapper = styled.div` + display: flex; + justify-content: space-between; + flex-direction: column; + min-height: 100%; +` +const CustomRow = styled(Row)` + display: flex; + justify-content: space-between; + align-items: center; + > div:last-child * { + display: flex; + flex-direction: column; + align-items: flex-end; + } +` + +const TransactionOverview = (props: Props) => { + const { formValues, showNewSendFlow } = props + const { selectedAccount, to } = formValues + const { coin } = selectedAccount + + return ( + + + + + + + + + {props.prebuildTxR.cata({ + Failure: (e) => , + Loading: () => ( +
+ +
+ ), + NotAsked: () => ( +
+ +
+ ), + Success: (tx) => ( + <> + + + {new BigNumber(tx.summary.amount).plus(tx.summary.absoluteFeeEstimate)} + + + +
+ + + +
+
+ + {tx.summary.amount} + + + {tx.summary.amount} + +
+
+ +
+ + + +
+
+ + {selectedAccount.label} + +
+
+ +
+ + + +
+
+ + + +
+
+ +
+ + + +
+
+ + {tx.summary.absoluteFeeEstimate} + + + {tx.summary.absoluteFeeEstimate} + +
+
+ {tx.rawTx?.payload.payload.memo.content ? ( + +
+ + + +
+
+ + {tx.rawTx.payload.payload.memo.content} + +
+
+ ) : null} + +
+ + + +
+
+ + {new BigNumber(tx.summary.amount).plus(tx.summary.absoluteFeeEstimate)} + + + {new BigNumber(tx.summary.amount).plus(tx.summary.absoluteFeeEstimate)} + +
+
+ + + + + {!showNewSendFlow && ( + + )} + + + ) + })} +
+ ) +} + +const mapStateToProps = (state: RootState) => ({ + prebuildTxR: selectors.components.sendCrypto.getPrebuildTx(state), + transaction: selectors.components.sendCrypto.getTransaction(state) +}) + +const connector = connect(mapStateToProps) + +type Props = ConnectedProps & OwnProps + +export default connector(TransactionOverview) diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/index.tsx b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/index.tsx index 8222374d77c..de8d032dc9a 100644 --- a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/index.tsx +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/index.tsx @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react' import { connect, ConnectedProps } from 'react-redux' +import { map } from 'ramda' import { bindActionCreators, compose, Dispatch } from 'redux' import { reduxForm } from 'redux-form' @@ -12,14 +13,17 @@ import { ModalName } from 'data/types' import ModalEnhancer from 'providers/ModalEnhancer' import { ModalPropsType } from '../types' +import AccountSelect from './AccountSelect' import CoinSelect from './CoinSelect' import Confirm from './Confirm' import EnterAmount from './EnterAmount' +import EnterAmountNew from './EnterAmountNew' import EnterTo from './EnterTo' import { SEND_FORM } from './model' import NoFunds from './NoFunds' import { getData } from './selectors' import Status from './Status' +import TransactionOverview from './TransactionOverview' import { SendFormType } from './types' class SendCrypto extends PureComponent { @@ -59,7 +63,11 @@ class SendCrypto extends PureComponent { )} {this.props.step === SendCryptoStepType.COIN_SELECTION && ( - + {this.props.showNewSendFlow ? ( + + ) : ( + + )} )} {this.props.step === SendCryptoStepType.ENTER_TO && ( @@ -69,7 +77,11 @@ class SendCrypto extends PureComponent { )} {this.props.step === SendCryptoStepType.ENTER_AMOUNT && ( - + {this.props.showNewSendFlow ? ( + + ) : ( + + )} )} {this.props.step === SendCryptoStepType.CONFIRM && ( @@ -82,12 +94,18 @@ class SendCrypto extends PureComponent { )} + {this.props.step === SendCryptoStepType.TX_OVERVIEW && ( + + + + )} ) } } const mapStateToProps = (state: RootState) => ({ + coinList: selectors.balances.getTotalWalletBalancesSorted(state).getOrElse([]), formErrors: selectors.form.getFormSyncErrors(SEND_FORM)(state), formValues: selectors.form.getFormValues(SEND_FORM)(state) as SendFormType, initialValues: { @@ -100,6 +118,9 @@ const mapStateToProps = (state: RootState) => ({ .getSendLimits(state) .getOrElse({} as CrossBorderLimits), sendableCoins: getData(), + showNewSendFlow: selectors.core.walletOptions + .getNewSendFlowEnabled(state) + .getOrElse(false) as boolean, step: selectors.components.sendCrypto.getStep(state), walletCurrency: selectors.core.settings.getCurrency(state).getOrElse('USD') }) diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/selectors.ts b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/selectors.ts index 766cc9f3011..1f5c80034f4 100644 --- a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/selectors.ts +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/selectors.ts @@ -2,10 +2,10 @@ export const getData = () => { return Object.keys(window.coins).filter((value) => { const { products, type } = window.coins[value].coinfig return ( - (!products.includes('PrivateKey') && - products.includes('CustodialWalletBalance') && - type.name !== 'FIAT') || - products.includes('DynamicSelfCustody') + (products.includes('PrivateKey') || + products.includes('CustodialWalletBalance') || + products.includes('DynamicSelfCustody')) && + type.name !== 'FIAT' ) }) } diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/types.ts b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/types.ts index 21cbba022fc..37def99bb35 100644 --- a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/types.ts +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/types.ts @@ -7,6 +7,7 @@ export type SendFormType = { fee: BuildTxFeeType fix: 'CRYPTO' | 'FIAT' memo?: string + payPro?: any selectedAccount: SwapAccountType to: string } From 759952b3c3c6783c9d95aab792dfa99630d13ee1 Mon Sep 17 00:00:00 2001 From: Milan Date: Wed, 31 Aug 2022 10:27:15 +0200 Subject: [PATCH 2/5] feat(sedn flow): added logic on proper place for handling send exceptions --- .../src/data/components/sendCrypto/sagas.ts | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts index 8ec684a8a3b..3c73ba53b84 100644 --- a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts +++ b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts @@ -60,11 +60,6 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne const { account, baseCryptoAmt, destination, fee, memo } = action.payload const { coin } = account const feesR = S.getWithdrawalFees(yield select(), coin) - if (nonMigratedCoins.includes(coin)) { - // console.log('coin to be processed differently - build TX', coin) - // TODO simulate buildTx logic for non migrated coins - return - } if (account.type === SwapBaseCounterTypes.ACCOUNT) { const password = yield call(promptForSecondPassword) @@ -72,6 +67,12 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne const guid = yield select(selectors.core.wallet.getGuid) const [uuid] = yield call(api.generateUUIDs, 1) + if (nonMigratedCoins.includes(coin)) { + // TODO simulate buildTx logic for non migrated coins + // only PK accounts will go here + return + } + const tx: ReturnType = yield call(api.buildTx, { id: { guid, @@ -344,7 +345,6 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne if (formValues && nonMigratedCoins.includes(formValues?.selectedAccount?.coin)) { const { coin } = formValues.selectedAccount // TODO add part for checking for change based on coin - // console.log('coin to be processed differently', coin) } } @@ -393,14 +393,6 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne .getRatesSelector(coin, yield select()) .getOrFail('Failed to get rates') - // check is selected account with coin in non migrated coins list and process it - if (nonMigratedCoins.includes(coin)) { - if (coin === 'BTC') { - call(submitBTCTransaction) - return - } - } - if (selectedAccount.type === SwapBaseCounterTypes.ACCOUNT) { const password = yield call(promptForSecondPassword) const guid = yield select(selectors.core.wallet.getGuid) @@ -409,6 +401,15 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne 'No prebuildTx' ) as BuildTxResponseType prebuildTxFee = prebuildTx.summary.absoluteFeeEstimate + + // check is selected account with coin in non migrated coins list and process it + if (nonMigratedCoins.includes(coin)) { + if (coin === 'BTC') { + call(submitBTCTransaction) + return + } + } + const signedTx: BuildTxResponseType = yield call(signTx, prebuildTx, password) const pushedTx: ReturnType = yield call( api.pushTx, From 3ff41d2514e54c8a1b24acb282da9080d2044a49 Mon Sep 17 00:00:00 2001 From: Milan Date: Fri, 16 Sep 2022 18:08:56 +0200 Subject: [PATCH 3/5] feat(send): fixed paddings --- .../modals/SendCrypto/AccountSelect/index.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/index.tsx b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/index.tsx index a75daa6e3ae..5aa17275754 100644 --- a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/index.tsx +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/AccountSelect/index.tsx @@ -1,8 +1,7 @@ import React from 'react' import { FormattedMessage } from 'react-intl' import { connect, ConnectedProps } from 'react-redux' -import { Icon, Padding } from '@blockchain-com/constellation' -import { IconCloseCircleV2 } from '@blockchain-com/icons' +import { IconCloseCircleV2, Padding, PaletteColors } from '@blockchain-com/constellation' import { bindActionCreators, compose } from 'redux' import reduxForm, { InjectedFormProps } from 'redux-form/lib/reduxForm' import styled from 'styled-components' @@ -50,15 +49,18 @@ class SendAccountSelect extends React.PureComponent return ( - + <FormattedMessage id='buttons.send' defaultMessage='Send' /> close()}> - - - + @@ -71,7 +73,7 @@ class SendAccountSelect extends React.PureComponent - + Date: Tue, 27 Sep 2022 11:41:51 +0200 Subject: [PATCH 4/5] feat(new send): fixed issues after rebase --- .../src/modals/SendCrypto/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/index.tsx b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/index.tsx index de8d032dc9a..18bcb49b541 100644 --- a/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/index.tsx +++ b/packages/blockchain-wallet-v4-frontend/src/modals/SendCrypto/index.tsx @@ -1,6 +1,5 @@ import React, { PureComponent } from 'react' import { connect, ConnectedProps } from 'react-redux' -import { map } from 'ramda' import { bindActionCreators, compose, Dispatch } from 'redux' import { reduxForm } from 'redux-form' From 5887ab74a34f3e2424a16f472a94158f84218673 Mon Sep 17 00:00:00 2001 From: Milan Date: Fri, 30 Sep 2022 14:04:37 +0200 Subject: [PATCH 5/5] feat(new send): testing stuff --- .../src/data/components/sendCrypto/sagas.ts | 74 ++++++++++++++++++- .../blockchain-wallet-v4/src/utils/btc.js | 6 +- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts index 3c73ba53b84..a210f56c8f8 100644 --- a/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts +++ b/packages/blockchain-wallet-v4-frontend/src/data/components/sendCrypto/sagas.ts @@ -1,10 +1,12 @@ import BigNumber from 'bignumber.js' +import * as Bitcoin from 'bitcoinjs-lib' import { SEND_FORM } from 'blockchain-wallet-v4-frontend/src/modals/SendCrypto/model' import { SendFormType } from 'blockchain-wallet-v4-frontend/src/modals/SendCrypto/types' +import { nth } from 'ramda' import { call, delay, put, select } from 'redux-saga/effects' import secp256k1 from 'secp256k1' -import { Exchange } from '@core' +import { Exchange, utils } from '@core' import { convertCoinToCoin, convertFiatToCoin } from '@core/exchange' import { APIType } from '@core/network/api' import { BuildTxIntentType, BuildTxResponseType } from '@core/network/api/coin/types' @@ -19,6 +21,7 @@ import { Analytics } from 'data/types' import { AccountType } from 'middleware/analyticsMiddleware/types' import { promptForSecondPassword } from 'services/sagas' +import coinSagas from '../../coins/sagas' import sendSagas from '../send/sagas' import * as S from './selectors' import { actions as A } from './slice' @@ -39,6 +42,12 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne networks }) + const { getNextReceiveAddressForCoin } = coinSagas({ + api, + coreSagas, + networks + }) + const initializeSend = function* () { const totalBalanceR = yield select(selectors.balances.getTotalWalletBalanceNotFormatted) const totalBalance = totalBalanceR.getOrElse({ total: 0 }) @@ -70,9 +79,70 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne if (nonMigratedCoins.includes(coin)) { // TODO simulate buildTx logic for non migrated coins // only PK accounts will go here + // return + + // console.log('all details', { account, baseCryptoAmt, destination, fee, memo }) + const accountsR = yield select(selectors.core.common.btc.getAccountsBalances) + // const txS = yield select(selectors.core.common.btc.getWalletTransactions) + // console.log('txS', txS) + if (account.accountIndex !== undefined) { + // const defaultAccount = accountsR.map(nth(account?.accountIndex)).getOrElse({}) + // console.log('defaultAccountR', defaultAccount) + // const pubb = Bitcoin.payments.p2pkh({ + // pubkey: Bitcoin.bip32.fromBase58(defaultAccount.xpub).derive(0).derive(1).publicKey + // }) + // const addressDefault = yield call(getNextReceiveAddressForCoin, coin) + // console.log('addresst', pubb.address) + // console.log('address6', addressDefault) + // const addresst = utils.btc.keyPairToAddress(defaultAccount.xpub) + // console.log('addresst', addresst) + // const defaultAccount = allAccount[account.accountIndex] + // console.log('defaultAccount', defaultAccount) + } + + // console.log('defaultAccount', defaultAccount) + + const tx = {} as BuildTxResponseType + + if (coin === 'BTC') { + // const txBTC = new Bitcoin.TransactionBuilder() + + let payment = coreSagas.payment.btc.create({ + network: networks.btc + }) + payment = yield payment.init() + + payment = yield payment.from(account.accountIndex, ADDRESS_TYPES.ACCOUNT) + + // console.log('payment', payment) + + // from + payment = yield payment.from(account.index, account.type) + // to + payment = yield payment.to(destination, account.type) + // amount + const satAmount = Exchange.convertCoinToCoin({ + baseToStandard: false, + coin, + value: baseCryptoAmt + }) + payment = yield payment.amount(parseInt(satAmount)) + + // build transaction + try { + payment = yield payment.build() + + // console.log('payment ready', payment.value()) + } catch (e) { + yield put(actions.logs.logErrorMessage(logLocation, 'formChanged', e)) + } + // console.log('pubKey', pubKey) + // console.log('txBTC', txBTC) + } + + yield put(A.buildTxSuccess(tx)) return } - const tx: ReturnType = yield call(api.buildTx, { id: { guid, diff --git a/packages/blockchain-wallet-v4/src/utils/btc.js b/packages/blockchain-wallet-v4/src/utils/btc.js index 106a2d395bc..dd8198196ad 100755 --- a/packages/blockchain-wallet-v4/src/utils/btc.js +++ b/packages/blockchain-wallet-v4/src/utils/btc.js @@ -11,6 +11,8 @@ import { compose, dropLast, equals, head, last, or, propOr } from 'ramda' import { selectAll } from '../coinSelection' import * as Exchange from '../exchange' +export const keyPairToAddress = (key) => payments.p2pkh({ pubkey: key.publicKey }).address + export const isValidBtcAddress = (value, network) => { try { const addr = address.fromBase58Check(value) @@ -222,6 +224,7 @@ export const getWifAddress = (key, compressed = true) => { } export const compressPublicKey = (publicKey) => { + // eslint-disable-next-line const prefix = (publicKey[64] & 1) !== 0 ? 0x03 : 0x02 const prefixBuffer = Buffer.alloc(1) prefixBuffer[0] = prefix @@ -230,6 +233,7 @@ export const compressPublicKey = (publicKey) => { export const fingerprint = (publickey) => { const pkh = compose(crypto.ripemd160, crypto.sha256)(publickey) + // eslint-disable-next-line return ((pkh[0] << 24) | (pkh[1] << 16) | (pkh[2] << 8) | pkh[3]) >>> 0 } @@ -250,5 +254,3 @@ export const createXpubFromChildAndParent = (path, child, parent) => { return hdnode.neutered().toBase58() } - -export const keyPairToAddress = (key) => payments.p2pkh({ pubkey: key.publicKey }).address