Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const WalletLineItem = ({
cancelling,
onCancelPress,
onRepeatPress,
onCopyPress,
}: any) => (
<TouchableOpacity onPress={onPress} disabled={!onPress} activeOpacity={0.8}>
<GrayWrapper isGray={index && index % 2 !== 0}>
Expand Down Expand Up @@ -132,6 +133,21 @@ const WalletLineItem = ({
/>
)}

{!!onCopyPress && (
<IconButton
backgroundColor="transparent"
name="content-copy"
iconType="MaterialIcons"
size={18}
onPress={() => {
onCopyPress();
}}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
color="#c1c5c7"
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated
isLoading={false}
style={styles.repeatContainer}
/>
)}

{isHasdropdown && (
<View style={styles.dropdownWrapper}>
<DropdownButton
Expand Down
51 changes: 43 additions & 8 deletions src/components/transaction/transactionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ import { getTimeFromNow } from '../../utils/time';
// Components
import { WalletLineItem } from '../basicUIElements';
import { getHumanReadableKeyString } from '../../utils/strings';
import { getTransactionExplorerUrl } from '../../utils/transactionExplorer';
import { writeToClipboard } from '../../utils/clipboard';
import { useAppDispatch } from '../../hooks';
import { toastNotification } from '../../redux/actions/uiAction';

const TransactionView = ({ item, index, cancelling, onCancelPress, onRepeatPress }: any) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const [collapsed, setCollapsed] = useState(true);

// Absent on every virtual operation, so the row has to be able to render without it.
const explorerUrl = getTransactionExplorerUrl(item.trxId);

const title = intl.messages[`wallet.${item.textKey}`]
? intl.formatMessage({
id: `wallet.${item.textKey}`,
Expand Down Expand Up @@ -49,6 +57,19 @@ const TransactionView = ({ item, index, cancelling, onCancelPress, onRepeatPress
}
};

// The explorer link rather than the bare id: it is the form that is useful to paste to
// someone else, and the id is still readable on screen for anyone who wants only that.
const _onCopyTrxIdPress = async () => {
if (!explorerUrl) {
return;
}

const copied = await writeToClipboard(explorerUrl);
if (copied) {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.copied' })));
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
};

const _cardHeader = (
<WalletLineItem
key={`keyt-${item.created.toString()}`}
Expand Down Expand Up @@ -76,15 +97,29 @@ const TransactionView = ({ item, index, cancelling, onCancelPress, onRepeatPress
/>
);

const _cardBody = (get(item, 'details') || get(item, 'memo')) && !collapsed && (
const _hasDetails = !!(get(item, 'details') || get(item, 'memo'));

const _cardBody = (_hasDetails || !!explorerUrl) && !collapsed && (
<Animated.View entering={SlideInLeft.duration(200)}>
<WalletLineItem
key={`keyd-${item.created.toString()}`}
text={get(item, 'details', '')}
isBlackText
isThin
description={get(item, 'memo')}
/>
{_hasDetails && (
<WalletLineItem
key={`keyd-${item.created.toString()}`}
text={get(item, 'details', '')}
isBlackText
isThin
description={get(item, 'memo')}
/>
)}
{!!explorerUrl && (
<WalletLineItem
key={`keyx-${item.created.toString()}`}
text={intl.formatMessage({ id: 'wallet.transaction_id' })}
isBlackText
isThin
description={item.trxId}
onCopyPress={_onCopyTrxIdPress}
/>
)}
</Animated.View>
);

Expand Down
1 change: 1 addition & 0 deletions src/config/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,7 @@
"purchase_estm": "GET POINTS",
"escrow_approve": "Escrow Approve",
"expires": "expires",
"transaction_id": "Transaction ID",
"author_reward": "Author Reward",
"withdraw_hbd": "Withdraw Savings",
"convert_request": "Convert Request",
Expand Down
6 changes: 6 additions & 0 deletions src/redux/reducers/walletReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ export interface PriceHistory {
export interface CoinActivity {
trxIndex: number;
engineTrxId?: string;
/**
* The Hive transaction this operation was broadcast in, or undefined when there is
* none: a virtual operation is emitted by the chain rather than broadcast, and a Hive
* Engine row can be generated by a contract with no Hive transaction behind it.
*/
trxId?: string;
iconType: string;
textKey?: string;
created?: string;
Expand Down
65 changes: 65 additions & 0 deletions src/utils/transactionExplorer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { getTransactionExplorerUrl, resolveTrxId } from './transactionExplorer';

describe('resolveTrxId', () => {
it('accepts a broadcast transaction id', () => {
expect(resolveTrxId('aa82751091f8eaf6977f9a634e9eff6c4ee4208d')).toBe(
'aa82751091f8eaf6977f9a634e9eff6c4ee4208d',
);
});

// A virtual operation is emitted by the chain rather than broadcast, so
// get_account_history gives it a zeroed id. author_reward, curation_reward and
// fill_order are the common ones and they are the majority of rows on some tabs.
it('rejects the zeroed id a virtual operation carries', () => {
expect(resolveTrxId('0000000000000000000000000000000000000000')).toBeUndefined();
});

it('rejects a missing id', () => {
expect(resolveTrxId(undefined)).toBeUndefined();
expect(resolveTrxId(null)).toBeUndefined();
expect(resolveTrxId('')).toBeUndefined();
});

// Hive Engine suffixes the operation's index within the transaction.
it('strips the Hive Engine operation index', () => {
expect(resolveTrxId('98c14ebb580b350d6f1538c2810be987cb0003db-0')).toBe(
'98c14ebb580b350d6f1538c2810be987cb0003db',
);
expect(resolveTrxId('7f333bb6e0e3dc8173ea78a857134508db70955b-9')).toBe(
'7f333bb6e0e3dc8173ea78a857134508db70955b',
);
});

// A contract-generated Hive Engine row is `<block>-<index>` with no Hive transaction
// behind it at all. 184 of 500 rows on the account this was written against.
it('rejects a Hive Engine row with no Hive transaction', () => {
expect(resolveTrxId('109092570-0')).toBeUndefined();
expect(resolveTrxId('109087215-4')).toBeUndefined();
});

it('rejects anything that is not a transaction id', () => {
expect(resolveTrxId('not-a-transaction')).toBeUndefined();
expect(resolveTrxId('aa82751091')).toBeUndefined();
expect(resolveTrxId(`${'a'.repeat(41)}`)).toBeUndefined();
});

it('normalises case and surrounding whitespace', () => {
expect(resolveTrxId(' AA82751091F8EAF6977F9A634E9EFF6C4EE4208D ')).toBe(
'aa82751091f8eaf6977f9a634e9eff6c4ee4208d',
);
});
});

describe('getTransactionExplorerUrl', () => {
it('builds the explorer link', () => {
expect(getTransactionExplorerUrl('aa82751091f8eaf6977f9a634e9eff6c4ee4208d')).toBe(
'https://hivexplorer.com/tx/aa82751091f8eaf6977f9a634e9eff6c4ee4208d',
);
});

it('offers no link where there is no transaction', () => {
expect(getTransactionExplorerUrl('0000000000000000000000000000000000000000')).toBeUndefined();
Comment on lines +60 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Lines exceed 100 characters 📘 Rule violation ⚙ Maintainability

New test lines exceed the 100 character limit, reducing readability and violating the line-length
policy. Wrap these expectations across multiple lines to keep each non-comment line within 100
characters.
Agent Prompt
## Issue description
Some newly added non-comment lines exceed 100 characters.

## Issue Context
The project compliance rule requires keeping non-comment, non-whitespace lines at or under 100 characters unless an approved exception mechanism is used.

## Fix Focus Areas
- src/utils/transactionExplorer.test.ts[60-63]
- src/utils/wallet.test.ts[417-424]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

expect(getTransactionExplorerUrl('109092570-0')).toBeUndefined();
expect(getTransactionExplorerUrl(undefined)).toBeUndefined();
});
});
46 changes: 46 additions & 0 deletions src/utils/transactionExplorer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Turning a wallet activity into a link someone else can verify.
*
* Not every row has a transaction behind it, and the ones that do not are the majority on
* some tabs, so the caller has to be able to tell them apart rather than build a link that
* resolves to nothing:
*
* - A virtual operation (`author_reward`, `curation_reward`, `fill_order`, ...) is emitted
* by the chain instead of broadcast, and `condenser_api.get_account_history` gives it a
* `trx_id` of 40 zeroes.
* - A Hive Engine row carries `<hive-trx-id>-<op index>` when it rode in on a custom_json,
* and `<block>-<index>` when a contract generated it with no Hive transaction at all.
* Both shapes appear in the same account's history (184 of 500 rows on the account this
* was written against).
*/

const EXPLORER_BASE = 'https://hivexplorer.com';

/** A Hive transaction id is 40 hex characters. All zeroes is the "no transaction" marker. */
const TRX_ID_PATTERN = /^[0-9a-f]{40}$/i;

/** Hive Engine appends the operation's index within the transaction. */
const OP_INDEX_SUFFIX = /-\d+$/;

/**
* The Hive transaction id behind an activity, or undefined when it has none.
*/
export const resolveTrxId = (rawId?: string | null): string | undefined => {
const candidate = String(rawId ?? '')
.trim()
.replace(OP_INDEX_SUFFIX, '');

if (!TRX_ID_PATTERN.test(candidate) || /^0+$/.test(candidate)) {
return undefined;
}

return candidate.toLowerCase();
};

/**
* The explorer link for an already-resolved transaction id.
*/
export const getTransactionExplorerUrl = (trxId?: string | null): string | undefined => {
const resolved = resolveTrxId(trxId);
return resolved ? `${EXPLORER_BASE}/tx/${resolved}` : undefined;
};
68 changes: 68 additions & 0 deletions src/utils/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,54 @@ describe('groomingTransactionData', () => {
expect(result).not.toBeNull();
expect(result!.value).toContain('HP');
});

describe('transaction id', () => {
it('carries the id of a broadcast operation', () => {
const tx = {
type: 'transfer',
timestamp: '2024-01-01T00:00:00',
num: 1,
trx_id: 'aa82751091f8eaf6977f9a634e9eff6c4ee4208d',
amount: '10.000 HIVE',
from: 'alice',
to: 'bob',
};
expect(groomingTransactionData(tx, hivePerMVests)!.trxId).toBe(
'aa82751091f8eaf6977f9a634e9eff6c4ee4208d',
);
});

// The chain emits a virtual operation instead of anyone broadcasting it, so its
// trx_id is 40 zeroes and there is nothing to link to.
it('leaves a virtual operation without one', () => {
const tx = {
type: 'author_reward',
timestamp: '2024-01-01T00:00:00',
num: 2,
trx_id: '0000000000000000000000000000000000000000',
hbd_payout: '0.000 HBD',
hive_payout: '1.000 HIVE',
vesting_payout: '0.000000 VESTS',
author: 'alice',
permlink: 'p',
};
expect(groomingTransactionData(tx, hivePerMVests)!.trxId).toBeUndefined();
});

it('reads the id off the legacy tuple shape too', () => {
const tx = [
3,
{
timestamp: '2024-01-01T00:00:00',
trx_id: 'aa82751091f8eaf6977f9a634e9eff6c4ee4208d',
op: ['transfer', { amount: '1.000 HIVE', from: 'alice', to: 'bob', memo: '' }],
},
];
expect(groomingTransactionData(tx, hivePerMVests)!.trxId).toBe(
'aa82751091f8eaf6977f9a634e9eff6c4ee4208d',
);
});
});
});

describe('groomingEngineHistory', () => {
Expand Down Expand Up @@ -364,6 +412,26 @@ describe('groomingEngineHistory', () => {
expect(groomingEngineHistory(tx)!.icon).toBe('compare-arrows');
});

// Hive Engine suffixes the Hive transaction with the operation's index, and a row a
// contract generated carries `<block>-<index>` with no Hive transaction behind it.
it('carries the Hive transaction the custom_json rode in on', () => {
const tx = {
...baseTx,
operation: EngineOperations.TOKENS_TRANSFER,
transactionId: '98c14ebb580b350d6f1538c2810be987cb0003db-0',
};
expect(groomingEngineHistory(tx)!.trxId).toBe('98c14ebb580b350d6f1538c2810be987cb0003db');
});

it('leaves a contract-generated row without one', () => {
const tx = {
...baseTx,
operation: EngineOperations.TOKENS_TRANSFER,
transactionId: '109092570-0',
};
expect(groomingEngineHistory(tx)!.trxId).toBeUndefined();
});

it('sets fiber-new icon for token creation', () => {
const tx = { ...baseTx, operation: EngineOperations.TOKENS_CREATE };
expect(groomingEngineHistory(tx)!.icon).toBe('fiber-new');
Expand Down
9 changes: 9 additions & 0 deletions src/utils/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CoinActivity } from '../redux/reducers/walletReducer';

import { EngineOperations, HistoryItem } from '../providers/hive-engine/hiveEngine.types';
import { RepeatableTransfers } from '../constants/repeatableTransfers';
import { resolveTrxId } from './transactionExplorer';

export const transferTypes = [
'curation_reward',
Expand Down Expand Up @@ -54,10 +55,14 @@ export const groomingTransactionData = (
const opData = isLegacy ? transaction[1]?.op?.[1] : transaction;
const timestamp = isLegacy ? transaction[1]?.timestamp : transaction.timestamp;
const trxIndex = isLegacy ? transaction[0] : transaction.num;
// Left undefined for a virtual operation, whose trx_id is 40 zeroes because the chain
// emitted it rather than anyone broadcasting it.
const trxId = resolveTrxId(isLegacy ? transaction[1]?.trx_id : transaction.trx_id);

const result: CoinActivity = {
iconType: 'MaterialIcons',
trxIndex,
trxId,
};

result.textKey = opType;
Expand Down Expand Up @@ -235,6 +240,10 @@ export const groomingEngineHistory = (transaction: HistoryItem): CoinActivity |
iconType: 'MaterialIcons',
trxIndex: blockNumber,
engineTrxId: transaction?.id || transaction?._id,
// `transactionId` is the Hive transaction the custom_json rode in on, suffixed with
// the operation's index. A contract-generated row carries `<block>-<index>` instead,
// which resolves to undefined rather than to a link that goes nowhere.
trxId: resolveTrxId(transaction?.transactionId),
textKey: operation,
created: new Date(timestamp).toISOString(),
value: `${quantity} ${symbol}`,
Expand Down
Loading