diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3728529ef..87d14f300 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,13 +3,8 @@ # More details are here: https://help.github.com/articles/about-codeowners/ +/.github/workflows/ @limpbrains -# -# Github workflows need approval from the following users -# -/.github/workflows/ @limpbrains @rbndg +/e2e/ @limpbrains -# -# Github E2E need approval from the following user -# -/e2e/ @limpbrains @rbndg +/detox/ @limpbrains diff --git a/e2e/backup.e2e.js b/e2e/backup.e2e.js index d1f929cf1..7fe560179 100644 --- a/e2e/backup.e2e.js +++ b/e2e/backup.e2e.js @@ -60,6 +60,8 @@ d('Backup', () => { await element(by.id('UnderstoodButton')).tap(); await sleep(1000); // animation // get address from qrcode + await waitFor(element(by.id('QRCode'))).toBeVisible(); + await sleep(100); // wait for qr code to render let { label: wAddress } = await element(by.id('QRCode')).getAttributes(); wAddress = wAddress.replace('bitcoin:', ''); diff --git a/e2e/channels.e2e.js b/e2e/channels.e2e.js index 6e40ab14d..6c37ef39a 100644 --- a/e2e/channels.e2e.js +++ b/e2e/channels.e2e.js @@ -70,6 +70,8 @@ d('LN Channel Onboarding', () => { await element(by.id('Receive')).tap(); await element(by.id('UnderstoodButton')).tap(); // get address from qrcode + await waitFor(element(by.id('QRCode'))).toBeVisible(); + await sleep(100); // wait for qr code to render let { label: wAddress } = await element(by.id('QRCode')).getAttributes(); wAddress = wAddress.replace('bitcoin:', ''); diff --git a/e2e/onchain.e2e.js b/e2e/onchain.e2e.js index 6c11e61b9..f5372a047 100644 --- a/e2e/onchain.e2e.js +++ b/e2e/onchain.e2e.js @@ -240,6 +240,8 @@ d('Onchain', () => { } catch (e) {} await sleep(1000); // animation // get address from qrcode + await waitFor(element(by.id('QRCode'))).toBeVisible(); + await sleep(100); // wait for qr code to render let { label: wAddress } = await element(by.id('QRCode')).getAttributes(); wAddress = wAddress.replace('bitcoin:', ''); diff --git a/e2e/security.e2e.js b/e2e/security.e2e.js index fc7fe49ca..550c5c99d 100644 --- a/e2e/security.e2e.js +++ b/e2e/security.e2e.js @@ -126,6 +126,8 @@ d('Settings Security And Privacy', () => { } catch (e) {} await sleep(100); // get address from qrcode + await waitFor(element(by.id('QRCode'))).toBeVisible(); + await sleep(100); // wait for qr code to render let { label: wAddress } = await element(by.id('QRCode')).getAttributes(); wAddress = wAddress.replace('bitcoin:', ''); await rpc.sendToAddress(wAddress, '1'); diff --git a/e2e/slashtags.e2e.js b/e2e/slashtags.e2e.js index 8ad14f9c5..4c9ecebcc 100644 --- a/e2e/slashtags.e2e.js +++ b/e2e/slashtags.e2e.js @@ -184,6 +184,8 @@ d('Profile and Contacts', () => { await element(by.id('Receive')).tap(); await element(by.id('UnderstoodButton')).tap(); await sleep(1000); + await waitFor(element(by.id('QRCode'))).toBeVisible(); + await sleep(100); // wait for qr code to render let { label: wAddress } = await element(by.id('QRCode')).getAttributes(); wAddress = wAddress.replace('bitcoin:', ''); await rpc.sendToAddress(wAddress, '1'); diff --git a/package.json b/package.json index 13b839d44..8e1760efc 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "immer": "10.0.4", "intl-messageformat": "10.5.11", "jdenticon": "3.2.0", + "@synonymdev/ledger": "0.0.3", "lodash": "4.17.21", "lottie-react-native": "6.7.2", "mime": "3.0.0", diff --git a/src/App.tsx b/src/App.tsx index 0d589a6a0..a600a7990 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,7 @@ import AppOnboarded from './AppOnboarded'; import './utils/i18n'; import './utils/quick-actions'; +import './utils/ledger'; import { useAppSelector } from './hooks/redux'; import { checkForAppUpdate } from './store/utils/ui'; import { themeSelector } from './store/reselect/settings'; diff --git a/src/navigation/settings/SettingsNavigator.tsx b/src/navigation/settings/SettingsNavigator.tsx index 783d62093..2e0216ee7 100644 --- a/src/navigation/settings/SettingsNavigator.tsx +++ b/src/navigation/settings/SettingsNavigator.tsx @@ -52,6 +52,8 @@ import LightningNavigator, { LightningStackParamList, } from '../lightning/LightningNavigator'; import WebRelay from '../../screens/Settings/WebRelay'; +import Ledger from '../../screens/Settings/Ledger'; +import LedgerTransaction from '../../screens/Settings/Ledger/LedgerTransaction'; import { __E2E__ } from '../../constants/env'; import AppStatus from '../../screens/Settings/AppStatus'; import { TChannel } from '../../store/types/lightning'; @@ -107,6 +109,8 @@ export type SettingsStackParamList = { AddressViewer: undefined; FeeSettings: undefined; WebRelay: undefined; + Ledger: undefined; + LedgerTransaction: { ledgerTxId: number }; }; const Stack = createStackNavigator(); @@ -185,6 +189,8 @@ const SettingsNavigator = (): ReactElement => { + + ); }; diff --git a/src/screens/Settings/AddressViewer/index.tsx b/src/screens/Settings/AddressViewer/index.tsx index 17cbbdb0b..30224f867 100644 --- a/src/screens/Settings/AddressViewer/index.tsx +++ b/src/screens/Settings/AddressViewer/index.tsx @@ -69,6 +69,7 @@ import { startWalletServices } from '../../../utils/startup'; import { updateOnchainFeeEstimates } from '../../../store/utils/fees'; import { viewControllerIsOpenSelector } from '../../../store/reselect/ui'; import { EAddressType, IAddress, IUtxo } from 'beignet'; +import { setupLedger, syncLedger } from '../../../utils/ledger'; export type TAddressViewerData = { [EAddressType.p2tr]: { @@ -749,6 +750,7 @@ const AddressViewer = ({ if (selectedNetwork !== config.selectedNetwork) { // Wipe existing activity dispatch(resetActivityState()); + setupLedger({ selectedWallet, selectedNetwork }); ldk.stop(); // Switch to new network. updateWallet({ selectedNetwork: config.selectedNetwork }); @@ -763,6 +765,7 @@ const AddressViewer = ({ forceUpdate: true, }); updateActivityList(); + await syncLedger(); } let _utxos: IUtxo[] = []; diff --git a/src/screens/Settings/DevSettings/index.tsx b/src/screens/Settings/DevSettings/index.tsx index bd88942ee..a18b69fd6 100644 --- a/src/screens/Settings/DevSettings/index.tsx +++ b/src/screens/Settings/DevSettings/index.tsx @@ -133,6 +133,14 @@ const DevSettings = ({ navigation.navigate('FeeSettings'); }, }, + { + title: 'Ledger', + type: EItemType.button, + testID: 'FeeSettings', + onPress: (): void => { + navigation.navigate('Ledger'); + }, + }, ], }, { diff --git a/src/screens/Settings/Ledger/LedgerTransaction.tsx b/src/screens/Settings/Ledger/LedgerTransaction.tsx new file mode 100644 index 000000000..fc3d265f5 --- /dev/null +++ b/src/screens/Settings/Ledger/LedgerTransaction.tsx @@ -0,0 +1,233 @@ +import Clipboard from '@react-native-clipboard/clipboard'; +import React, { ReactElement, memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; + +import NavigationHeader from '../../../components/NavigationHeader'; +import SafeAreaInset from '../../../components/SafeAreaInset'; +import { SettingsScreenProps } from '../../../navigation/types'; +import { ScrollView, View as ThemedView } from '../../../styles/components'; +import { Caption13M, Caption13Up } from '../../../styles/text'; +import { i18nTime } from '../../../utils/i18n'; +import { bitkitLedger } from '../../../utils/ledger'; +import { showToast } from '../../../utils/notifications'; +import { accToEmoji } from '.'; + +const Section = memo( + ({ + name, + value, + testID, + onPress, + }: { + name: string; + value: ReactElement; + testID?: string; + onPress?: () => void; + }): ReactElement => { + return ( + + + {name} + + + {value} + + + ); + }, +); + +const LedgerTransaction = ({ + navigation, + route, +}: SettingsScreenProps<'LedgerTransaction'>): ReactElement => { + const { ledgerTxId } = route.params; + const { t } = useTranslation(); + const { t: tTime } = useTranslation('intl', { i18n: i18nTime }); + + const tx = useMemo( + () => bitkitLedger?.ledger.getTransaction(ledgerTxId)!, + [ledgerTxId], + ); + const { id, balancesBefore, amount, fromAcc, toAcc, metadata, timestamp } = + tx; + const meta = JSON.stringify(metadata, null, 2); + const fromText = accToEmoji(fromAcc); + const toText = accToEmoji(toAcc); + + return ( + + + navigation.goBack()} + /> + + + + Details + +
+ {id} + + } + onPress={(): void => { + Clipboard.setString(String(id)); + showToast({ + type: 'success', + title: t('copied'), + description: String(id), + }); + }} + /> +
+ {tTime('dateTime', { + v: new Date(timestamp), + formatParams: { + v: { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: false, + }, + }, + })} + + } + /> +
{amount}} /> +
+ {fromAcc.wallet} / {fromAcc.account} + + } + /> +
+ {toAcc.wallet} / {toAcc.account} + + } + /> + + + + + + + Balance before From + +
+ {balancesBefore.fromWallet.available} + + } + /> +
+ {balancesBefore.fromWallet.hold} + + } + /> + + + + Balance before To + +
+ {balancesBefore.toWallet.available} + + } + /> +
+ {balancesBefore.toWallet.hold} + + } + /> + + + + + + + Metadata + + + {`${meta}`} + + + + + + ); +}; + +const styles = StyleSheet.create({ + root: { + flex: 1, + justifyContent: 'space-between', + }, + content: { + paddingHorizontal: 16, + flexGrow: 1, + }, + section: { + marginTop: 16, + }, + sectionTitle: { + marginBottom: 8, + flexDirection: 'row', + alignItems: 'center', + }, + sectionRoot: { + height: 50, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderBottomWidth: 1, + borderBottomColor: 'rgba(255, 255, 255, 0.1)', + }, + sectionName: { + flex: 1, + }, + sectionValue: { + flex: 1.5, + alignItems: 'flex-end', + justifyContent: 'center', + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + column: { + width: '45%', + }, +}); + +export default memo(LedgerTransaction); diff --git a/src/screens/Settings/Ledger/index.tsx b/src/screens/Settings/Ledger/index.tsx new file mode 100644 index 000000000..a5091c427 --- /dev/null +++ b/src/screens/Settings/Ledger/index.tsx @@ -0,0 +1,330 @@ +import { TBitkitTransaction, TDestination } from '@synonymdev/ledger'; +import React, { ReactElement, memo, useState } from 'react'; +import { Alert, StyleSheet, View } from 'react-native'; + +import Button from '../../../components/Button'; +import NavigationHeader from '../../../components/NavigationHeader'; +import SafeAreaInset from '../../../components/SafeAreaInset'; +import { useBalance } from '../../../hooks/wallet'; +import { SettingsScreenProps } from '../../../navigation/types'; +import { + ScrollView, + TouchableOpacity as ThemedTouchableOpacity, + View as ThemedView, +} from '../../../styles/components'; +import { Caption13Up, Text01S } from '../../../styles/text'; +import { + bitkitLedger, + deleteLedger, + syncLedger, + exportLedger, +} from '../../../utils/ledger'; + +export const accToEmoji = (acc: TDestination): string => { + let wallet = ''; + if (acc.wallet === 'onchain') { + wallet = '🏠🔗'; + } else if (acc.wallet === 'lightning') { + wallet = '🏠⚡️'; + } else if (acc.wallet === 'onchain_remote') { + wallet = '🌐🔗'; + } else if (acc.wallet === 'lightning_remote') { + wallet = '🌐⚡️'; + } + + let account = ''; + if (acc.account === 'available') { + account = '💰'; + } else if (acc.account === 'hold') { + account = '🔒'; + } + + return wallet + account; +}; + +const Transaction = ({ + tx, + onPress, +}: { + tx: TBitkitTransaction; + onPress: () => void; +}): ReactElement => { + const { id, amount, fromAcc, toAcc } = tx; + const fromText = accToEmoji(fromAcc); + const toText = accToEmoji(toAcc); + + let color; + if (toAcc.account === 'hold') { + color = 'white16'; + } else if (toAcc.wallet.includes('_remote')) { + color = 'red16'; + } else { + color = 'green16'; + } + + return ( + + + {id} + + + {amount} + + + + {fromText} ⟶ {toText} + + + + ); +}; + +const Ledger = ({ + navigation, +}: SettingsScreenProps<'Ledger'>): ReactElement => { + const [_, setRerender] = useState(0); + const [syncing, setSyncing] = useState(false); + const balance = useBalance(); + + const reRender = (): NodeJS.Timeout => + setTimeout(() => setRerender((prev) => prev + 1), 10); + + const handleSync = async (): Promise => { + setSyncing(true); + const res = await syncLedger(); + Alert.alert('Init', res.isErr() ? res.error.message : 'Success'); + setSyncing(false); + }; + + const handleReset = async (): Promise => { + const res = await deleteLedger(); + Alert.alert('Reset', res.isErr() ? res.error.message : 'Success'); + reRender(); + }; + + const handleExport = async (): Promise => { + const res = await exportLedger(); + Alert.alert('Export', res.isErr() ? res.error.message : 'Success'); + }; + + const handleTransaction = (id: number): void => { + navigation.navigate('LedgerTransaction', { ledgerTxId: id }); + }; + + return ( + + + navigation.goBack()} + /> + + {!bitkitLedger ? ( + <> + Ledger is not initialized yet + + ) : ( + <> + +