ADD: display bip47 payment code contact on tx details screen

This commit is contained in:
overtorment 2024-05-05 20:27:43 +01:00
parent 1384c8453f
commit 438a671299
6 changed files with 135 additions and 52 deletions

View File

@ -591,6 +591,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber();
}
}
if (this.allowBIP47() && this.isBIP47Enabled()) {
tx.counterparty = this.getBip47CounterpartyByTx(tx);
}
ret.push(tx);
}
@ -1557,16 +1561,28 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
/**
* return BIP47 payment code of the counterparty of this transaction (someone paid us, or we paid someone)
* or false if it was a non-BIP47 transaction
* return BIP47 payment code of the counterparty of this transaction (someone who paid us, or someone we paid)
* or undefined if it was a non-BIP47 transaction
*/
getSenderByTxid(txid: string): string | false {
getBip47CounterpartyByTxid(txid: string): string | undefined {
const foundTx = this.getTransactions().find(tx => tx.txid === txid);
if (foundTx) {
return this.getBip47CounterpartyByTx(foundTx);
}
return undefined;
}
/**
* return BIP47 payment code of the counterparty of this transaction (someone who paid us, or someone we paid)
* or undefined if it was a non-BIP47 transaction
*/
getBip47CounterpartyByTx(tx: Transaction): string | undefined {
for (const pc of Object.keys(this._txs_by_payment_code_index)) {
// iterating all payment codes
for (const txs of Object.values(this._txs_by_payment_code_index[pc])) {
for (const tx of txs) {
if (tx.txid === txid) {
for (const tx2 of txs) {
if (tx2.txid === tx.txid) {
return pc; // found it!
}
}
@ -1576,7 +1592,6 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
// checking txs we sent to counterparties
for (const pc of this._send_payment_codes) {
for (const tx of this.getTransactions().filter(transaction => transaction.txid === txid)) {
for (const out of tx.outputs) {
for (const address of out.scriptPubKey?.addresses ?? []) {
if (this._addresses_by_payment_code_send[pc] && Object.values(this._addresses_by_payment_code_send[pc]).includes(address)) {
@ -1586,9 +1601,8 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
}
}
}
}
return false; // found nothing
return undefined; // found nothing
}
createBip47NotificationTransaction(utxos: CreateTransactionUtxo[], receiverPaymentCode: string, feeRate: number, changeAddress: string) {

View File

@ -78,6 +78,17 @@ export type TransactionOutput = {
};
};
export type LightningTransaction = {
memo?: string;
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';
payment_hash?: string | { data: string };
category?: 'receive';
timestamp?: number;
expire_time?: number;
ispaid?: boolean;
walletID?: string;
};
export type Transaction = {
txid: string;
hash: string;
@ -94,6 +105,11 @@ export type Transaction = {
blocktime: number;
received?: number;
value?: number;
/**
* if known, who is on the other end of the transaction (BIP47 payment code)
*/
counterparty?: string;
};
export type TWallet =

View File

@ -1,5 +1,4 @@
/* eslint react/prop-types: "off" */
import React, { useState, useMemo, useCallback, useContext, useEffect, useRef } from 'react';
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Linking, StyleSheet, View } from 'react-native';
import Clipboard from '@react-native-clipboard/clipboard';
import AsyncStorage from '@react-native-async-storage/async-storage';
@ -20,8 +19,15 @@ import TransactionPendingIcon from '../components/icons/TransactionPendingIcon';
import { useTheme } from './themes';
import ListItem from './ListItem';
import { useSettings } from './Context/SettingsContext';
import { LightningTransaction, Transaction } from '../class/wallets/types';
export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUnit.BTC, walletID }) => {
interface TransactionListItemProps {
itemPriceUnit: BitcoinUnit;
walletID: string;
item: Transaction & LightningTransaction; // using type intersection to have less issues with ts
}
export const TransactionListItem: React.FC<TransactionListItemProps> = React.memo(({ item, itemPriceUnit = BitcoinUnit.BTC, walletID }) => {
const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1);
const { colors } = useTheme();
const { navigate } = useNavigation();
@ -39,17 +45,22 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
[colors.lightBorder],
);
const shortenContactName = (addr: string): string => {
return addr.substr(0, 5) + '...' + addr.substr(addr.length - 4, 4);
};
const title = useMemo(() => {
if (item.confirmations === 0) {
return loc.transactions.pending;
} else {
return transactionTimeToReadable(item.received);
return transactionTimeToReadable(item.received!);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item.confirmations, item.received, language]);
const txMemo = txMetadata[item.hash]?.memo ?? '';
const txMemo = (item.counterparty ? `[${shortenContactName(item.counterparty)}] ` : '') + (txMetadata[item.hash]?.memo ?? '');
const subtitle = useMemo(() => {
let sub = item.confirmations < 7 ? loc.formatString(loc.transactions.list_conf, { number: item.confirmations }) : '';
let sub = Number(item.confirmations) < 7 ? loc.formatString(loc.transactions.list_conf, { number: item.confirmations }) : '';
if (sub !== '') sub += ' ';
sub += txMemo;
if (item.memo) sub += item.memo;
@ -58,12 +69,12 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
const rowTitle = useMemo(() => {
if (item.type === 'user_invoice' || item.type === 'payment_request') {
if (isNaN(item.value)) {
item.value = '0';
if (isNaN(Number(item.value))) {
item.value = 0;
}
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp + item.expire_time;
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (invoiceExpiration > now) {
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
@ -86,7 +97,7 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
if (item.type === 'user_invoice' || item.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp + item.expire_time;
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (invoiceExpiration > now) {
color = colors.successColor;
@ -97,7 +108,7 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
color = '#9AA0AA';
}
}
} else if (item.value / 100000000 < 0) {
} else if (item.value! / 100000000 < 0) {
color = colors.foregroundColor;
}
@ -112,7 +123,7 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
const avatar = useMemo(() => {
// is it lightning refill tx?
if (item.category === 'receive' && item.confirmations < 3) {
if (item.category === 'receive' && item.confirmations! < 3) {
return (
<View style={styles.iconWidth}>
<TransactionPendingIcon />
@ -140,7 +151,7 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
if (!item.ispaid) {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = item.timestamp + item.expire_time;
const invoiceExpiration = item.timestamp! + item.expire_time!;
if (invoiceExpiration < now) {
return (
<View style={styles.iconWidth}>
@ -163,7 +174,7 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
<TransactionPendingIcon />
</View>
);
} else if (item.value < 0) {
} else if (item.value! < 0) {
return (
<View style={styles.iconWidth}>
<TransactionOutgoingIcon />
@ -183,8 +194,10 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
}, [subtitle]);
const onPress = useCallback(async () => {
menuRef?.current?.dismissMenu();
// @ts-ignore: idk how to fix
menuRef?.current?.dismissMenu?.();
if (item.hash) {
// @ts-ignore: idk how to fix
navigate('TransactionStatus', { hash: item.hash, walletID });
} else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') {
const lightningWallet = wallets.filter(wallet => wallet?.getID() === item.walletID);
@ -192,13 +205,14 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
try {
// is it a successful lnurl-pay?
const LN = new Lnurl(false, AsyncStorage);
let paymentHash = item.payment_hash;
let paymentHash = item.payment_hash!;
if (typeof paymentHash === 'object') {
paymentHash = Buffer.from(paymentHash.data).toString('hex');
}
const loaded = await LN.loadSuccessfulPayment(paymentHash);
if (loaded) {
NavigationService.navigate('ScanLndInvoiceRoot', {
// @ts-ignore: idk how to fix
screen: 'LnurlPaySuccess',
params: {
paymentHash,
@ -212,6 +226,7 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
console.log(e);
}
// @ts-ignore: idk how to fix
navigate('LNDViewInvoice', {
invoice: item,
walletID: lightningWallet[0].getID(),
@ -244,18 +259,18 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
}, [item.hash]);
const onToolTipPress = useCallback(
id => {
if (id === TransactionListItem.actionKeys.CopyAmount) {
(id: any) => {
if (id === actionKeys.CopyAmount) {
handleOnCopyAmountTap();
} else if (id === TransactionListItem.actionKeys.CopyNote) {
} else if (id === actionKeys.CopyNote) {
handleOnCopyNote();
} else if (id === TransactionListItem.actionKeys.OpenInBlockExplorer) {
} else if (id === actionKeys.OpenInBlockExplorer) {
handleOnViewOnBlockExplorer();
} else if (id === TransactionListItem.actionKeys.ExpandNote) {
} else if (id === actionKeys.ExpandNote) {
handleOnExpandNote();
} else if (id === TransactionListItem.actionKeys.CopyBlockExplorerLink) {
} else if (id === actionKeys.CopyBlockExplorerLink) {
handleCopyOpenInBlockExplorerPress();
} else if (id === TransactionListItem.actionKeys.CopyTXID) {
} else if (id === actionKeys.CopyTXID) {
handleOnCopyTransactionID();
}
},
@ -273,36 +288,36 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
const actions = [];
if (rowTitle !== loc.lnd.expired) {
actions.push({
id: TransactionListItem.actionKeys.CopyAmount,
id: actionKeys.CopyAmount,
text: loc.transactions.details_copy_amount,
icon: TransactionListItem.actionIcons.Clipboard,
icon: actionIcons.Clipboard,
});
}
if (subtitle) {
actions.push({
id: TransactionListItem.actionKeys.CopyNote,
id: actionKeys.CopyNote,
text: loc.transactions.details_copy_note,
icon: TransactionListItem.actionIcons.Clipboard,
icon: actionIcons.Clipboard,
});
}
if (item.hash) {
actions.push(
{
id: TransactionListItem.actionKeys.CopyTXID,
id: actionKeys.CopyTXID,
text: loc.transactions.details_copy_txid,
icon: TransactionListItem.actionIcons.Clipboard,
icon: actionIcons.Clipboard,
},
{
id: TransactionListItem.actionKeys.CopyBlockExplorerLink,
id: actionKeys.CopyBlockExplorerLink,
text: loc.transactions.details_copy_block_explorer_link,
icon: TransactionListItem.actionIcons.Clipboard,
icon: actionIcons.Clipboard,
},
[
{
id: TransactionListItem.actionKeys.OpenInBlockExplorer,
id: actionKeys.OpenInBlockExplorer,
text: loc.transactions.details_show_in_block_explorer,
icon: TransactionListItem.actionIcons.Link,
icon: actionIcons.Link,
},
],
);
@ -311,9 +326,9 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
if (subtitle && subtitleNumberOfLines === 1) {
actions.push([
{
id: TransactionListItem.actionKeys.ExpandNote,
id: actionKeys.ExpandNote,
text: loc.transactions.expand_note,
icon: TransactionListItem.actionIcons.Note,
icon: actionIcons.Note,
},
]);
}
@ -326,6 +341,7 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
<View style={styles.container}>
<ToolTipMenu ref={menuRef} actions={toolTipActions} onPressMenuItem={onToolTipPress} onPress={onPress}>
<ListItem
// @ts-ignore wtf
leftAvatar={avatar}
title={title}
subtitleNumberOfLines={subtitleNumberOfLines}
@ -342,7 +358,7 @@ export const TransactionListItem = React.memo(({ item, itemPriceUnit = BitcoinUn
);
});
TransactionListItem.actionKeys = {
const actionKeys = {
CopyTXID: 'copyTX_ID',
CopyBlockExplorerLink: 'copy_blockExplorer',
ExpandNote: 'expandNote',
@ -351,7 +367,7 @@ TransactionListItem.actionKeys = {
CopyNote: 'copyNote',
};
TransactionListItem.actionIcons = {
const actionIcons = {
Eye: {
iconType: 'SYSTEM',
iconValue: 'eye',

View File

@ -373,6 +373,8 @@
"status_cancel": "Cancel Transaction",
"transactions_count": "Transactions Count",
"txid": "Transaction ID",
"from": "From: {counterparty}",
"to": "To: {counterparty}",
"updating": "Updating..."
},
"wallets": {

View File

@ -426,7 +426,14 @@ const TransactionsStatus = () => {
}
};
const shortenCounterpartyName = (addr: string): string => {
if (addr.length < 20) return addr;
return addr.substr(0, 10) + '...' + addr.substr(addr.length - 10, 10);
};
const renderTXMetadata = () => {
const counterparty = tx.counterparty ? shortenCounterpartyName(tx.counterparty) : false;
if (txMetadata[tx.hash]) {
if (txMetadata[tx.hash].memo) {
return (
@ -434,6 +441,20 @@ 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>
);

View File

@ -207,7 +207,7 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
);
assert.strictEqual(
w.getSenderByTxid('64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f'),
w.getBip47CounterpartyByTxid('64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f'),
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
); // we got paid
@ -240,8 +240,22 @@ describe('Bech32 Segwit HD (BIP84) with BIP47', () => {
);
assert.strictEqual(
w.getSenderByTxid('73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d'),
w.getBip47CounterpartyByTxid('73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d'),
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
); // we paid sparrow
let txWithCounterparty = w.getTransactions().find(tx => tx.txid === '73a2ac70858c5b306b101a861d582f40c456a692096a4e4805aa739258c4400d');
assert(txWithCounterparty);
assert.strictEqual(
txWithCounterparty.counterparty,
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
);
txWithCounterparty = w.getTransactions().find(tx => tx.txid === '64058a49bb75481fc0bebbb0d84a4aceebe319f9d32929e73cefb21d83342e9f');
assert(txWithCounterparty);
assert.strictEqual(
txWithCounterparty.counterparty,
'PM8TJi1RuCrgSHTzGMoayUf8xUW6zYBGXBPSWwTiMhMMwqto7G6NA4z9pN5Kn8Pbhryo2eaHMFRRcidCGdB3VCDXJD4DdPD2ZyG3ScLMEvtStAetvPMo',
);
});
});