From af785a9b9ff92004fa0dc8f589cb53ae79348a35 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Mon, 12 Feb 2024 20:14:13 +0100 Subject: [PATCH] fix(lightning): update transfer UI --- __tests__/todos.ts | 101 ++++---------- src/assets/icons/wallet.ts | 13 +- src/components/AssetCard.tsx | 20 +-- src/components/Assets.tsx | 3 +- src/components/BalanceHeader.tsx | 6 +- src/components/SuggestionCard.tsx | 4 +- src/components/Suggestions.tsx | 8 +- src/hooks/wallet.ts | 37 ++++- src/screens/Activity/ActivityFiltered.tsx | 1 - src/screens/Activity/ActivityList.tsx | 16 +-- src/screens/Activity/ListItem.tsx | 43 +++--- src/screens/Lightning/CustomConfirm.tsx | 5 +- src/screens/Lightning/QuickConfirm.tsx | 5 +- .../Bitcoin/BitcoinNetworkSelection.tsx | 3 + src/screens/Transfer/Confirm.tsx | 4 +- src/screens/Transfer/Setup.tsx | 4 +- src/screens/Wallets/Header.tsx | 4 + .../WalletsDetail/BitcoinBreakdown.tsx | 45 +++--- .../Wallets/WalletsDetail/NetworkRow.tsx | 56 ++++---- src/screens/Wallets/WalletsDetail/index.tsx | 7 +- src/screens/Wallets/index.tsx | 4 +- src/store/actions/actions.ts | 3 + src/store/actions/wallet.ts | 85 +++++++++++- src/store/index.ts | 2 +- src/store/migrations/index.ts | 12 ++ src/store/reducers/wallet.ts | 49 ++++++- src/store/reselect/lightning.ts | 14 ++ src/store/reselect/todos.ts | 80 +++++------ src/store/reselect/wallet.ts | 57 ++++---- src/store/shapes/todos.ts | 18 +-- src/store/shapes/wallet.ts | 1 + src/store/slices/blocktank.ts | 4 +- src/store/types/todos.ts | 4 +- src/store/types/wallet.ts | 30 ++++ src/store/utils/activity.ts | 12 +- src/store/utils/blocktank.ts | 48 +++++-- src/store/utils/lightning.ts | 23 +++- src/styles/colors.ts | 4 +- src/styles/icons.ts | 4 +- src/utils/activity/index.ts | 39 ++---- src/utils/i18n/locales/en/cards.json | 10 +- src/utils/i18n/locales/en/lightning.json | 6 +- src/utils/i18n/locales/en/wallet.json | 27 ++-- src/utils/lightning/index.ts | 28 +++- src/utils/startup/index.ts | 2 +- src/utils/wallet/index.ts | 39 +++--- src/utils/wallet/txdecoder.ts | 128 ++++++++++++++++++ 47 files changed, 719 insertions(+), 399 deletions(-) create mode 100644 src/utils/wallet/txdecoder.ts diff --git a/__tests__/todos.ts b/__tests__/todos.ts index 025268523..fa43fea6f 100644 --- a/__tests__/todos.ts +++ b/__tests__/todos.ts @@ -12,18 +12,17 @@ import { backupSeedPhraseTodo, btFailedTodo, buyBitcoinTodo, - lightningConnectingTodo, lightningReadyTodo, lightningSettingUpTodo, lightningTodo, pinTodo, slashtagsProfileTodo, - transferClosingChannel, - transferToSavingsTodo, - transferToSpendingTodo, + transferPendingTodo, + transferClosingChannelTodo, } from '../src/store/shapes/todos'; import { createNewWallet } from '../src/utils/startup'; import { EAvailableNetwork } from '../src/utils/networks'; +import { ETransferStatus, ETransferType } from '../src/store/types/wallet'; describe('Todos selector', () => { let s: RootState; @@ -101,59 +100,20 @@ describe('Todos selector', () => { assert.deepEqual(todosFullSelector(state), []); }); - it('should return lightningSettingUpTodo if there is a pending BT order', () => { + it('should return lightningSettingUpTodo if there is a pending transfer to spending', () => { const state = cloneDeep(s); - state.blocktank.orders.push({ - id: 'order1', - state: 'created', - } as IBtOrder); - state.blocktank.paidOrders = { order1: 'txid' }; - + state.wallet.wallets.wallet0.transfers.bitcoinRegtest.push({ + txId: 'txid', + type: ETransferType.open, + status: ETransferStatus.pending, + amount: 100000, + orderId: 'order1', + }); expect(todosFullSelector(state)).toEqual( expect.arrayContaining([lightningSettingUpTodo]), ); }); - it('should return lightningConnectingTodo if there is a pending channel', () => { - const state = cloneDeep(s); - - const channel1 = { - channel_id: 'channel1', - is_channel_ready: false, - } as TChannel; - state.lightning.nodes.wallet0.channels.bitcoinRegtest = { channel1 }; - state.lightning.nodes.wallet0.openChannelIds.bitcoinRegtest = ['channel1']; - - expect(todosFullSelector(state)).toEqual( - expect.arrayContaining([lightningConnectingTodo]), - ); - }); - - it('should return lightningConnectingTodo if there is a pending channel and open channels', () => { - const state = cloneDeep(s); - - const channel1 = { - channel_id: 'channel1', - is_channel_ready: true, - } as TChannel; - const channel2 = { - channel_id: 'channel2', - is_channel_ready: false, - } as TChannel; - state.lightning.nodes.wallet0.channels.bitcoinRegtest = { - channel1, - channel2, - }; - state.lightning.nodes.wallet0.openChannelIds.bitcoinRegtest = [ - 'channel1', - 'channel2', - ]; - - expect(todosFullSelector(state)).toEqual( - expect.arrayContaining([lightningConnectingTodo]), - ); - }); - it('should return transferClosingChannel if there are gracefully closing channels', () => { const state = cloneDeep(s); @@ -166,42 +126,41 @@ describe('Todos selector', () => { state.user.startCoopCloseTimestamp = 123; expect(todosFullSelector(state)).toEqual( - expect.arrayContaining([transferClosingChannel]), + expect.arrayContaining([transferClosingChannelTodo]), ); }); - it('should return transferToSpendingTodo if there are new pending BT orders', () => { + it('should return transferPendingTodo for addtional pending transfers to spending', () => { const state = cloneDeep(s); - const channel1 = { channel_id: 'channel1', is_channel_ready: true, } as TChannel; state.lightning.nodes.wallet0.channels.bitcoinRegtest = { channel1 }; state.lightning.nodes.wallet0.openChannelIds.bitcoinRegtest = ['channel1']; - state.blocktank.orders.push({ id: 'order1', state: 'created' } as IBtOrder); - state.blocktank.paidOrders = { order1: 'txid' }; - + state.wallet.wallets.wallet0.transfers.bitcoinRegtest.push({ + txId: 'txid', + type: ETransferType.open, + status: ETransferStatus.pending, + amount: 100000, + orderId: 'order1', + }); expect(todosFullSelector(state)).toEqual( - expect.arrayContaining([transferToSpendingTodo]), + expect.arrayContaining([{ ...transferPendingTodo, duration: 10 }]), ); }); - it('should return transferToSavingsTodo if there is a new claimable balance', () => { + it('should return transferPendingTodo if there is a transfer to savings', () => { const state = cloneDeep(s); - - const channel1 = { - channel_id: 'channel1', - is_channel_ready: true, - } as TChannel; - state.lightning.nodes.wallet0.channels.bitcoinRegtest = { channel1 }; - state.lightning.nodes.wallet0.openChannelIds.bitcoinRegtest = ['channel1']; - state.lightning.nodes.wallet0.claimableBalances.bitcoinRegtest = [ - { amount_satoshis: 123, type: 'ClaimableOnChannelClose' }, - ]; - + state.wallet.wallets.wallet0.transfers.bitcoinRegtest.push({ + txId: 'txid', + type: ETransferType.coopClose, + status: ETransferStatus.pending, + amount: 100000, + confirmations: 1, + }); expect(todosFullSelector(state)).toEqual( - expect.arrayContaining([transferToSavingsTodo]), + expect.arrayContaining([{ ...transferPendingTodo, duration: 50 }]), ); }); diff --git a/src/assets/icons/wallet.ts b/src/assets/icons/wallet.ts index 70f79b77b..9e4995ff7 100644 --- a/src/assets/icons/wallet.ts +++ b/src/assets/icons/wallet.ts @@ -1,8 +1,11 @@ -export const transferIcon = ( - color = 'white', -): string => ` - -`; +export const transferIcon = (color = 'white'): string => ` + + + + + + +`; export const unitBitcoinIcon = (color = 'white'): string => ` diff --git a/src/components/AssetCard.tsx b/src/components/AssetCard.tsx index 8d0795bd2..f6aff9bc6 100644 --- a/src/components/AssetCard.tsx +++ b/src/components/AssetCard.tsx @@ -1,19 +1,17 @@ import React, { memo, ReactElement } from 'react'; import { View, GestureResponderEvent, StyleSheet } from 'react-native'; -import { ClockIcon } from '../styles/icons'; +import { TransferIcon } from '../styles/icons'; import { Text01M, Caption13M } from '../styles/text'; import { TouchableOpacity } from '../styles/components'; +import { useBalance } from '../hooks/wallet'; import Money from '../components/Money'; -import { useAppSelector } from '../hooks/redux'; -import { openChannelIdsSelector } from '../store/reselect/lightning'; const AssetCard = ({ name, ticker, icon, satoshis, - pending, testID, onPress, }: { @@ -21,13 +19,11 @@ const AssetCard = ({ ticker: string; icon: ReactElement; satoshis: number; - pending?: boolean; testID?: string; onPress: (event: GestureResponderEvent) => void; }): ReactElement => { - const openChannelIds = useAppSelector(openChannelIdsSelector); - - const isTransferToSavings = openChannelIds.length === 0; + const { balanceInTransferToSpending, balanceInTransferToSavings } = + useBalance(); return ( @@ -43,8 +39,11 @@ const AssetCard = ({ - {pending && ( - + {balanceInTransferToSpending !== 0 && ( + + )} + {balanceInTransferToSavings !== 0 && ( + )} { const { t } = useTranslation('wallet'); const navigation = useNavigation(); - const { totalBalance, claimableBalance } = useBalance(); + const { totalBalance } = useBalance(); return ( <> @@ -23,7 +23,6 @@ const Assets = (): ReactElement => { name="Bitcoin" ticker="BTC" satoshis={totalBalance} - pending={claimableBalance > 0} icon={} testID="BitcoinAsset" onPress={(): void => { diff --git a/src/components/BalanceHeader.tsx b/src/components/BalanceHeader.tsx index 165e70358..8790bb650 100644 --- a/src/components/BalanceHeader.tsx +++ b/src/components/BalanceHeader.tsx @@ -16,7 +16,7 @@ import { unitSelector, hideBalanceSelector } from '../store/reselect/settings'; const BalanceHeader = (): ReactElement => { const { t } = useTranslation('wallet'); const onSwitchUnit = useSwitchUnitAnnounced(); - const { totalBalance, claimableBalance } = useBalance(); + const { totalBalance, pendingBalance } = useBalance(); const dispatch = useAppDispatch(); const unit = useAppSelector(unitSelector); const hideBalance = useAppSelector(hideBalanceSelector); @@ -28,7 +28,7 @@ const BalanceHeader = (): ReactElement => { return ( - {claimableBalance ? ( + {pendingBalance ? ( { {title} - + {description} diff --git a/src/components/Suggestions.tsx b/src/components/Suggestions.tsx index 62dd1fd2b..4e8c2750b 100644 --- a/src/components/Suggestions.tsx +++ b/src/components/Suggestions.tsx @@ -13,7 +13,7 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { Caption13Up } from '../styles/text'; import { View as ThemedView } from '../styles/components'; import { showToast } from '../utils/notifications'; -import { TTodoType } from '../store/types/todos'; +import { ITodo, TTodoType } from '../store/types/todos'; import { channelsNotificationsShown, hideTodo } from '../store/slices/todos'; import { showBottomSheet } from '../store/utils/ui'; import { @@ -114,9 +114,11 @@ const Suggestions = (): ReactElement => { ); const handleRenderItem = useCallback( - ({ item }): ReactElement => { + // eslint-disable-next-line react/no-unused-prop-types + ({ item }: { item: ITodo }): ReactElement => { const title = t(`${item.id}.title`); - let description = t(`${item.id}.description`); + const duration = item.duration; + let description = t(`${item.id}.description`, { duration }); if (item.id === 'lightningSettingUp') { description = t(`${item.id}.description${lightningSettingUpStep}`); diff --git a/src/hooks/wallet.ts b/src/hooks/wallet.ts index 6bf3440aa..a25596a14 100644 --- a/src/hooks/wallet.ts +++ b/src/hooks/wallet.ts @@ -11,6 +11,7 @@ import { currentWalletSelector, selectedNetworkSelector, selectedWalletSelector, + transfersSelector, } from '../store/reselect/wallet'; import { useCurrency } from './displayValues'; import i18n from '../utils/i18n'; @@ -18,6 +19,7 @@ import { showToast } from '../utils/notifications'; import { ignoresSwitchUnitToastSelector } from '../store/reselect/user'; import { ignoreSwitchUnitToast } from '../store/slices/user'; import { EUnit } from '../store/types/wallet'; +import { newChannelsNotificationsSelector } from '../store/reselect/todos'; /** * Retrieves wallet balances for the currently selected wallet and network. @@ -29,6 +31,9 @@ export const useBalance = (): { reserveBalance: number; // Share of lightning funds that are locked up in channels claimableBalance: number; // Funds that will be available after a channel opens/closes spendableBalance: number; // Total spendable funds (onchain + spendable lightning) + balanceInTransferToSpending: number; + balanceInTransferToSavings: number; + pendingBalance: number; // Funds that are currently in transfer totalBalance: number; // Total funds (all of the above) } => { const selectedWallet = useAppSelector(selectedWalletSelector); @@ -36,8 +41,10 @@ export const useBalance = (): { const currentWallet = useAppSelector((state) => { return currentWalletSelector(state, selectedWallet); }); + const transfers = useAppSelector(transfersSelector); const openChannels = useAppSelector(openChannelsSelector); const claimableBalance = useAppSelector(claimableBalanceSelector); + const newChannels = useAppSelector(newChannelsNotificationsSelector); // Get the total spending & reserved balance of all open channels let spendingBalance = 0; @@ -54,8 +61,33 @@ export const useBalance = (): { const onchainBalance = currentWallet.balance[selectedNetwork]; const lightningBalance = spendingBalance + reserveBalance + claimableBalance; const spendableBalance = onchainBalance + spendingBalance; + + let balanceInTransferToSpending = transfers.reduce((acc, transfer) => { + if (transfer.type === 'open' && transfer.status === 'pending') { + return acc + transfer.amount; + } + return acc; + }, 0); + let balanceInTransferToSavings = transfers.reduce((acc, transfer) => { + if (transfer.type === 'coop-close' && transfer.status === 'pending') { + return acc + transfer.amount; + } + return acc; + }, 0); + + const pendingBalance = + balanceInTransferToSpending + balanceInTransferToSavings; + + if (newChannels.length > 0) { + // avoid flashing wrong balance on channel open + balanceInTransferToSpending = 0; + } + const totalBalance = - onchainBalance + spendingBalance + reserveBalance + claimableBalance; + onchainBalance + + spendingBalance + + reserveBalance + + balanceInTransferToSpending; return { onchainBalance, @@ -64,6 +96,9 @@ export const useBalance = (): { reserveBalance, claimableBalance, spendableBalance, + pendingBalance, + balanceInTransferToSpending, + balanceInTransferToSavings, totalBalance, }; }; diff --git a/src/screens/Activity/ActivityFiltered.tsx b/src/screens/Activity/ActivityFiltered.tsx index 09ee90e00..7d17a830b 100644 --- a/src/screens/Activity/ActivityFiltered.tsx +++ b/src/screens/Activity/ActivityFiltered.tsx @@ -206,7 +206,6 @@ const ActivityFiltered = ({ { - const { t } = useTranslation('wallet'); - return {t('activity')}; - }, - () => true, -); - const ActivityList = ({ style, panGestureRef, contentContainerStyle, progressViewOffset, - showTitle = true, filter = {}, onScroll, }: { @@ -56,7 +47,6 @@ const ActivityList = ({ panGestureRef?: MutableRefObject; contentContainerStyle?: StyleProp; progressViewOffset?: number; - showTitle?: boolean; filter?: TActivityFilter; onScroll?: (event: NativeSyntheticEvent) => void; }): ReactElement => { @@ -133,7 +123,6 @@ const ActivityList = ({ progressViewOffset={progressViewOffset} /> } - ListHeaderComponent={showTitle ? ListHeaderComponent : undefined} ListEmptyComponent={{t('activity_no')}} /> ); @@ -147,9 +136,6 @@ const styles = StyleSheet.create({ category: { marginBottom: 16, }, - title: { - marginBottom: 23, - }, }); export default memo(ActivityList); diff --git a/src/screens/Activity/ListItem.tsx b/src/screens/Activity/ListItem.tsx index 27874ce56..0b57c1723 100644 --- a/src/screens/Activity/ListItem.tsx +++ b/src/screens/Activity/ListItem.tsx @@ -22,10 +22,11 @@ import { import { useAppSelector } from '../../hooks/redux'; import { useProfile2 } from '../../hooks/slashtags2'; import { useFeeText } from '../../hooks/fees'; -import { EPaymentType } from '../../store/types/wallet'; +import { EPaymentType, TTransferToSavings } from '../../store/types/wallet'; import { slashTagsUrlSelector } from '../../store/reselect/metadata'; import { truncate } from '../../utils/helpers'; import { getActivityItemDate } from '../../utils/activity'; +import { transferSelector } from '../../store/reselect/wallet'; export const ListItem = ({ title, @@ -83,6 +84,7 @@ const OnchainListItem = ({ }): ReactElement => { const { t } = useTranslation('wallet'); const { + txId, txType, value, fee, @@ -95,9 +97,10 @@ const OnchainListItem = ({ exists = true, } = item; const { shortRange: feeRateDescription } = useFeeText(feeRate); + const transfer = useAppSelector((state) => transferSelector(state, txId)); const isSend = txType === EPaymentType.sent; - const isTransferringToSpending = isTransfer && isSend; + const isTransferToSpending = isTransfer && isSend; let title = t(isSend ? 'activity_sent' : 'activity_received'); const amount = isSend ? value + fee : value; @@ -119,29 +122,33 @@ const OnchainListItem = ({ description = t('activity_low_fee'); } - if (isTransfer) { - title = t('activity_transfer'); - - if (isTransferringToSpending) { - description = t( - confirmed - ? 'activity_transfer_spending_done' - : 'activity_transfer_spending_inprogres', - ); + if (transfer) { + if (isTransferToSpending) { + title = t('activity_transfer_spending'); + if (confirmed) { + description = t('activity_transfer_spending_done'); + } else { + const duration = 10; + description = t('activity_transfer_spending_pending', { duration }); + } icon = ( - + ); } else { - description = t( - confirmed - ? 'activity_transfer_savings_done' - : 'activity_transfer_savings_inprogress', - ); + const transferToSavings = transfer as TTransferToSavings; + const requiredConfs = 6; + title = t('activity_transfer_savings'); + if (transferToSavings.confirmations >= requiredConfs) { + description = t('activity_transfer_savings_done'); + } else { + const duration = (requiredConfs - transferToSavings.confirmations) * 10; + description = t('activity_transfer_savings_pending', { duration }); + } icon = ( - + ); } diff --git a/src/screens/Lightning/CustomConfirm.tsx b/src/screens/Lightning/CustomConfirm.tsx index 9ed2b440c..ca51655b7 100644 --- a/src/screens/Lightning/CustomConfirm.tsx +++ b/src/screens/Lightning/CustomConfirm.tsx @@ -59,9 +59,12 @@ const CustomConfirm = ({ const lspFee = purchaseFeeValue.fiatValue - clientBalance.fiatValue; const handleConfirm = async (): Promise => { + if (!order) { + return; + } setLoading(true); await sleep(5); - const res = await confirmChannelPurchase({ orderId, selectedNetwork }); + const res = await confirmChannelPurchase({ order, selectedNetwork }); if (res.isErr()) { setLoading(false); return; diff --git a/src/screens/Lightning/QuickConfirm.tsx b/src/screens/Lightning/QuickConfirm.tsx index 2376ad9d1..74e5a1c99 100644 --- a/src/screens/Lightning/QuickConfirm.tsx +++ b/src/screens/Lightning/QuickConfirm.tsx @@ -60,9 +60,12 @@ const QuickConfirm = ({ const savingsPercentage = Math.round((savingsAmount / onchainBalance) * 100); const handleConfirm = async (): Promise => { + if (!order) { + return; + } setLoading(true); await sleep(5); - const res = await confirmChannelPurchase({ orderId, selectedNetwork }); + const res = await confirmChannelPurchase({ order, selectedNetwork }); if (res.isErr()) { setLoading(false); return; diff --git a/src/screens/Settings/Bitcoin/BitcoinNetworkSelection.tsx b/src/screens/Settings/Bitcoin/BitcoinNetworkSelection.tsx index 6b54dbd82..098744bd2 100644 --- a/src/screens/Settings/Bitcoin/BitcoinNetworkSelection.tsx +++ b/src/screens/Settings/Bitcoin/BitcoinNetworkSelection.tsx @@ -8,6 +8,7 @@ import { selectedNetworkSelector } from '../../../store/reselect/wallet'; import { networkLabels } from '../../../utils/networks'; import { switchNetwork } from '../../../utils/wallet'; import { SettingsScreenProps } from '../../../navigation/types'; +import { startWalletServices } from '../../../utils/startup'; const BitcoinNetworkSelection = ({ navigation, @@ -28,6 +29,8 @@ const BitcoinNetworkSelection = ({ onPress: async (): Promise => { setLoading(true); await switchNetwork(network.id); + // Start wallet services with the newly selected network. + await startWalletServices({ selectedNetwork }); setLoading(false); navigation.goBack(); }, diff --git a/src/screens/Transfer/Confirm.tsx b/src/screens/Transfer/Confirm.tsx index 5a6f9f68d..1ad4e9661 100644 --- a/src/screens/Transfer/Confirm.tsx +++ b/src/screens/Transfer/Confirm.tsx @@ -64,10 +64,10 @@ const Confirm = ({ const handleConfirm = async (): Promise => { setLoading(true); - if (orderId) { + if (order) { // savings -> spending setLoading(true); - const res = await confirmChannelPurchase({ orderId, selectedNetwork }); + const res = await confirmChannelPurchase({ order, selectedNetwork }); if (res.isErr()) { setLoading(false); return; diff --git a/src/screens/Transfer/Setup.tsx b/src/screens/Transfer/Setup.tsx index eb03a6666..e919952ff 100644 --- a/src/screens/Transfer/Setup.tsx +++ b/src/screens/Transfer/Setup.tsx @@ -131,9 +131,7 @@ const Setup = ({ navigation }: TransferScreenProps<'Setup'>): ReactElement => { const onContinue = useCallback(async (): Promise => { if (lnSetup.isTransferringToSavings) { - navigation.push('Confirm', { - spendingAmount, - }); + navigation.push('Confirm', { spendingAmount }); return; } diff --git a/src/screens/Wallets/Header.tsx b/src/screens/Wallets/Header.tsx index ee6b1bbc6..53c8189d8 100644 --- a/src/screens/Wallets/Header.tsx +++ b/src/screens/Wallets/Header.tsx @@ -26,6 +26,7 @@ const EnabledSlashtagsProfileButton = (): ReactElement => { return ( @@ -49,6 +50,7 @@ const ProfileButton = (): ReactElement => { return __DISABLE_SLASHTAGS__ ? ( {}}> @@ -80,6 +82,7 @@ const Header = (): ReactElement => { @@ -87,6 +90,7 @@ const Header = (): ReactElement => { diff --git a/src/screens/Wallets/WalletsDetail/BitcoinBreakdown.tsx b/src/screens/Wallets/WalletsDetail/BitcoinBreakdown.tsx index 1780137bd..1d7ba4428 100644 --- a/src/screens/Wallets/WalletsDetail/BitcoinBreakdown.tsx +++ b/src/screens/Wallets/WalletsDetail/BitcoinBreakdown.tsx @@ -4,7 +4,11 @@ import { useNavigation } from '@react-navigation/native'; import { useTranslation } from 'react-i18next'; import { View as ThemedView } from '../../../styles/components'; -import { TransferIcon, SavingsIcon, CoinsIcon } from '../../../styles/icons'; +import { + TransferIcon, + SavingsIcon, + LightningHollow, +} from '../../../styles/icons'; import { Caption13M } from '../../../styles/text'; import { useBalance } from '../../../hooks/wallet'; import { useAppSelector } from '../../../hooks/redux'; @@ -12,7 +16,6 @@ import { RootNavigationProp } from '../../../navigation/types'; import { isGeoBlockedSelector } from '../../../store/reselect/user'; import { accountVersionSelector } from '../../../store/reselect/lightning'; import { showToast } from '../../../utils/notifications'; -import { openChannelIdsSelector } from '../../../store/reselect/lightning'; import NetworkRow from './NetworkRow'; const BitcoinBreakdown = (): ReactElement => { @@ -20,17 +23,13 @@ const BitcoinBreakdown = (): ReactElement => { const navigation = useNavigation(); const isGeoBlocked = useAppSelector(isGeoBlockedSelector); const accountVersion = useAppSelector(accountVersionSelector); - const openChannelIds = useAppSelector(openChannelIdsSelector); const { onchainBalance, - lightningBalance, spendingBalance, - reserveBalance, - claimableBalance, + balanceInTransferToSpending, + balanceInTransferToSavings, } = useBalance(); - const isTransferToSavings = openChannelIds.length === 0; - const onRebalancePress = (): void => { if (accountVersion < 2) { showToast({ @@ -40,7 +39,8 @@ const BitcoinBreakdown = (): ReactElement => { }); return; } - if (lightningBalance && !isGeoBlocked) { + + if (spendingBalance && !isGeoBlocked) { navigation.navigate('Transfer', { screen: 'Setup' }); } else { navigation.navigate('LightningRoot', { screen: 'Introduction' }); @@ -51,40 +51,31 @@ const BitcoinBreakdown = (): ReactElement => { <> - + } /> - + - - + + {t('transfer_text')} - + - + } /> @@ -105,7 +96,7 @@ const styles = StyleSheet.create({ transferRow: { flexDirection: 'row', alignItems: 'center', - paddingVertical: 16, + paddingVertical: 10, }, transferButton: { paddingHorizontal: 15, diff --git a/src/screens/Wallets/WalletsDetail/NetworkRow.tsx b/src/screens/Wallets/WalletsDetail/NetworkRow.tsx index 41ddd8586..ce5a70592 100644 --- a/src/screens/Wallets/WalletsDetail/NetworkRow.tsx +++ b/src/screens/Wallets/WalletsDetail/NetworkRow.tsx @@ -1,58 +1,47 @@ import React, { ReactElement } from 'react'; import { View, StyleSheet } from 'react-native'; +import { useTranslation } from 'react-i18next'; -import { IColors } from '../../../styles/colors'; -import { ClockIcon, LockIcon } from '../../../styles/icons'; -import { Caption13M, Text01M } from '../../../styles/text'; +import { TransferIcon } from '../../../styles/icons'; +import { Title, Caption13M } from '../../../styles/text'; import Money from '../../../components/Money'; const NetworkRow = ({ title, - subtitle, balance, pendingBalance, - reserveBalance, - color, icon, }: { title: string; - subtitle: string; balance: number; pendingBalance?: number; - reserveBalance?: number; - color: keyof IColors; icon: ReactElement; }): ReactElement => { + const { t } = useTranslation('wallet'); + return ( - {icon} - - {title} - {subtitle} + {icon} + + {title} + {pendingBalance !== 0 && ( + + + + {t('details_transfer_subtitle')} + + + )} - + {pendingBalance ? ( - - - ) : null} - - {pendingBalance === 0 && reserveBalance ? ( - - - @@ -66,10 +55,13 @@ const styles = StyleSheet.create({ root: { flexDirection: 'row', justifyContent: 'space-between', - minHeight: 40, }, - text: { - justifyContent: 'space-between', + subtitle: { + flexDirection: 'row', + alignItems: 'center', + }, + subtitleIcon: { + marginRight: 3, }, amount: { justifyContent: 'space-between', diff --git a/src/screens/Wallets/WalletsDetail/index.tsx b/src/screens/Wallets/WalletsDetail/index.tsx index 00797a121..ab6a49ddf 100644 --- a/src/screens/Wallets/WalletsDetail/index.tsx +++ b/src/screens/Wallets/WalletsDetail/index.tsx @@ -158,11 +158,11 @@ const WalletsDetail = ({ @@ -182,7 +182,6 @@ const WalletsDetail = ({ { const hh = e.nativeEvent.layout.height; @@ -261,7 +260,7 @@ const styles = StyleSheet.create({ flex: 1, }, assetDetailContainer: { - paddingBottom: 20, + paddingBottom: 16, }, radiusContainer: { overflow: 'hidden', @@ -278,7 +277,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, }, balanceContainer: { - marginTop: 20, + marginTop: 8, marginBottom: 30, }, largeValueContainer: { diff --git a/src/screens/Wallets/index.tsx b/src/screens/Wallets/index.tsx index 52618d490..11c1e0570 100644 --- a/src/screens/Wallets/index.tsx +++ b/src/screens/Wallets/index.tsx @@ -26,7 +26,7 @@ import Widgets from '../../components/Widgets'; import SafeAreaInset from '../../components/SafeAreaInset'; import BetaWarning from '../../components/BetaWarning'; import Assets from '../../components/Assets'; -import Header, { HEADER_HEIGHT } from './Header'; +import Header from './Header'; import type { WalletScreenProps } from '../../navigation/types'; import { enableSwipeToHideBalanceSelector, @@ -39,6 +39,8 @@ import { useTranslation } from 'react-i18next'; import { ignoresHideBalanceToastSelector } from '../../store/reselect/user'; import { ignoreHideBalanceToast } from '../../store/slices/user'; +const HEADER_HEIGHT = 46; + // Workaround for crash on Android // https://github.com/software-mansion/react-native-reanimated/issues/4306#issuecomment-1538184321 const AnimatedRefreshControl = Animated.createAnimatedComponent(RefreshControl); diff --git a/src/store/actions/actions.ts b/src/store/actions/actions.ts index 4fb31f72a..78b8ba112 100644 --- a/src/store/actions/actions.ts +++ b/src/store/actions/actions.ts @@ -21,6 +21,9 @@ const actions = { ADD_ADDRESSES: 'ADD_ADDRESSES', RESET_ADDRESSES: 'RESET_ADDRESSES', UPDATE_UTXOS: 'UPDATE_UTXOS', + ADD_TRANSFER: 'ADD_TRANSFER', + UPDATE_TRANSFER: 'UPDATE_TRANSFER', + REMOVE_TRANSFER: 'REMOVE_TRANSFER', UPDATE_TRANSACTIONS: 'UPDATE_TRANSACTIONS', RESET_TRANSACTIONS: 'RESET_TRANSACTIONS', ADD_UNCONFIRMED_TRANSACTIONS: 'ADD_UNCONFIRMED_TRANSACTIONS', diff --git a/src/store/actions/wallet.ts b/src/store/actions/wallet.ts index b68e3cc1b..4282cf584 100644 --- a/src/store/actions/wallet.ts +++ b/src/store/actions/wallet.ts @@ -3,6 +3,8 @@ import { err, ok, Result } from '@synonymdev/result'; import actions from './actions'; import { EBoostType, + ETransferStatus, + ETransferType, IAddress, ICreateWallet, IFormattedTransactions, @@ -10,6 +12,8 @@ import { IUtxo, IWallets, IWalletStore, + TTransfer, + TTransferToSavings, TWalletName, } from '../types/wallet'; import { @@ -24,6 +28,7 @@ import { } from '../../utils/wallet'; import { dispatch, + getBlocktankStore, getFeesStore, getSettingsStore, getWalletStore, @@ -296,6 +301,78 @@ export const injectFakeTransaction = ({ } }; +/** + * Adds a new transfer transaction to the store. + * @param {TTransfer} payload + */ +export const addTransfer = (payload: TTransfer): void => { + dispatch({ type: actions.ADD_TRANSFER, payload }); +}; + +export const updateTransfer = ({ + txId, + type, + confirmations, +}: { + txId: string; + type: ETransferType; + confirmations?: number; +}): void => { + switch (type) { + case ETransferType.open: { + const orders = getBlocktankStore().orders; + const order = orders.find((o) => o.channel?.fundingTx.id === txId); + if (order) { + const paymentTxId = order.payment.onchain.transactions[0].txId; + dispatch({ + type: actions.UPDATE_TRANSFER, + payload: { txId: paymentTxId }, + }); + } + break; + } + case ETransferType.coopClose: + case ETransferType.forceClose: { + dispatch({ + type: actions.UPDATE_TRANSFER, + payload: { txId, confirmations }, + }); + break; + } + } +}; + +export const updatePendingTransfers = (headerHeight: number): void => { + const { currentWallet, selectedNetwork } = getCurrentWallet(); + const transactions = currentWallet.transactions[selectedNetwork]; + const transfers = currentWallet.transfers[selectedNetwork]; + const pendingTransfers = transfers.filter((t) => { + return ( + t.type !== ETransferType.open && t.status === ETransferStatus.pending + ); + }) as TTransferToSavings[]; + + pendingTransfers.forEach((transfer) => { + const tx = transactions[transfer.txId]; + if (tx && transfer.confirmations <= 6) { + const confs = tx.height < 1 ? 0 : headerHeight - tx.height + 1; + updateTransfer({ + txId: transfer.txId, + type: ETransferType.coopClose, + confirmations: confs, + }); + } + }); +}; + +/** + * Removes a transfer from the store. + * @param {string} txId + */ +export const removeTransfer = (txId: string): void => { + dispatch({ type: actions.REMOVE_TRANSFER, payload: txId }); +}; + /** * Retrieves, formats & stores the transaction history for the selected wallet/network. * @param {boolean} [scanAllAddresses] @@ -809,10 +886,10 @@ export const setWalletData = async ( try { switch (value) { case 'header': - updateHeader({ - header: data as IHeader, - selectedNetwork: getNetworkFromBeignet(network), - }); + const header = data as IHeader; + const selectedNetwork = getNetworkFromBeignet(network); + updateHeader({ header, selectedNetwork }); + updatePendingTransfers(header.height); break; case 'feeEstimates': updateOnchainFeeEstimates({ diff --git a/src/store/index.ts b/src/store/index.ts index 99868055b..da412f2be 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -40,7 +40,7 @@ const persistConfig = { key: 'root', storage: mmkvStorage, // increase version after store shape changes - version: 36, + version: 37, stateReconciler: autoMergeLevel2, blacklist: ['receive', 'ui'], migrate: createMigrate(migrations, { debug: __ENABLE_MIGRATION_DEBUG__ }), diff --git a/src/store/migrations/index.ts b/src/store/migrations/index.ts index 956ed9fdf..b7c542dfd 100644 --- a/src/store/migrations/index.ts +++ b/src/store/migrations/index.ts @@ -497,6 +497,18 @@ const migrations = { }, }; }, + 37: (state): PersistedState => { + const newState = { ...state }; + // Loop through all wallets + for (const walletName in newState.wallet.wallets) { + // Add transfers to each wallet, with the initial value set. + newState.wallet.wallets[walletName] = { + ...newState.wallet.wallets[walletName], + transfers: getNetworkContent([]), + }; + } + return newState; + }, }; export default migrations; diff --git a/src/store/reducers/wallet.ts b/src/store/reducers/wallet.ts index 77b2b747a..9f48d072d 100644 --- a/src/store/reducers/wallet.ts +++ b/src/store/reducers/wallet.ts @@ -1,7 +1,7 @@ import { produce } from 'immer'; import actions from '../actions/actions'; -import { IWalletStore } from '../types/wallet'; +import { ETransferStatus, ETransferType, IWalletStore } from '../types/wallet'; import { getDefaultWalletShape, defaultWalletStoreShape, @@ -215,6 +215,53 @@ const wallet = ( }, }; + case actions.ADD_TRANSFER: { + return produce(state, (draftState) => { + draftState.wallets[selectedWallet].transfers[selectedNetwork].push( + action.payload, + ); + }); + } + + case actions.UPDATE_TRANSFER: { + return produce(state, (draftState) => { + const current = + state.wallets[selectedWallet].transfers[selectedNetwork]; + const { txId, confirmations } = action.payload; + const updated = current.map((transfer) => { + if (transfer.txId === txId) { + let status = ETransferStatus.done; + if (transfer.type !== ETransferType.open) { + status = + confirmations < 6 + ? ETransferStatus.pending + : ETransferStatus.done; + } + return { + ...transfer, + status, + confirmations, + }; + } else { + return transfer; + } + }); + + draftState.wallets[selectedWallet].transfers[selectedNetwork] = updated; + }); + } + + case actions.REMOVE_TRANSFER: { + return produce(state, (draftState) => { + const current = + state.wallets[selectedWallet].transfers[selectedNetwork]; + const updated = current.filter((transfer) => { + return transfer.txId !== action.payload; + }); + draftState.wallets[selectedWallet].transfers[selectedNetwork] = updated; + }); + } + case actions.UPDATE_TRANSACTIONS: return { ...state, diff --git a/src/store/reselect/lightning.ts b/src/store/reselect/lightning.ts index 58eaf77a9..a4d794960 100644 --- a/src/store/reselect/lightning.ts +++ b/src/store/reselect/lightning.ts @@ -12,6 +12,7 @@ import { TWalletName } from '../types/wallet'; import { reduceValue } from '../../utils/helpers'; import { EAvailableNetwork } from '../../utils/networks'; import { selectedNetworkSelector, selectedWalletSelector } from './wallet'; +import { blocktankOrderSelector } from './blocktank'; export const lightningState = (state: RootState): TLightningState => { return state.lightning; @@ -227,3 +228,16 @@ export const lightningBackupSelector = createSelector( return node?.backup[selectedNetwork] ?? {}; }, ); + +/** + * Find the channel that corresponds to the provided order. + */ +export const channelForOrderSelector = createSelector( + [openChannelsSelector, blocktankOrderSelector], + (openChannels, order) => { + const channel = openChannels.find((c) => { + return order.channel?.fundingTx.id === c.funding_txid; + }); + return channel; + }, +); diff --git a/src/store/reselect/todos.ts b/src/store/reselect/todos.ts index 018f14442..52cb3d4d4 100644 --- a/src/store/reselect/todos.ts +++ b/src/store/reselect/todos.ts @@ -7,15 +7,13 @@ import { ITodo } from '../types/todos'; import { backupSeedPhraseTodo, buyBitcoinTodo, - lightningConnectingTodo, lightningReadyTodo, lightningSettingUpTodo, lightningTodo, pinTodo, slashtagsProfileTodo, - transferClosingChannel, - transferToSavingsTodo, - transferToSpendingTodo, + transferPendingTodo, + transferClosingChannelTodo, btFailedTodo, } from '../shapes/todos'; import { @@ -24,13 +22,10 @@ import { } from './user'; import { pinSelector } from './settings'; import { onboardingProfileStepSelector } from './slashtags'; -import { - claimableBalancesSelector, - closedChannelsSelector, - openChannelsSelector, - pendingChannelsSelector, -} from './lightning'; +import { closedChannelsSelector, openChannelsSelector } from './lightning'; import { blocktankPaidOrdersFullSelector } from './blocktank'; +import { transfersSelector } from './wallet'; +import { ETransferType, TTransferToSavings } from '../types/wallet'; export const todosSelector = (state: RootState): TTodosState => state.todos; @@ -55,28 +50,26 @@ export const todosFullSelector = createSelector( pinSelector, onboardingProfileStepSelector, openChannelsSelector, - pendingChannelsSelector, closedChannelsSelector, startCoopCloseTimestampSelector, - claimableBalancesSelector, blocktankPaidOrdersFullSelector, newChannelsNotificationsSelector, + transfersSelector, ( todos, backupVerified, pinTodoDone, onboardingStep, openChannels, - pendingChannels, closedChannels, startCoopCloseTimestamp, - claimableBalances, paidOrders, newChannels, - ): Array => { + transfers, + ): ITodo[] => { const { hide } = todos; - const res: Array = []; + const res: ITodo[] = []; if (!hide.backupSeedPhrase && !backupVerified) { res.push(backupSeedPhraseTodo); @@ -98,45 +91,52 @@ export const todosFullSelector = createSelector( return Number(new Date(order.orderExpiresAt)) > hide.btFailed; }); - // TODO: wait for improvements to `Balance` in LDK v0.0.120 - const claimableOnChannelClose = claimableBalances.find( - (b) => b.type === 'ClaimableOnChannelClose', - ); - const balanceInTransfer = claimableOnChannelClose; + const transferToSpending = transfers.find((t) => { + const isOpen = t.type === ETransferType.open; + const isPending = t.status === 'pending'; + return isOpen && isPending; + }); + + const transferToSavings = transfers.find((t) => { + const isClose = + t.type === ETransferType.coopClose || + t.type === ETransferType.forceClose; + + return isClose && t.confirmations < 6; + }); // lightning - if (showFailedBTOrder) { + if (newChannels.length > 0) { + // Show lightningReadyTodo if we have one channel opened recently + res.push(lightningReadyTodo); + } else if (showFailedBTOrder) { // failed blocktank order res.push(btFailedTodo); } else if (openChannels.length === 0 && closedChannels.length === 0) { - // no open channels - inital setup - if (pendingChannels.length > 0) { - res.push(lightningConnectingTodo); - } else if (Object.keys(paidOrders.created).length > 0) { + if (transferToSpending) { res.push(lightningSettingUpTodo); - } else if (balanceInTransfer?.amount_satoshis) { - res.push(transferToSavingsTodo); // TODO: find a way to distinguish between transfer to and from spendings + } else if (transferToSavings) { + const transfer = transferToSavings as TTransferToSavings; + const requiredConfs = 6; + const duration = (requiredConfs - transfer.confirmations) * 10; + res.push({ ...transferPendingTodo, duration }); // TODO: distinguish between coop and force close } else if (!hide.lightning) { res.push(lightningTodo); } } else { // some channels exist if (startCoopCloseTimestamp > 0) { - res.push(transferClosingChannel); - } else if (Object.keys(paidOrders.created).length > 0) { - res.push(transferToSpendingTodo); - } else if (balanceInTransfer?.amount_satoshis) { - res.push(transferToSavingsTodo); // TODO: find a way to distinguish between transfer to and from spendings - } else if (pendingChannels.length > 0) { - res.push(lightningConnectingTodo); + res.push(transferClosingChannelTodo); + } else if (transferToSpending) { + res.push({ ...transferPendingTodo, duration: 10 }); + } else if (transferToSavings) { + const transfer = transferToSavings as TTransferToSavings; + const requiredConfs = 6; + const duration = (requiredConfs - transfer.confirmations) * 10; + res.push({ ...transferPendingTodo, duration }); // TODO: find a way to distinguish between transfer to and from spendings } } - // Show lightningReadyTodo if we have one channel opened recently - if (newChannels.length > 0) { - res.push(lightningReadyTodo); - } - if (!hide.pin && !pinTodoDone) { res.push(pinTodo); } diff --git a/src/store/reselect/wallet.ts b/src/store/reselect/wallet.ts index 60f780ec5..653b98ed9 100644 --- a/src/store/reselect/wallet.ts +++ b/src/store/reselect/wallet.ts @@ -67,8 +67,7 @@ export const currentWalletSelector = createSelector( export const addressTypeSelector = createSelector( [walletState], (wallet): EAddressType => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; return wallet.wallets[selectedWallet]?.addressType[selectedNetwork]; }, ); @@ -93,6 +92,27 @@ export const exchangeRateSelector = createSelector( }, ); +/** + * Returns transfers for the currently selected wallet. + */ +export const transfersSelector = createSelector([walletState], (wallet) => { + const { selectedWallet, selectedNetwork } = wallet; + return wallet.wallets[selectedWallet].transfers[selectedNetwork]; +}); + +/** + * Returns transfers for the currently selected wallet. + */ +export const transferSelector = createSelector( + [walletState, (_wallet, txId: string): string => txId], + (wallet, txId) => { + const { selectedWallet, selectedNetwork } = wallet; + const transfers = wallet.wallets[selectedWallet].transfers[selectedNetwork]; + const transfer = transfers.find((t) => t.txId === txId); + return transfer; + }, +); + /** * Returns object of on-chain transactions for the currently selected wallet & network. * @param {RootState} state @@ -101,8 +121,7 @@ export const exchangeRateSelector = createSelector( export const transactionsSelector = createSelector( [walletState], (wallet): IFormattedTransactions => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; return wallet.wallets[selectedWallet]?.transactions[selectedNetwork] || {}; }, ); @@ -115,8 +134,7 @@ export const transactionsSelector = createSelector( export const transactionSelector = createSelector( [walletState], (wallet): ISendTransaction => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; return ( wallet.wallets[selectedWallet]?.transaction[selectedNetwork] || defaultSendTransaction @@ -132,8 +150,7 @@ export const transactionSelector = createSelector( export const transactionInputsSelector = createSelector( [walletState], (wallet): IUtxo[] => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; const transaction = wallet.wallets[selectedWallet]?.transaction[selectedNetwork] || defaultSendTransaction; @@ -149,8 +166,7 @@ export const transactionInputsSelector = createSelector( export const transactionFeeSelector = createSelector( [walletState], (wallet) => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; return ( wallet.wallets[selectedWallet]?.transaction[selectedNetwork].fee || defaultSendTransaction.fee @@ -166,8 +182,7 @@ export const transactionFeeSelector = createSelector( export const transactionMaxSelector = createSelector( [walletState], (wallet): boolean => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; return ( wallet.wallets[selectedWallet]?.transaction[selectedNetwork].max ?? false ); @@ -182,8 +197,7 @@ export const transactionMaxSelector = createSelector( export const boostedTransactionsSelector = createSelector( [walletState], (wallet): IBoostedTransactions => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; return ( wallet.wallets[selectedWallet]?.boostedTransactions[selectedNetwork] || {} ); @@ -198,8 +212,7 @@ export const boostedTransactionsSelector = createSelector( export const unconfirmedTransactionsSelector = createSelector( [walletState], (wallet): IFormattedTransaction[] => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; const transactions: IFormattedTransactions = wallet.wallets[selectedWallet]?.transactions[selectedNetwork] || {}; return Object.values(transactions).filter((tx) => tx.height < 1); @@ -217,14 +230,13 @@ export const walletSelector = (state: RootState): IWalletStore => state.wallet; export const onChainBalanceSelector = createSelector( walletState, (wallet): number => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; return wallet.wallets[selectedWallet]?.balance[selectedNetwork] || 0; }, ); export const utxosSelector = createSelector(walletState, (wallet): IUtxo[] => { - const selectedWallet = wallet.selectedWallet; + const { selectedWallet } = wallet; const selectedNetwork = wallet.selectedNetwork; return wallet.wallets[selectedWallet]?.utxos[selectedNetwork] || []; }); @@ -237,7 +249,7 @@ export const walletExistsSelector = createSelector( export const seedHashSelector = createSelector( [walletState], (wallet): string | undefined => { - const selectedWallet = wallet.selectedWallet; + const { selectedWallet } = wallet; return wallet.wallets[selectedWallet]?.seedHash; }, ); @@ -245,7 +257,7 @@ export const seedHashSelector = createSelector( // export const changeAddressSelector = createSelector( // [walletState], // (wallet): IAddress => { -// const selectedWallet = wallet.selectedWallet; +// const { selectedWallet } = wallet; // const selectedNetwork = wallet.selectedNetwork; // return ( // wallet.wallets[selectedWallet]?.changeAddressIndex[selectedNetwork] @@ -257,8 +269,7 @@ export const seedHashSelector = createSelector( export const selectedFeeIdSelector = createSelector( [walletState], (wallet): EFeeId => { - const selectedWallet = wallet.selectedWallet; - const selectedNetwork = wallet.selectedNetwork; + const { selectedWallet, selectedNetwork } = wallet; return ( wallet.wallets[selectedWallet]?.transaction[selectedNetwork] ?.selectedFeeId ?? EFeeId.none diff --git a/src/store/shapes/todos.ts b/src/store/shapes/todos.ts index ab6a62345..f84c4e2fa 100644 --- a/src/store/shapes/todos.ts +++ b/src/store/shapes/todos.ts @@ -25,31 +25,19 @@ export const lightningSettingUpTodo: ITodo = { image: imageLightning, dismissable: false, }; -export const lightningConnectingTodo: ITodo = { - id: 'lightningConnecting', - color: 'purple', - image: imageLightning, - dismissable: false, -}; export const lightningReadyTodo: ITodo = { id: 'lightningReady', color: 'purple', image: imageLightning, dismissable: false, }; -export const transferToSpendingTodo: ITodo = { - id: 'transferToSpending', - color: 'purple', - image: imageTransfer, - dismissable: false, -}; -export const transferToSavingsTodo: ITodo = { - id: 'transferToSavings', +export const transferPendingTodo: ITodo = { + id: 'transferPending', color: 'purple', image: imageTransfer, dismissable: false, }; -export const transferClosingChannel: ITodo = { +export const transferClosingChannelTodo: ITodo = { id: 'transferClosingChannel', color: 'purple', image: imageTransfer, diff --git a/src/store/shapes/wallet.ts b/src/store/shapes/wallet.ts index ecb381761..92838a673 100644 --- a/src/store/shapes/wallet.ts +++ b/src/store/shapes/wallet.ts @@ -168,6 +168,7 @@ export const defaultWalletShape: Readonly = { blacklistedUtxos: getNetworkContent([]), boostedTransactions: getNetworkContent({}), unconfirmedTransactions: getNetworkContent({}), + transfers: getNetworkContent([]), transactions: getNetworkContent({}), transaction: getNetworkContent(defaultSendTransaction), balance: getNetworkContent(0), diff --git a/src/store/slices/blocktank.ts b/src/store/slices/blocktank.ts index cf30c7337..fa131ef3b 100644 --- a/src/store/slices/blocktank.ts +++ b/src/store/slices/blocktank.ts @@ -38,9 +38,9 @@ export const blocktankSlice = createSlice({ }, addPaidBlocktankOrder: ( state, - action: PayloadAction<{ orderId: string; txid: string }>, + action: PayloadAction<{ orderId: string; txId: string }>, ) => { - state.paidOrders[action.payload.orderId] = action.payload.txid; + state.paidOrders[action.payload.orderId] = action.payload.txId; }, addCjitEntry: (state, action: PayloadAction) => { state.cJitEntries.push(action.payload); diff --git a/src/store/types/todos.ts b/src/store/types/todos.ts index 08a18e5c3..3fa980c55 100644 --- a/src/store/types/todos.ts +++ b/src/store/types/todos.ts @@ -8,8 +8,7 @@ export type TTodoType = | 'lightningSettingUp' | 'lightningConnecting' | 'lightningReady' - | 'transferToSpending' - | 'transferToSavings' + | 'transferPending' | 'transferClosingChannel' | 'slashtagsProfile' | 'buyBitcoin' @@ -20,6 +19,7 @@ export interface ITodo { color: keyof IColors; image: ImageSourcePropType; dismissable: boolean; + duration?: number; } export interface IOpenChannelNotification { diff --git a/src/store/types/wallet.ts b/src/store/types/wallet.ts index c368399c7..721a32a9a 100644 --- a/src/store/types/wallet.ts +++ b/src/store/types/wallet.ts @@ -184,6 +184,35 @@ export interface IBoostedTransactions { [txId: string]: IBoostedTransaction; } +export type TTransfer = TTransferToSpending | TTransferToSavings; + +export enum ETransferType { + open = 'open', + coopClose = 'coop-close', + forceClose = 'force-close', +} + +export enum ETransferStatus { + pending = 'pending', + done = 'done', +} + +export type TTransferToSpending = { + txId: string; // The txId of the transaction that paid for the channel. + type: ETransferType.open; + orderId: string; + status: ETransferStatus.pending | ETransferStatus.done; + amount: number; +}; + +export type TTransferToSavings = { + txId: string; // The txId of the transaction that closed the channel. + type: ETransferType.coopClose | ETransferType.forceClose; + status: ETransferStatus.pending | ETransferStatus.done; + amount: number; + confirmations: number; +}; + export interface IWallet { id: string; name: string; @@ -198,6 +227,7 @@ export interface IWallet { blacklistedUtxos: IWalletItem<[]>; boostedTransactions: IWalletItem; unconfirmedTransactions: IWalletItem; + transfers: IWalletItem; transactions: IWalletItem; transaction: IWalletItem; balance: IWalletItem; diff --git a/src/store/utils/activity.ts b/src/store/utils/activity.ts index 438230143..12c6f5a7f 100644 --- a/src/store/utils/activity.ts +++ b/src/store/utils/activity.ts @@ -81,7 +81,7 @@ export const updateActivityList = (): Result => { * @returns {Result} */ export const updateOnChainActivityList = (): Result => { - let { currentWallet } = getCurrentWallet({}); + let { currentWallet } = getCurrentWallet(); if (!currentWallet) { console.warn( 'No wallet found. Cannot update activity list with transactions.', @@ -89,18 +89,12 @@ export const updateOnChainActivityList = (): Result => { return ok(''); } const { selectedNetwork, selectedWallet } = getCurrentWallet(); - const blocktankTransactions = getBlocktankStore().paidOrders; - const blocktankOrders = getBlocktankStore().orders; const boostedTransactions = currentWallet.boostedTransactions[selectedNetwork]; const transactions = currentWallet.transactions[selectedNetwork]; - const activityItems = Object.values(transactions).map((transaction) => { - return onChainTransactionToActivityItem({ - transaction, - blocktankTransactions, - blocktankOrders, - }); + const activityItems = Object.values(transactions).map((tx) => { + return onChainTransactionToActivityItem({ transaction: tx }); }); const boostFormattedItems = formatBoostedActivityItems({ diff --git a/src/store/utils/blocktank.ts b/src/store/utils/blocktank.ts index 3793a98dc..730df7cc0 100644 --- a/src/store/utils/blocktank.ts +++ b/src/store/utils/blocktank.ts @@ -1,6 +1,18 @@ import { err, ok, Result } from '@synonymdev/result'; +import { CJitStateEnum } from '@synonymdev/blocktank-lsp-http-client/dist/shared/CJitStateEnum'; +import { + BtOrderState, + BtPaymentState, + IBtOrder, + ICJitEntry, +} from '@synonymdev/blocktank-lsp-http-client'; -import { resetSendTransaction, updateSendTransaction } from '../actions/wallet'; +import { + addTransfer, + removeTransfer, + resetSendTransaction, + updateSendTransaction, +} from '../actions/wallet'; import { setLightningSetupStep } from '../slices/user'; import { getBlocktankStore, @@ -36,14 +48,7 @@ import { showToast } from '../../utils/notifications'; import { getDisplayValues } from '../../utils/displayValues'; import i18n from '../../utils/i18n'; import { refreshLdk } from '../../utils/lightning'; -import { TWalletName } from '../types/wallet'; -import { - BtOrderState, - BtPaymentState, - IBtOrder, - ICJitEntry, -} from '@synonymdev/blocktank-lsp-http-client'; -import { CJitStateEnum } from '@synonymdev/blocktank-lsp-http-client/dist/shared/CJitStateEnum'; +import { ETransferStatus, ETransferType, TWalletName } from '../types/wallet'; import { addPaidBlocktankOrder, resetBlocktankOrders, @@ -349,11 +354,11 @@ export const startChannelPurchase = async ({ * @returns {Promise>} */ export const confirmChannelPurchase = async ({ - orderId, + order, selectedNetwork, selectedWallet, }: { - orderId: string; + order: IBtOrder; selectedNetwork?: EAvailableNetwork; selectedWallet?: TWalletName; }): Promise> => { @@ -364,7 +369,7 @@ export const confirmChannelPurchase = async ({ selectedWallet = getSelectedWallet(); } - const rawTx = await createTransaction({}); + const rawTx = await createTransaction(); if (rawTx.isErr()) { showToast({ type: 'error', @@ -391,12 +396,21 @@ export const confirmChannelPurchase = async ({ }); return err(broadcastResponse.error.message); } - dispatch(addPaidBlocktankOrder({ orderId, txid: broadcastResponse.value })); + dispatch( + addPaidBlocktankOrder({ orderId: order.id, txId: broadcastResponse.value }), + ); + addTransfer({ + txId: broadcastResponse.value, + type: ETransferType.open, + status: ETransferStatus.pending, + orderId: order.id, + amount: order.clientBalanceSat, + }); // Reset tx data. await resetSendTransaction(); - watchOrder(orderId).then(); + watchOrder(order.id).then(); dispatch(setLightningSetupStep(0)); refreshWallet({ onchain: true, @@ -413,6 +427,8 @@ export const confirmChannelPurchase = async ({ * @param {IBtOrder} order */ const handleOrderStateChange = (order: IBtOrder): void => { + const paymentTxId = order.payment.onchain.transactions[0].txId; + // queued for opening if (!order.channel?.state) { dispatch(setLightningSetupStep(2)); @@ -430,6 +446,7 @@ const handleOrderStateChange = (order: IBtOrder): void => { title: i18n.t('lightning:order_given_up_title'), description: i18n.t('lightning:order_given_up_msg'), }); + removeTransfer(paymentTxId); } // order expired @@ -439,12 +456,13 @@ const handleOrderStateChange = (order: IBtOrder): void => { title: i18n.t('lightning:order_expired_title'), description: i18n.t('lightning:order_expired_msg'), }); + removeTransfer(paymentTxId); } // new channel open if (order.state === BtOrderState.OPEN) { // refresh LDK after channel open - refreshLdk({}); + refreshLdk(); } }; diff --git a/src/store/utils/lightning.ts b/src/store/utils/lightning.ts index b7744ace7..ff32c6237 100644 --- a/src/store/utils/lightning.ts +++ b/src/store/utils/lightning.ts @@ -14,6 +14,7 @@ import { updateLightningNodeVersion, } from '../slices/lightning'; import { moveMetaIncTxTag } from '../slices/metadata'; +import { updateTransfer } from '../actions/wallet'; import { EAvailableNetwork } from '../../utils/networks'; import { getActivityItemById } from '../../utils/activity'; import { getSelectedNetwork, getSelectedWallet } from '../../utils/wallet'; @@ -33,7 +34,7 @@ import { TCreateLightningInvoice, TLightningNodeVersion, } from '../types/lightning'; -import { EPaymentType, TWalletName } from '../types/wallet'; +import { EPaymentType, ETransferType, TWalletName } from '../types/wallet'; import { EActivityType, TLightningActivityItem } from '../types/activity'; /** @@ -95,25 +96,33 @@ export const updateLightningChannelsThunk = async ({ }: { selectedWallet?: TWalletName; selectedNetwork?: EAvailableNetwork; -}): Promise> => { +}): Promise> => { if (!selectedNetwork) { selectedNetwork = getSelectedNetwork(); } if (!selectedWallet) { selectedWallet = getSelectedWallet(); } - const lightningChannels = await getLightningChannels(); - if (lightningChannels.isErr()) { - return err(lightningChannels.error.message); + const lightningChannelsRes = await getLightningChannels(); + if (lightningChannelsRes.isErr()) { + return err(lightningChannelsRes.error.message); } const channels: { [channelId: string]: TChannel } = {}; const openChannelIds: string[] = []; + const lightningChannels = lightningChannelsRes.value; - lightningChannels.value.forEach((channel) => { + lightningChannels.forEach((channel) => { channels[channel.channel_id] = channel; if (!openChannelIds.includes(channel.channel_id)) { openChannelIds.push(channel.channel_id); + + if (channel.is_channel_ready && channel.funding_txid) { + updateTransfer({ + txId: channel.funding_txid, + type: ETransferType.open, + }); + } } }); @@ -126,7 +135,7 @@ export const updateLightningChannelsThunk = async ({ }), ); - return ok(lightningChannels.value); + return ok('Updated Lightning Channels'); }; /** diff --git a/src/styles/colors.ts b/src/styles/colors.ts index c0a31475c..16af48f7d 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -119,13 +119,13 @@ const colors: IColors = { black5: 'rgba(0, 0, 0, 0.5)', black92: 'rgba(0, 0, 0, 0.92)', brand08: 'rgba(255, 102, 0, 0.08)', + brand16: 'rgba(255, 102, 0, 0.16)', + brand32: 'rgba(255, 102, 0, 0.32)', yellow08: 'rgba(255, 174, 0, 0.08)', yellow16: 'rgba(255, 174, 0, 0.16)', purple5: 'rgba(185, 92, 232, 0.5)', purple16: 'rgba(185, 92, 232, 0.16)', purple32: 'rgba(185, 92, 232, 0.32)', - brand16: 'rgba(255, 102, 0, 0.16)', - brand32: 'rgba(255, 102, 0, 0.32)', }; export default colors; diff --git a/src/styles/icons.ts b/src/styles/icons.ts index a2591c2e3..488907c5d 100644 --- a/src/styles/icons.ts +++ b/src/styles/icons.ts @@ -132,8 +132,8 @@ export const SettingsIcon = styled(SvgXml).attrs((props) => ({ export const TransferIcon = styled(SvgXml).attrs((props) => ({ xml: transferIcon(props.color ? props.theme.colors[props.color] : 'white'), - height: props.height ?? '19.8px', - width: props.width ?? '21.6px', + height: props.height ?? '17px', + width: props.width ?? '16px', }))((props) => ({ color: props.color ? props.theme.colors[props.color] : 'white', })); diff --git a/src/utils/activity/index.ts b/src/utils/activity/index.ts index 6b4b52aee..513760e8f 100644 --- a/src/utils/activity/index.ts +++ b/src/utils/activity/index.ts @@ -1,51 +1,42 @@ -import i18n, { i18nTime } from '../../utils/i18n'; +import { err, ok, Result } from '@synonymdev/result'; +import { IFormattedTransaction } from 'beignet'; + import { btcToSats } from '../conversion'; -import { TPaidBlocktankOrders } from '../../store/types/blocktank'; +import { getCurrentWallet } from '../wallet'; +import i18n, { i18nTime } from '../../utils/i18n'; +import { getActivityStore } from '../../store/helpers'; import { EPaymentType } from '../../store/types/wallet'; import { EActivityType, IActivityItem, TOnchainActivityItem, } from '../../store/types/activity'; -import { err, ok, Result } from '@synonymdev/result'; -import { getActivityStore } from '../../store/helpers'; -import { IBtOrder } from '@synonymdev/blocktank-lsp-http-client'; -import { IFormattedTransaction } from 'beignet'; /** * Converts a formatted transaction to an activity item * @param {IFormattedTransaction} transaction - * @param {TPaidBlocktankOrders} blocktankTransactions - * @param {IBtOrder[]} blocktankOrders * @returns {TOnchainActivityItem} activityItem */ export const onChainTransactionToActivityItem = ({ transaction, - blocktankTransactions, - blocktankOrders, }: { transaction: IFormattedTransaction; - blocktankTransactions: TPaidBlocktankOrders; - blocktankOrders: IBtOrder[]; }): TOnchainActivityItem => { + const { currentWallet, selectedNetwork } = getCurrentWallet(); + const { transfers } = currentWallet; + + const transfer = transfers[selectedNetwork].find((t) => { + return t.txId === transaction.txid; + }); + const isTransferToSpending = transfer?.type === 'open' ?? false; + const isTransferToSavings = transfer?.type === 'coop-close' ?? false; + // subtract fee from amount if applicable const amount = transaction.type === 'sent' ? transaction.value + transaction.fee : transaction.value; - // check if tx is a payment to Blocktank (i.e. transfer to spending) - const isTransferToSpending = !!Object.values(blocktankTransactions).find( - (txId) => transaction.txid === txId, - ); - - // check if tx is a payment from Blocktank (i.e. transfer to savings) - const isTransferToSavings = !!blocktankOrders.find((order) => { - return !!transaction.vin.find( - (input) => input.txid === order.channel?.close?.txId, - ); - }); - return { exists: transaction?.exists ?? true, id: transaction.txid, diff --git a/src/utils/i18n/locales/en/cards.json b/src/utils/i18n/locales/en/cards.json index 375488a7f..75f7c833d 100644 --- a/src/utils/i18n/locales/en/cards.json +++ b/src/utils/i18n/locales/en/cards.json @@ -9,7 +9,7 @@ }, "lightningSettingUp": { "title": "Setting Up", - "description0": "Awaiting Payment", + "description0": "Processing Payment", "description1": "Paid", "description2": "Queued", "description3": "Opening" @@ -22,13 +22,9 @@ "title": "Ready", "description": "Connected!" }, - "transferToSpending": { + "transferPending": { "title": "Transferring", - "description": "Ready in ±10m" - }, - "transferToSavings": { - "title": "Transferring", - "description": "Ready in ±60m" + "description": "Ready in ±{duration}m" }, "transferClosingChannel": { "title": "Initiating...", diff --git a/src/utils/i18n/locales/en/lightning.json b/src/utils/i18n/locales/en/lightning.json index 665d582a8..3a4bfb5b7 100644 --- a/src/utils/i18n/locales/en/lightning.json +++ b/src/utils/i18n/locales/en/lightning.json @@ -35,7 +35,7 @@ "duration_text": "Choose the minimum number of weeks you want your connection to remain open.", "duration_week": "{count, plural, one {week} other {weeks}}", "setting_up_header": "Setting Up.", - "setting_up_text": "Please wait and keep Bitkit open while your Lightning connection is being set up. This process takes about ±10 minutes.", + "setting_up_text": "Please wait and keep Bitkit open while your Lightning connection is being set up. This process takes about ±10 minutes.", "setting_up_step1": "Processing Payment", "setting_up_step2": "Payment Successful", "setting_up_step3": "Queued For Opening", @@ -53,9 +53,9 @@ "transfer_open": "It costs {txFee} in network fees and {lspFee} in server provider fees to transfer the additional funds to your spending balance.", "transfer_swipe": "Swipe To Transfer", "transfer_successful": "Transfer Successful", - "ts_savings_title": "Transferring to Savings.", + "ts_savings_title": "Transferring\nto Savings.", "ts_savings_text": "Transferring funds from your spending balance to your savings. You will be able to use these funds in ±1 hour.", - "ts_spendings_title": "Transferring to Spending.", + "ts_spendings_title": "Transferring\nto Spending.", "ts_spendings_text": "Transferring funds from your savings to your spending balance. You will be able to use these funds in ±10 minutes.", "interrupted_title": "Transfer Interrupted", "interrupted_header": "Please keep Bitkit open.", diff --git a/src/utils/i18n/locales/en/wallet.json b/src/utils/i18n/locales/en/wallet.json index 867df86e0..1fb6a1cd0 100644 --- a/src/utils/i18n/locales/en/wallet.json +++ b/src/utils/i18n/locales/en/wallet.json @@ -2,7 +2,7 @@ "send": "Send", "receive": "Receive", "balance_total": "Total balance", - "balance_total_pending": "Total balance ( Pending)", + "balance_total_pending": "Total balance ( in transfer)", "create_wallet_mnemonic_error": "Invalid recovery phrase.", "create_wallet_mnemonic_restore_error": "Please double-check if your recovery phrase is accurate.", "assets": "Assets", @@ -105,11 +105,12 @@ "activity_no_explain": "Receive some funds to get started", "activity_sent": "Sent", "activity_received": "Received", - "activity_transfer": "Transfer", - "activity_transfer_savings_inprogress": "Moving to Savings", - "activity_transfer_savings_done": "Moved to Savings", - "activity_transfer_spending_inprogres": "Moving to Spending Balance", - "activity_transfer_spending_done": "Moved to Spending Balance", + "activity_transfer_spending": "Spending Balance", + "activity_transfer_savings": "Savings Balance", + "activity_transfer_savings_pending": "Transfer from Spending (±{duration}m)", + "activity_transfer_savings_done": "Transfer from Spending", + "activity_transfer_spending_pending": "Transfer from Savings (±{duration}m)", + "activity_transfer_spending_done": "Transfer from Savings", "activity_confirms_in": "Confirms in {feeRateDescription}", "activity_confirms_in_boosted": "Boosting. Confirms in {feeRateDescription}", "activity_low_fee": "Fee potentially too low", @@ -157,11 +158,11 @@ "received": "Received", "other": "Other" }, - "details_savings_title": "Savings Balance", + "details_savings_title": "Savings", "details_savings_subtitle": "BTC", "transfer_text": "Transfer", - "details_spending_title": "Spending Balance", - "details_spending_subtitle": "Instant BTC", + "details_spending_title": "Spending", + "details_transfer_subtitle": "Incoming Transfer", "tx_invalid": "Transaction Invalid", "boost": "Boost", "boost_title": "Boost Transaction", @@ -193,8 +194,8 @@ "lnurl_w_success_description": "Withdraw Requested Successful", "lnurl_p_title": "Send Bitcoin", "lnurl_p_max": "Maximum amount", - "balance_hidden_title" : "Wallet Balance Hidden", - "balance_hidden_message" : "Swipe your wallet balance to reveal it again.", - "balance_unit_switched_title" : "Switched to {unit}", - "balance_unit_switched_message" : "Tap your wallet balance to switch it back to {unit}." + "balance_hidden_title": "Wallet Balance Hidden", + "balance_hidden_message": "Swipe your wallet balance to reveal it again.", + "balance_unit_switched_title": "Switched to {unit}", + "balance_unit_switched_message": "Tap your wallet balance to switch it back to {unit}." } diff --git a/src/utils/lightning/index.ts b/src/utils/lightning/index.ts index a5c45aed3..7df011eeb 100644 --- a/src/utils/lightning/index.ts +++ b/src/utils/lightning/index.ts @@ -3,6 +3,8 @@ import Keychain from 'react-native-keychain'; import * as bitcoin from 'bitcoinjs-lib'; import RNFS from 'react-native-fs'; import { err, ok, Result } from '@synonymdev/result'; +import { TBroadcastTransaction } from '@synonymdev/react-native-ldk/dist/utils/types'; +import { TGetAddressHistory } from 'beignet'; import lm, { ldk, DefaultTransactionDataShape, @@ -73,6 +75,8 @@ import { addActivityItem } from '../../store/slices/activity'; import { addCJitActivityItem } from '../../store/utils/activity'; import { EPaymentType, + ETransferStatus, + ETransferType, IWalletItem, TWalletName, } from '../../store/types/wallet'; @@ -91,8 +95,8 @@ import { __BACKUPS_SERVER_PUBKEY__, __TRUSTED_ZERO_CONF_PEERS__, } from '../../constants/env'; -import { TBroadcastTransaction } from '@synonymdev/react-native-ldk/dist/utils/types'; -import { TGetAddressHistory } from 'beignet'; +import { addTransfer } from '../../store/actions/wallet'; +import { decodeRawTx } from '../wallet/txdecoder'; let LDKIsStayingSynced = false; @@ -170,10 +174,26 @@ const broadcastTransaction: TBroadcastTransaction = async ( rawTx: string, ): Promise> => { const electrum = getOnChainWalletElectrum(); - return await electrum.broadcastTransaction({ + const res = await electrum.broadcastTransaction({ rawTx, subscribeToOutputAddress: false, }); + if (res.isErr()) { + return err(''); + } + + const transaction = decodeRawTx(rawTx, bitcoin.networks.regtest); + + // TODO: distinguish between different coop and force-close + addTransfer({ + txId: transaction.txid, + type: ETransferType.coopClose, + status: ETransferStatus.pending, + amount: transaction.outputs[0].satoshi, + confirmations: 0, + }); + + return ok(res.value); }; const getScriptPubKeyHistory = async ( @@ -1414,7 +1434,7 @@ export const getOpenChannels = async ({ fromStorage?: boolean; selectedWallet?: TWalletName; selectedNetwork?: EAvailableNetwork; -}): Promise> => { +} = {}): Promise> => { if (!selectedWallet) { selectedWallet = getSelectedWallet(); } diff --git a/src/utils/startup/index.ts b/src/utils/startup/index.ts index 61557679f..611f656e1 100644 --- a/src/utils/startup/index.ts +++ b/src/utils/startup/index.ts @@ -1,5 +1,6 @@ import { InteractionManager } from 'react-native'; import { err, ok, Result } from '@synonymdev/result'; +import { generateMnemonic, TServer } from 'beignet'; import { getAddressTypesToMonitor, @@ -23,7 +24,6 @@ import { promiseTimeout } from '../helpers'; import { EAvailableNetwork } from '../networks'; import { TWalletName } from '../../store/types/wallet'; import { runChecks } from '../wallet/checks'; -import { generateMnemonic, TServer } from 'beignet'; /** * Creates a new wallet from scratch diff --git a/src/utils/wallet/index.ts b/src/utils/wallet/index.ts index 418f228ff..68eae6e03 100644 --- a/src/utils/wallet/index.ts +++ b/src/utils/wallet/index.ts @@ -93,7 +93,6 @@ import { import { TServer } from 'beignet/src/types/electrum'; import { showToast } from '../notifications'; import { updateUi } from '../../store/slices/ui'; -import { startWalletServices } from '../startup'; import { ICustomGetScriptHash } from 'beignet/src/types/wallet'; import { ldk } from '@synonymdev/react-native-ldk'; import { resetActivityState } from '../../store/slices/activity'; @@ -134,9 +133,7 @@ export const refreshWallet = async ({ let notificationTxid: string | undefined; if (onchain) { - await wallet.refreshWallet({ - scanAllAddresses, - }); + await wallet.refreshWallet({ scanAllAddresses }); } if (lightning) { @@ -854,12 +851,6 @@ export const getCurrentWallet = ({ const walletStore = getWalletStore(); const lightning = getLightningStore(); const currentLightningNode = lightning.nodes[selectedWallet]; - if (!selectedNetwork) { - selectedNetwork = walletStore.selectedNetwork; - } - if (!selectedWallet) { - selectedWallet = walletStore.selectedWallet; - } const currentWallet = walletStore.wallets[selectedWallet]; return { currentWallet, @@ -1468,10 +1459,20 @@ const onMessage: TOnMessage = (key, data) => { !wallet?.isSwitchingNetworks ) { const txMsg: TTransactionMessage = data as TTransactionMessage; - showNewOnchainTxPrompt({ - id: txMsg.transaction.txid, - value: btcToSats(txMsg.transaction.value), + const txId = txMsg.transaction.txid; + const { currentWallet, selectedNetwork } = getCurrentWallet(); + + const transfer = currentWallet.transfers[selectedNetwork].find((t) => { + return t.txId === txId; }); + const isTransferToSavings = transfer?.type === 'coop-close' ?? false; + + if (!isTransferToSavings) { + showNewOnchainTxPrompt({ + id: txId, + value: btcToSats(txMsg.transaction.value), + }); + } } setTimeout(() => { updateActivityList(); @@ -2288,9 +2289,7 @@ export const switchNetwork = async ( ): Promise> => { const originalNetwork = getSelectedNetwork(); if (!servers) { - servers = getCustomElectrumPeers({ - selectedNetwork, - }); + servers = getCustomElectrumPeers({ selectedNetwork }); } await promiseTimeout(2000, ldk.stop()); // Wipe existing activity @@ -2305,12 +2304,6 @@ export const switchNetwork = async ( updateWallet({ selectedNetwork: originalNetwork }); return err(response.error.message); } - // Start wallet services with the newly selected network. - await startWalletServices({ - selectedNetwork, - }); - setTimeout(() => { - updateActivityList(); - }, 500); + setTimeout(updateActivityList, 500); return ok(true); }; diff --git a/src/utils/wallet/txdecoder.ts b/src/utils/wallet/txdecoder.ts new file mode 100644 index 000000000..cd0e787a1 --- /dev/null +++ b/src/utils/wallet/txdecoder.ts @@ -0,0 +1,128 @@ +import * as bitcoin from 'bitcoinjs-lib'; + +type Transaction = bitcoin.Transaction; +type Network = bitcoin.Network; +type TxOutput = bitcoin.TxOutput; + +type Format = { + txid: string; + version: number; + locktime: number; +}; + +const decodeFormat = (tx: Transaction): Format => { + return { + txid: tx.getId(), + version: tx.version, + locktime: tx.locktime, + }; +}; + +type Input = { + txid: string; + n: number; + script: string; + sequence: number; +}; + +const decodeInput = (tx: Transaction): Input[] => { + return tx.ins.map((input) => ({ + txid: input.hash.reverse().toString('hex'), + n: input.index, + script: bitcoin.script.toASM(input.script), + sequence: input.sequence, + })); +}; + +type PaymentFn = ( + a: bitcoin.Payment, + opts?: bitcoin.PaymentOpts, +) => bitcoin.Payment; + +// this is replacement for missing bitcoin.script.classifyOutput +const classifyOutputScript = (script): string => { + const isOutput = (paymentFn: PaymentFn): bitcoin.Payment | undefined => { + try { + return paymentFn({ output: script }); + } catch (e) {} + }; + + if (isOutput(bitcoin.payments.p2pk)) { + return 'pubkey'; + } else if (isOutput(bitcoin.payments.p2pkh)) { + return 'pubkeyhash'; + } else if (isOutput(bitcoin.payments.p2ms)) { + return 'multisig'; + } else if (isOutput(bitcoin.payments.p2wpkh)) { + return 'pay-to-witness-pubkey-hash'; + } else if (isOutput(bitcoin.payments.p2sh)) { + return 'scripthash'; + } else if (isOutput(bitcoin.payments.p2tr)) { + return 'pay-to-taproot'; + } + + return 'nonstandard'; +}; + +type VOut = { + satoshi: number; + value: string; + n: number; + scriptPubKey: { + asm: string; + hex: string; + type: string; + addresses: string[]; + }; +}; + +const formatOutput = (out: TxOutput, n: number, network: Network): VOut => { + const vout: VOut = { + satoshi: out.value, + value: (1e-8 * out.value).toFixed(8), + n: n, + scriptPubKey: { + asm: bitcoin.script.toASM(out.script), + hex: out.script.toString('hex'), + type: classifyOutputScript(out.script), + addresses: [], + }, + }; + switch (vout.scriptPubKey.type) { + case 'pubkeyhash': + case 'pubkey': + case 'multisig': + case 'pay-to-witness-pubkey-hash': + case 'pay-to-taproot': + case 'scripthash': + const address = bitcoin.address.fromOutputScript(out.script, network); + vout.scriptPubKey.addresses.push(address); + break; + } + return vout; +}; + +const decodeOutput = (tx: Transaction, network: Network): VOut[] => { + return tx.outs.map((out, n) => formatOutput(out, n, network)); +}; + +type Result = { + txid: string; + version: number; + locktime: number; + inputs: Input[]; + outputs: VOut[]; +}; + +export const decodeRawTx = (rawTx: string, network: Network): Result => { + const tx = bitcoin.Transaction.fromHex(rawTx); + const format = decodeFormat(tx); + const inputs = decodeInput(tx); + const outputs = decodeOutput(tx, network); + + const result = {} as Result; + Object.keys(format).forEach((key) => (result[key] = format[key])); + result.inputs = inputs; + result.outputs = outputs; + return result; +};