ADD: rename counterparty paymentcode

This commit is contained in:
overtorment 2024-05-08 19:08:52 +01:00
parent 8007c73094
commit 8ad6af025e
10 changed files with 357 additions and 165 deletions

View File

@ -1,7 +1,7 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { startAndDecrypt } from './start-and-decrypt';
import Notifications from '../blue_modules/notifications';
import { LegacyWallet, TTXMetadata, WatchOnlyWallet, BlueApp as BlueAppClass } from '../class';
import { LegacyWallet, TTXMetadata, WatchOnlyWallet, BlueApp as BlueAppClass, TCounterpartyMetadata } from '../class';
import type { TWallet } from '../class/wallets/types';
import presentAlert from '../components/Alert';
import loc from '../loc';
@ -19,6 +19,7 @@ interface BlueStorageContextType {
wallets: TWallet[];
setWalletsWithNewOrder: (wallets: TWallet[]) => void;
txMetadata: TTXMetadata;
counterpartyMetadata: TCounterpartyMetadata;
saveToDisk: (force?: boolean) => Promise<void>;
selectedWalletID: string | undefined;
setSelectedWalletID: (walletID: string | undefined) => void;
@ -87,9 +88,11 @@ export const BlueStorageProvider = ({ children }: { children: React.ReactNode })
return;
}
BlueApp.tx_metadata = txMetadata;
BlueApp.counterparty_metadata = counterpartyMetadata;
await BlueApp.saveToDisk();
setWallets([...BlueApp.getWallets()]);
txMetadata = BlueApp.tx_metadata;
counterpartyMetadata = BlueApp.counterparty_metadata;
});
};
@ -195,6 +198,7 @@ export const BlueStorageProvider = ({ children }: { children: React.ReactNode })
};
let txMetadata = BlueApp.tx_metadata;
let counterpartyMetadata = BlueApp.counterparty_metadata || {}; // init
const getTransactions = BlueApp.getTransactions;
const fetchWalletBalances = BlueApp.fetchWalletBalances;
const fetchWalletTransactions = BlueApp.fetchWalletTransactions;
@ -214,6 +218,7 @@ export const BlueStorageProvider = ({ children }: { children: React.ReactNode })
wallets,
setWalletsWithNewOrder,
txMetadata,
counterpartyMetadata,
saveToDisk,
getTransactions,
selectedWalletID,

View File

@ -1,4 +1,4 @@
import { Transaction, TWallet } from './wallets/types';
import { ExtendedTransaction, Transaction, TWallet } from './wallets/types';
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as encryption from '../blue_modules/encryption';
@ -31,7 +31,18 @@ let savingInProgress = 0; // its both a flag and a counter of attempts to write
export type TTXMetadata = {
[txid: string]: {
memo?: string;
txhex?: string;
};
};
export type TCounterpartyMetadata = {
/**
* our contact identifier, such as bip47 payment code
*/
[counterparty: string]: {
/**
* custom human-readable name we assign ourselves
*/
label: string;
};
};
@ -44,6 +55,7 @@ type TRealmTransaction = {
type TBucketStorage = {
wallets: string[]; // array of serialized wallets, not actual wallet objects
tx_metadata: TTXMetadata;
counterparty_metadata: TCounterpartyMetadata;
};
const isReactNative = typeof navigator !== 'undefined' && navigator?.product === 'ReactNative';
@ -61,11 +73,13 @@ export class BlueApp {
public cachedPassword?: false | string;
public tx_metadata: TTXMetadata;
public counterparty_metadata: TCounterpartyMetadata;
public wallets: TWallet[];
constructor() {
this.wallets = [];
this.tx_metadata = {};
this.counterparty_metadata = {};
this.cachedPassword = false;
}
@ -187,6 +201,7 @@ export class BlueApp {
await this.saveToDisk();
this.wallets = [];
this.tx_metadata = {};
this.counterparty_metadata = {};
return this.loadFromDisk();
} else {
throw new Error('Incorrect password. Please, try again.');
@ -216,10 +231,12 @@ export class BlueApp {
usedBucketNum = false; // resetting currently used bucket so we wont overwrite it
this.wallets = [];
this.tx_metadata = {};
this.counterparty_metadata = {};
const data: TBucketStorage = {
wallets: [],
tx_metadata: {},
counterparty_metadata: {},
};
let buckets = await this.getItem('data');
@ -454,6 +471,7 @@ export class BlueApp {
if (!this.wallets.some(wallet => wallet.getID() === ID)) {
this.wallets.push(unserializedWallet);
this.tx_metadata = data.tx_metadata;
this.counterparty_metadata = data.counterparty_metadata;
}
}
if (realm) realm.close();
@ -653,6 +671,7 @@ export class BlueApp {
let data: TBucketStorage | string[] /* either a bucket, or an array of encrypted buckets */ = {
wallets: walletsToSave,
tx_metadata: this.tx_metadata,
counterparty_metadata: this.counterparty_metadata,
};
if (this.cachedPassword) {
@ -797,35 +816,49 @@ export class BlueApp {
* Getter for all transactions in all wallets.
* But if index is provided - only for wallet with corresponding index
*
* @param index {Integer|null} Wallet index in this.wallets. Empty (or null) for all wallets.
* @param limit {Integer} How many txs return, starting from the earliest. Default: all of them.
* @param includeWalletsWithHideTransactionsEnabled {Boolean} Wallets' _hideTransactionsInWalletsList property determines wether the user wants this wallet's txs hidden from the main list view.
* @param index {number|undefined} Wallet index in this.wallets. Empty (or undef) for all wallets.
* @param limit {number} How many txs return, starting from the earliest. Default: all of them.
* @param includeWalletsWithHideTransactionsEnabled {boolean} Wallets' _hideTransactionsInWalletsList property determines wether the user wants this wallet's txs hidden from the main list view.
*/
getTransactions = (
index?: number,
limit: number = Infinity,
includeWalletsWithHideTransactionsEnabled: boolean = false,
): Transaction[] => {
): ExtendedTransaction[] => {
if (index || index === 0) {
let txs: Transaction[] = [];
let c = 0;
for (const wallet of this.wallets) {
if (c++ === index) {
txs = txs.concat(wallet.getTransactions());
const txsRet: ExtendedTransaction[] = [];
const walletID = wallet.getID();
const walletPreferredBalanceUnit = wallet.getPreferredBalanceUnit();
txs.map(tx =>
txsRet.push({
...tx,
walletID,
walletPreferredBalanceUnit,
}),
);
return txsRet;
}
}
return txs;
}
let txs: Transaction[] = [];
const txs: ExtendedTransaction[] = [];
for (const wallet of this.wallets.filter(w => includeWalletsWithHideTransactionsEnabled || !w.getHideTransactionsInWalletsList())) {
const walletTransactions = wallet.getTransactions();
const walletTransactions: Transaction[] = wallet.getTransactions();
const walletID = wallet.getID();
const walletPreferredBalanceUnit = wallet.getPreferredBalanceUnit();
for (const t of walletTransactions) {
t.walletPreferredBalanceUnit = wallet.getPreferredBalanceUnit();
t.walletID = walletID;
txs.push({
...t,
walletID,
walletPreferredBalanceUnit,
});
}
txs = txs.concat(walletTransactions);
}
return txs

View File

@ -15,6 +15,7 @@ import { SegwitBech32Wallet } from './segwit-bech32-wallet';
import { SegwitP2SHWallet } from './segwit-p2sh-wallet';
import { SLIP39LegacyP2PKHWallet, SLIP39SegwitBech32Wallet, SLIP39SegwitP2SHWallet } from './slip39-wallets';
import { WatchOnlyWallet } from './watch-only-wallet';
import { BitcoinUnit } from '../../models/bitcoinUnits';
export type Utxo = {
// Returned by BlueElectrum
@ -112,6 +113,15 @@ export type Transaction = {
counterparty?: string;
};
/**
* in some cases we add additional data to each tx object so the code that works with that transaction can find the
* wallet that owns it etc
*/
export type ExtendedTransaction = Transaction & {
walletID: string;
walletPreferredBalanceUnit: BitcoinUnit;
};
export type TWallet =
| HDAezeedWallet
| HDLegacyBreadwalletWallet

View File

@ -32,7 +32,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
const { colors } = useTheme();
const { navigate } = useNavigation();
const menuRef = useRef();
const { txMetadata, wallets } = useContext(BlueStorageContext);
const { txMetadata, counterpartyMetadata, wallets } = useContext(BlueStorageContext);
const { preferredFiatCurrency, language } = useSettings();
const containerStyle = useMemo(
() => ({
@ -45,8 +45,9 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
[colors.lightBorder],
);
const shortenContactName = (addr: string): string => {
return addr.substr(0, 5) + '...' + addr.substr(addr.length - 4, 4);
const shortenContactName = (name: string): string => {
if (name.length < 16) return name;
return name.substr(0, 8) + '...' + name.substr(name.length - 8, 8);
};
const title = useMemo(() => {
@ -58,7 +59,11 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item.confirmations, item.received, language]);
const txMemo = (item.counterparty ? `[${shortenContactName(item.counterparty)}] ` : '') + (txMetadata[item.hash]?.memo ?? '');
let counterparty;
if (item.counterparty) {
counterparty = counterpartyMetadata?.[item.counterparty]?.label ?? item.counterparty;
}
const txMemo = (counterparty ? `[${shortenContactName(counterparty)}] ` : '') + (txMetadata[item.hash]?.memo ?? '');
const subtitle = useMemo(() => {
let sub = Number(item.confirmations) < 7 ? loc.formatString(loc.transactions.list_conf, { number: item.confirmations }) : '';
if (sub !== '') sub += ' ';

View File

@ -167,6 +167,7 @@
"details_next": "Next",
"details_no_signed_tx": "The selected file does not contain a transaction that can be imported.",
"details_note_placeholder": "Note to Self",
"counterparty_label_placeholder": "Edit contact name",
"details_scan": "Scan",
"details_scan_hint": "Double tap to scan or import a destination",
"details_total_exceeds_balance": "The sending amount exceeds the available balance.",
@ -341,7 +342,6 @@
"cpfp_title": "Bump Fee (CPFP)",
"details_balance_hide": "Hide Balance",
"details_balance_show": "Show Balance",
"details_block": "Block Height",
"details_copy": "Copy",
"details_copy_amount": "Copy Amount",
"details_copy_block_explorer_link": "Copy Block Explorer Link",
@ -352,7 +352,7 @@
"details_outputs": "Outputs",
"date": "Date",
"details_received": "Received",
"transaction_note_saved": "Transaction note has been successfully saved.",
"transaction_saved": "Saved",
"details_show_in_block_explorer": "View in Block Explorer",
"details_title": "Transaction",
"details_to": "Output",

View File

@ -26,7 +26,7 @@ enum ButtonStatus {
}
interface TransactionStatusProps {
route: RouteProp<{ params: { hash?: string; walletID?: string } }, 'params'>;
route: RouteProp<{ params: { hash: string; walletID: string } }, 'params'>;
navigation: NativeStackNavigationProp<any>;
}
@ -84,10 +84,10 @@ const reducer = (state: State, action: { type: ActionType; payload?: any }): Sta
}
};
const TransactionsStatus = () => {
const TransactionStatus = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { isCPFPPossible, isRBFBumpFeePossible, isRBFCancelPossible, tx, isLoading, eta, intervalMs } = state;
const { setSelectedWalletID, wallets, txMetadata, fetchAndSaveWalletTransactions } = useStorage();
const { setSelectedWalletID, wallets, txMetadata, counterpartyMetadata, fetchAndSaveWalletTransactions } = useStorage();
const { hash, walletID } = useRoute<TransactionStatusProps['route']>().params;
const { navigate, setOptions, goBack } = useNavigation<TransactionStatusProps['navigation']>();
const { colors } = useTheme();
@ -366,7 +366,7 @@ const TransactionsStatus = () => {
});
};
const navigateToTransactionDetials = () => {
navigate('TransactionDetails', { hash: tx.hash });
navigate('TransactionDetails', { hash: tx.hash, walletID });
};
const renderCPFP = () => {
@ -432,8 +432,6 @@ const TransactionsStatus = () => {
};
const renderTXMetadata = () => {
const counterparty = tx.counterparty ? shortenCounterpartyName(tx.counterparty) : false;
if (txMetadata[tx.hash]) {
if (txMetadata[tx.hash].memo) {
return (
@ -441,27 +439,35 @@ const TransactionsStatus = () => {
<Text selectable style={styles.memoText}>
{txMetadata[tx.hash].memo}
</Text>
{counterparty ? (
<View>
<BlueSpacing10 />
<Text selectable style={styles.memoText}>
{tx.value < 0
? loc.formatString(loc.transactions.to, {
counterparty,
})
: loc.formatString(loc.transactions.from, {
counterparty,
})}
</Text>
</View>
) : null}
<BlueSpacing20 />
</View>
);
}
}
};
const renderTXCounterparty = () => {
if (!tx.counterparty) return; // no BIP47 counterparty for this tx, return early
// theres a counterparty. lets lookup if theres an alias for him
let counterparty = counterpartyMetadata?.[tx.counterparty]?.label ?? tx.counterparty;
counterparty = shortenCounterpartyName(counterparty);
return (
<View style={styles.memo}>
<Text selectable style={styles.memoText}>
{tx.value < 0
? loc.formatString(loc.transactions.to, {
counterparty,
})
: loc.formatString(loc.transactions.from, {
counterparty,
})}
</Text>
<BlueSpacing20 />
</View>
);
};
if (isLoading || !tx || wallet.current === undefined) {
return (
<SafeArea>
@ -489,6 +495,7 @@ const TransactionsStatus = () => {
</View>
{renderTXMetadata()}
{renderTXCounterparty()}
<View style={[styles.iconRoot, stylesHook.iconRoot]}>
<View>
@ -553,7 +560,7 @@ const TransactionsStatus = () => {
);
};
export default TransactionsStatus;
export default TransactionStatus;
const styles = StyleSheet.create({
container: {
flex: 1,

View File

@ -1,6 +1,6 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { View, ScrollView, TouchableOpacity, Text, TextInput, Linking, StyleSheet, Keyboard, InteractionManager } from 'react-native';
import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
import { InteractionManager, Keyboard, Linking, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { RouteProp, useFocusEffect, useNavigation, useRoute } from '@react-navigation/native';
import Clipboard from '@react-native-clipboard/clipboard';
import dayjs from 'dayjs';
import { BlueCard, BlueLoading, BlueSpacing20, BlueText } from '../../BlueComponents';
@ -13,13 +13,20 @@ import presentAlert from '../../components/Alert';
import { useTheme } from '../../components/themes';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import CopyToClipboardButton from '../../components/CopyToClipboardButton';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Transaction, TWallet } from '../../class/wallets/types';
import assert from 'assert';
function onlyUnique(value, index, self) {
interface TransactionDetailsProps {
route: RouteProp<{ params: { hash: string; walletID: string } }, 'params'>;
navigation: NativeStackNavigationProp<any>;
}
function onlyUnique(value: any, index: number, self: any[]) {
return self.indexOf(value) === index;
}
function arrDiff(a1, a2) {
function arrDiff(a1: any[], a2: any[]) {
const ret = [];
for (const v of a2) {
if (a1.indexOf(v) === -1) {
@ -29,15 +36,18 @@ function arrDiff(a1, a2) {
return ret;
}
const TransactionsDetails = () => {
const TransactionDetails = () => {
const { setOptions, navigate } = useNavigation();
const { hash } = useRoute().params;
const { saveToDisk, txMetadata, wallets, getTransactions } = useContext(BlueStorageContext);
const [from, setFrom] = useState();
const [to, setTo] = useState();
const { hash, walletID } = useRoute<TransactionDetailsProps['route']>().params;
const { saveToDisk, txMetadata, counterpartyMetadata, wallets, getTransactions } = useContext(BlueStorageContext);
const [from, setFrom] = useState<string[]>([]);
const [to, setTo] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [tx, setTX] = useState();
const [memo, setMemo] = useState();
const [tx, setTX] = useState<Transaction>();
const [memo, setMemo] = useState<string>('');
const [counterpartyLabel, setCounterpartyLabel] = useState<string>('');
const [paymentCode, setPaymentCode] = useState<string>('');
const [isCounterpartyLabelVisible, setIsCounterpartyLabelVisible] = useState<boolean>(false);
const { colors } = useTheme();
const stylesHooks = StyleSheet.create({
memoTextInput: {
@ -61,12 +71,16 @@ const TransactionsDetails = () => {
const handleOnSaveButtonTapped = useCallback(() => {
Keyboard.dismiss();
if (!tx) return;
txMetadata[tx.hash] = { memo };
if (counterpartyLabel && paymentCode) {
counterpartyMetadata[paymentCode] = { label: counterpartyLabel };
}
saveToDisk().then(_success => {
triggerHapticFeedback(HapticFeedbackTypes.Success);
presentAlert({ message: loc.transactions.transaction_note_saved });
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
presentAlert({ message: loc.transactions.transaction_saved });
});
}, [tx, memo, saveToDisk, txMetadata]);
}, [tx, txMetadata, memo, counterpartyLabel, paymentCode, saveToDisk, counterpartyMetadata]);
const HeaderRightButton = useMemo(() => {
return (
@ -89,35 +103,39 @@ const TransactionsDetails = () => {
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
let foundTx = {};
let newFrom = [];
let newTo = [];
for (const transaction of getTransactions(null, Infinity, true)) {
let foundTx: Transaction | false = false;
let newFrom: string[] = [];
let newTo: string[] = [];
for (const transaction of getTransactions(undefined, Infinity, true)) {
if (transaction.hash === hash) {
foundTx = transaction;
for (const input of foundTx.inputs) {
newFrom = newFrom.concat(input.addresses);
newFrom = newFrom.concat(input?.addresses ?? []);
}
for (const output of foundTx.outputs) {
if (output.addresses) newTo = newTo.concat(output.addresses);
if (output.scriptPubKey && output.scriptPubKey.addresses) newTo = newTo.concat(output.scriptPubKey.addresses);
if (output?.scriptPubKey?.addresses) newTo = newTo.concat(output.scriptPubKey.addresses);
}
}
}
for (const w of wallets) {
for (const t of w.getTransactions()) {
if (t.hash === hash) {
console.log('tx', hash, 'belongs to', w.getLabel());
}
}
}
if (txMetadata[foundTx.hash]) {
if (txMetadata[foundTx.hash].memo) {
setMemo(txMetadata[foundTx.hash].memo);
assert(foundTx, 'Internal error: could not find transaction');
const wallet = wallets.find(w => w.getID() === walletID);
assert(wallet, 'Internal error: could not find wallet');
if (wallet.allowBIP47() && wallet.isBIP47Enabled() && 'getBip47CounterpartyByTxid' in wallet) {
const foundPaymentCode = wallet.getBip47CounterpartyByTxid(hash);
if (foundPaymentCode) {
// okay, this txid _was_ with someone using payment codes, so we show the label edit dialog
// and load user-defined alias for the pc if any
setCounterpartyLabel(counterpartyMetadata ? counterpartyMetadata[foundPaymentCode]?.label ?? '' : '');
setIsCounterpartyLabelVisible(true);
setPaymentCode(foundPaymentCode);
}
}
setMemo(txMetadata[foundTx.hash]?.memo ?? '');
setTX(foundTx);
setFrom(newFrom);
setTo(newTo);
@ -131,7 +149,7 @@ const TransactionsDetails = () => {
);
const handleOnOpenTransactionOnBlockExplorerTapped = () => {
const url = `https://mempool.space/tx/${tx.hash}`;
const url = `https://mempool.space/tx/${tx?.hash}`;
Linking.canOpenURL(url)
.then(supported => {
if (supported) {
@ -155,9 +173,9 @@ const TransactionsDetails = () => {
});
};
const handleCopyPress = stringToCopy => {
const handleCopyPress = (stringToCopy: string) => {
Clipboard.setString(
stringToCopy !== TransactionsDetails.actionKeys.CopyToClipboard ? stringToCopy : `https://mempool.space/tx/${tx.hash}`,
stringToCopy !== TransactionDetails.actionKeys.CopyToClipboard ? stringToCopy : `https://mempool.space/tx/${tx?.hash}`,
);
};
@ -165,7 +183,7 @@ const TransactionsDetails = () => {
return <BlueLoading />;
}
const weOwnAddress = address => {
const weOwnAddress = (address: string): TWallet | null => {
for (const w of wallets) {
if (w.weOwnAddress(address)) {
return w;
@ -174,30 +192,30 @@ const TransactionsDetails = () => {
return null;
};
const navigateToWallet = wallet => {
const walletID = wallet.getID();
const navigateToWallet = (wallet: TWallet) => {
// @ts-ignore idk how to fix it
navigate('WalletTransactions', {
walletID,
walletID: wallet.getID(),
walletType: wallet.type,
});
};
const renderSection = array => {
const renderSection = (array: any[]) => {
const fromArray = [];
for (const [index, address] of array.entries()) {
const actions = [];
actions.push({
id: TransactionsDetails.actionKeys.CopyToClipboard,
id: TransactionDetails.actionKeys.CopyToClipboard,
text: loc.transactions.details_copy,
icon: TransactionsDetails.actionIcons.Clipboard,
icon: TransactionDetails.actionIcons.Clipboard,
});
const isWeOwnAddress = weOwnAddress(address);
if (isWeOwnAddress) {
actions.push({
id: TransactionsDetails.actionKeys.GoToWallet,
id: TransactionDetails.actionKeys.GoToWallet,
text: loc.formatString(loc.transactions.view_wallet, { walletLabel: isWeOwnAddress.getLabel() }),
icon: TransactionsDetails.actionIcons.GoToWallet,
icon: TransactionDetails.actionIcons.GoToWallet,
});
}
@ -208,11 +226,11 @@ const TransactionsDetails = () => {
title={address}
isMenuPrimaryAction
actions={actions}
onPressMenuItem={id => {
if (id === TransactionsDetails.actionKeys.CopyToClipboard) {
onPressMenuItem={(id: string) => {
if (id === TransactionDetails.actionKeys.CopyToClipboard) {
handleCopyPress(address);
} else if (id === TransactionsDetails.actionKeys.GoToWallet) {
navigateToWallet(isWeOwnAddress);
} else if (id === TransactionDetails.actionKeys.GoToWallet) {
isWeOwnAddress && navigateToWallet(isWeOwnAddress);
}
}}
>
@ -243,7 +261,19 @@ const TransactionsDetails = () => {
style={[styles.memoTextInput, stylesHooks.memoTextInput]}
onChangeText={setMemo}
/>
<BlueSpacing20 />
{isCounterpartyLabelVisible ? (
<View>
<BlueSpacing20 />
<TextInput
placeholder={loc.send.counterparty_label_placeholder}
value={counterpartyLabel}
placeholderTextColor="#81868e"
style={[styles.memoTextInput, stylesHooks.memoTextInput]}
onChangeText={setCounterpartyLabel}
/>
<BlueSpacing20 />
</View>
) : null}
</View>
{from && (
@ -268,14 +298,6 @@ const TransactionsDetails = () => {
</>
)}
{tx.fee && (
<>
<BlueText style={styles.rowCaption}>{loc.send.create_fee}</BlueText>
<BlueText style={styles.rowValue}>{tx.fee + ` ${BitcoinUnit.SATS}`}</BlueText>
<View style={styles.marginBottom18} />
</>
)}
{tx.hash && (
<>
<View style={styles.rowHeader}>
@ -295,14 +317,6 @@ const TransactionsDetails = () => {
</>
)}
{tx.block_height > 0 && (
<>
<BlueText style={styles.rowCaption}>{loc.transactions.details_block}</BlueText>
<BlueText style={styles.rowValue}>{tx.block_height}</BlueText>
<View style={styles.marginBottom18} />
</>
)}
{tx.inputs && (
<>
<BlueText style={styles.rowCaption}>{loc.transactions.details_inputs}</BlueText>
@ -322,9 +336,9 @@ const TransactionsDetails = () => {
isButton
actions={[
{
id: TransactionsDetails.actionKeys.CopyToClipboard,
id: TransactionDetails.actionKeys.CopyToClipboard,
text: loc.transactions.copy_link,
icon: TransactionsDetails.actionIcons.Clipboard,
icon: TransactionDetails.actionIcons.Clipboard,
},
]}
onPressMenuItem={handleCopyPress}
@ -338,12 +352,12 @@ const TransactionsDetails = () => {
);
};
TransactionsDetails.actionKeys = {
TransactionDetails.actionKeys = {
CopyToClipboard: 'copyToClipboard',
GoToWallet: 'goToWallet',
};
TransactionsDetails.actionIcons = {
TransactionDetails.actionIcons = {
Clipboard: {
iconType: 'SYSTEM',
iconValue: 'doc.on.doc',
@ -422,9 +436,9 @@ const styles = StyleSheet.create({
},
});
export default TransactionsDetails;
export default TransactionDetails;
TransactionsDetails.navigationOptions = navigationStyle({ headerTitle: loc.transactions.details_title }, (options, { theme }) => {
TransactionDetails.navigationOptions = navigationStyle({ headerTitle: loc.transactions.details_title }, (options, { theme }) => {
return {
...options,
statusBarStyle: 'auto',

View File

@ -30,7 +30,7 @@ import presentAlert from '../../components/Alert';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import A from '../../blue_modules/analytics';
import * as fs from '../../blue_modules/fs';
import { TWallet, Transaction } from '../../class/wallets/types';
import { TWallet, Transaction, ExtendedTransaction } from '../../class/wallets/types';
import { useIsLargeScreen } from '../../hooks/useIsLargeScreen';
import { Header } from '../../components/Header';
@ -263,11 +263,10 @@ const WalletsList: React.FC = () => {
}
};
const renderTransactionListsRow = (data: { item: Transaction }) => {
const renderTransactionListsRow = (item: ExtendedTransaction) => {
return (
<View style={styles.transaction}>
{/** @ts-ignore: Fix later **/}
<TransactionListItem item={data.item} itemPriceUnit={data.item.walletPreferredBalanceUnit} walletID={data.item.walletID} />
<TransactionListItem item={item} itemPriceUnit={item.walletPreferredBalanceUnit} walletID={item.walletID} />
</View>
);
};
@ -289,13 +288,12 @@ const WalletsList: React.FC = () => {
);
};
const renderSectionItem = (item: { section?: any; item?: Transaction }) => {
const renderSectionItem = (item: { section: any; item: ExtendedTransaction }) => {
switch (item.section.key) {
case WalletsListSections.CAROUSEL:
return isLargeScreen ? null : renderWalletsCarousel();
case WalletsListSections.TRANSACTIONS:
/* @ts-ignore: fix later */
return renderTransactionListsRow(item);
return renderTransactionListsRow(item.item);
default:
return null;
}

View File

@ -7,7 +7,6 @@ import {
InteractionManager,
Keyboard,
KeyboardAvoidingView,
Linking,
Platform,
ScrollView,
StyleSheet,
@ -18,11 +17,7 @@ import {
TouchableWithoutFeedback,
View,
} from 'react-native';
import RNFS from 'react-native-fs';
import { PERMISSIONS, RESULTS, request } from 'react-native-permissions';
import Share from 'react-native-share';
import { BlueCard, BlueLoading, BlueSpacing10, BlueSpacing20, BlueText } from '../../BlueComponents';
import { isDesktop } from '../../blue_modules/environment';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import Notifications from '../../blue_modules/notifications';
import { BlueStorageContext } from '../../blue_modules/storage-context';
@ -51,6 +46,7 @@ import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
import SaveFileButton from '../../components/SaveFileButton';
import { useSettings } from '../../components/Context/SettingsContext';
import { HeaderRightButton } from '../../components/HeaderRightButton';
import { writeFileAndExport } from '../../blue_modules/fs';
const styles = StyleSheet.create({
scrollViewContent: {
@ -332,46 +328,8 @@ const WalletDetails = () => {
null,
2,
);
if (Platform.OS === 'ios') {
const filePath = RNFS.TemporaryDirectoryPath + `/${fileName}`;
await RNFS.writeFile(filePath, contents);
Share.open({
url: 'file://' + filePath,
saveToFiles: isDesktop,
failOnCancel: false,
})
.catch(error => {
console.log(error);
})
.finally(() => {
RNFS.unlink(filePath);
});
} else if (Platform.OS === 'android') {
const granted = await request(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE);
if (granted === RESULTS.GRANTED) {
console.log('Storage Permission: Granted');
const filePath = RNFS.DownloadDirectoryPath + `/${fileName}`;
try {
await RNFS.writeFile(filePath, contents);
presentAlert({ message: loc.formatString(loc.send.txSaved, { filePath: fileName }) });
} catch (e) {
console.log(e);
presentAlert({ message: e.message });
}
} else {
console.log('Storage Permission: Denied');
Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [
{
text: loc.send.open_settings,
onPress: () => {
Linking.openSettings();
},
style: 'default',
},
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
]);
}
}
await writeFileAndExport(fileName, contents, false);
};
const purgeTransactions = async () => {

View File

@ -1,6 +1,6 @@
import assert from 'assert';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SegwitP2SHWallet, BlueApp } from '../../class';
import { SegwitP2SHWallet, BlueApp, HDSegwitBech32Wallet } from '../../class';
jest.mock('../../blue_modules/BlueElectrum', () => {
return {
@ -15,6 +15,16 @@ it('Appstorage - loadFromDisk works', async () => {
w.setLabel('testlabel');
await w.generate();
Storage.wallets.push(w);
Storage.tx_metadata = {
txid: {
memo: 'tx label',
},
};
Storage.counterparty_metadata = {
'payment code': {
label: 'yegor letov',
},
};
await Storage.saveToDisk();
// saved, now trying to load
@ -23,6 +33,8 @@ it('Appstorage - loadFromDisk works', async () => {
await Storage2.loadFromDisk();
assert.strictEqual(Storage2.wallets.length, 1);
assert.strictEqual(Storage2.wallets[0].getLabel(), 'testlabel');
assert.strictEqual(Storage2.tx_metadata.txid.memo, 'tx label');
assert.strictEqual(Storage2.counterparty_metadata['payment code'].label, 'yegor letov');
let isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(!isEncrypted);
@ -35,6 +47,156 @@ it('Appstorage - loadFromDisk works', async () => {
assert.ok(isEncrypted);
});
it('AppStorage - getTransactions() work', async () => {
const Storage = new BlueApp();
const w = new HDSegwitBech32Wallet();
w.setLabel('testlabel');
await w.generate();
w._txs_by_internal_index = {
0: [
{
blockhash: '000000000000000000054fae1935a8e5c3ac29ce04a45cca25d7329af5e5db2e',
blocktime: 1678137003,
confirmations: 61788,
hash: '73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d',
locktime: 0,
size: 192,
time: 1678137003,
txid: '73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d',
version: 1,
vsize: 110,
weight: 438,
inputs: [
{
scriptSig: {
asm: '',
hex: '',
},
sequence: 4294967295,
txid: '06b4c14587182fd0474f265a77b156519b4778769a99c21623863a8194d0fa4f',
txinwitness: [
'3045022100f2dfd9679719a5b10695c5142cb2998c0dde9d84fb3a0f6e2f82c972846da2b10220059c34862231eda0b8b4059859ae55e2fca5739c664f3ff45be71fbcf438a68d01',
'034f150e09d0489a047b1299131180ce174769b28c03ca6a96054555211fdd7fd6',
],
vout: 3,
addresses: ['bc1qtnsyvl8zkteg7ap57j6w8hc7gk5nxk8vj5vrmz'],
value: 0.00077308,
},
],
outputs: [
{
n: 0,
scriptPubKey: {
address: 'bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt',
asm: '0 e98d8aa1c6d0dba1936079c0e09af9836b2070b1',
desc: 'addr(bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt)#pl83f4nc',
hex: '0014e98d8aa1c6d0dba1936079c0e09af9836b2070b1',
type: 'witness_v0_keyhash',
addresses: ['bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt'],
},
value: 0.00074822,
},
],
received: 1678137003000,
value: -77308,
sort_ts: 1678137003000,
},
],
};
const w2 = new HDSegwitBech32Wallet();
w2.setLabel('testlabel');
await w2.generate();
w2._txs_by_internal_index = {
0: [
{
blockhash: '000000000000000000054fae1935a8e5c3ac29ce04a45cca25d7329af5e5db2e',
blocktime: 1678137003,
confirmations: 61788,
hash: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
locktime: 0,
size: 192,
time: 1678137003,
txid: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
version: 1,
vsize: 110,
weight: 438,
inputs: [
{
scriptSig: {
asm: '',
hex: '',
},
sequence: 4294967295,
txid: '06b4c14587182fd0474f265a77b156519b4778769a99c21623863a8194d0fa4f',
txinwitness: [
'3045022100f2dfd9679719a5b10695c5142cb2998c0dde9d84fb3a0f6e2f82c972846da2b10220059c34862231eda0b8b4059859ae55e2fca5739c664f3ff45be71fbcf438a68d01',
'034f150e09d0489a047b1299131180ce174769b28c03ca6a96054555211fdd7fd6',
],
vout: 3,
addresses: ['bc1qtnsyvl8zkteg7ap57j6w8hc7gk5nxk8vj5vrmz'],
value: 0.00077308,
},
],
outputs: [
{
n: 0,
scriptPubKey: {
address: 'bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt',
asm: '0 e98d8aa1c6d0dba1936079c0e09af9836b2070b1',
desc: 'addr(bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt)#pl83f4nc',
hex: '0014e98d8aa1c6d0dba1936079c0e09af9836b2070b1',
type: 'witness_v0_keyhash',
addresses: ['bc1qaxxc4gwx6rd6rymq08qwpxhesd4jqu93lvjsyt'],
},
value: 0.00074822,
},
],
received: 1678137003000,
value: -77308,
sort_ts: 1678137003000,
},
],
};
Storage.wallets.push(w);
Storage.wallets.push(w2);
// setup complete. now we have a storage with 2 wallets, each wallet has
// exactly one transaction
let txs = Storage.getTransactions();
assert.strictEqual(txs.length, 2); // getter for _all_ txs works
for (const tx of txs) {
assert.ok([w.getID(), w2.getID()].includes(tx.walletID));
assert.strictEqual(tx.walletPreferredBalanceUnit, w.getPreferredBalanceUnit());
assert.strictEqual(tx.walletPreferredBalanceUnit, 'BTC');
}
//
txs = Storage.getTransactions(0, 666, true);
assert.strictEqual(txs.length, 1); // getter for a specific wallet works
for (const tx of txs) {
assert.ok([w.getID()].includes(tx.walletID));
assert.strictEqual(tx.walletPreferredBalanceUnit, w.getPreferredBalanceUnit());
assert.strictEqual(tx.walletPreferredBalanceUnit, 'BTC');
}
//
txs = Storage.getTransactions(1, 666, true);
assert.strictEqual(txs.length, 1); // getter for a specific wallet works
for (const tx of txs) {
assert.ok([w2.getID()].includes(tx.walletID));
assert.strictEqual(tx.walletPreferredBalanceUnit, w.getPreferredBalanceUnit());
assert.strictEqual(tx.walletPreferredBalanceUnit, 'BTC');
}
});
it('Appstorage - encryptStorage & load encrypted storage works', async () => {
/** @type {BlueApp} */
const Storage = new BlueApp();