mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 09:50:15 +01:00
ADD: display bip47 payment code contact on tx details screen
This commit is contained in:
parent
1384c8453f
commit
438a671299
@ -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,19 +1592,17 @@ 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)) {
|
||||
// found it!
|
||||
return pc;
|
||||
}
|
||||
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)) {
|
||||
// found it!
|
||||
return pc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false; // found nothing
|
||||
return undefined; // found nothing
|
||||
}
|
||||
|
||||
createBip47NotificationTransaction(utxos: CreateTransactionUtxo[], receiverPaymentCode: string, feeRate: number, changeAddress: string) {
|
||||
|
@ -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 =
|
||||
|
@ -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',
|
||||
|
@ -373,6 +373,8 @@
|
||||
"status_cancel": "Cancel Transaction",
|
||||
"transactions_count": "Transactions Count",
|
||||
"txid": "Transaction ID",
|
||||
"from": "From: {counterparty}",
|
||||
"to": "To: {counterparty}",
|
||||
"updating": "Updating..."
|
||||
},
|
||||
"wallets": {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user