mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-02-20 00:33:07 +01:00
feat: abstract target/output to use it in native
test: implement test for createTargets
This commit is contained in:
committed by
Peter Sanderson
parent
f373e4c9f5
commit
5da09763f3
@@ -187,7 +187,7 @@ const addAccountMetadata: Fixture<(typeof metadataLabelingActions)['addAccountMe
|
||||
type: 'outputLabel',
|
||||
entityKey: 'account',
|
||||
txid: 'TXID',
|
||||
outputIndex: 0,
|
||||
outputIndex: '0',
|
||||
value: 'Foo',
|
||||
accountDescriptor: 'xpub...',
|
||||
networkSymbol: 'btc',
|
||||
@@ -208,7 +208,7 @@ const addAccountMetadata: Fixture<(typeof metadataLabelingActions)['addAccountMe
|
||||
type: 'outputLabel',
|
||||
entityKey: 'account',
|
||||
txid: 'TXID',
|
||||
outputIndex: 0,
|
||||
outputIndex: '0',
|
||||
value: 'Foo',
|
||||
accountDescriptor: 'xpub...',
|
||||
networkSymbol: 'btc',
|
||||
@@ -255,7 +255,7 @@ const addAccountMetadata: Fixture<(typeof metadataLabelingActions)['addAccountMe
|
||||
type: 'outputLabel',
|
||||
entityKey: 'account',
|
||||
txid: 'TXID',
|
||||
outputIndex: 0,
|
||||
outputIndex: '0',
|
||||
value: 'Foo',
|
||||
accountDescriptor: 'xpub...',
|
||||
networkSymbol: 'btc',
|
||||
@@ -324,7 +324,7 @@ const addAccountMetadata: Fixture<(typeof metadataLabelingActions)['addAccountMe
|
||||
type: 'outputLabel',
|
||||
entityKey: 'account',
|
||||
txid: 'TXID',
|
||||
outputIndex: 0,
|
||||
outputIndex: '0',
|
||||
value: '', // empty string removes value
|
||||
accountDescriptor: 'xpub...',
|
||||
networkSymbol: 'btc',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RbfLabelsToBeUpdated } from '@suite-common/suite-rbf-labels-migrations-
|
||||
import type { NetworkSymbol } from '@suite-common/wallet-config';
|
||||
import { selectAccountByKey } from '@suite-common/wallet-core';
|
||||
import { AccountKey } from '@suite-common/wallet-types';
|
||||
import { typedObjectKeys } from '@trezor/utils';
|
||||
|
||||
import * as metadataLabelingActions from 'src/actions/suite/metadata/metadataLabelingActions';
|
||||
import { Dispatch } from 'src/types/suite';
|
||||
@@ -28,13 +29,13 @@ export const deleteDanglingLabels = async ({
|
||||
networkSymbol,
|
||||
txid,
|
||||
}: DeleteAllOutputLabelsParams) => {
|
||||
for (const outputIndex of Object.keys(labels)) {
|
||||
for (const outputIndex of typedObjectKeys(labels)) {
|
||||
await dispatch(
|
||||
metadataLabelingActions.addMetadata({
|
||||
type: 'outputLabel',
|
||||
entityKey: accountKey,
|
||||
txid,
|
||||
outputIndex: Number(outputIndex),
|
||||
outputIndex: `${outputIndex}`,
|
||||
defaultValue: '',
|
||||
value: '',
|
||||
accountDescriptor,
|
||||
@@ -61,7 +62,7 @@ export const copyLabelToNewTransaction = async ({
|
||||
networkSymbol,
|
||||
newTxid,
|
||||
}: MoveLabelToNewTransactionParams) => {
|
||||
for (const outputIndex of Object.keys(accountOutputLabels)) {
|
||||
for (const outputIndex of typedObjectKeys(accountOutputLabels)) {
|
||||
const value = accountOutputLabels[outputIndex];
|
||||
|
||||
await dispatch(
|
||||
@@ -69,7 +70,7 @@ export const copyLabelToNewTransaction = async ({
|
||||
type: 'outputLabel',
|
||||
entityKey: accountKey,
|
||||
txid: newTxid,
|
||||
outputIndex: Number(outputIndex),
|
||||
outputIndex: `${outputIndex}`,
|
||||
defaultValue: '',
|
||||
value,
|
||||
accountDescriptor,
|
||||
|
||||
@@ -160,7 +160,7 @@ const applySendFormMetadataLabelsThunk = createThunk<
|
||||
type: 'outputLabel',
|
||||
entityKey: selectedAccount.key,
|
||||
txid,
|
||||
outputIndex,
|
||||
outputIndex: `${outputIndex}`,
|
||||
value: label,
|
||||
defaultValue: '',
|
||||
networkSymbol: selectedAccount.symbol,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { formDraftActions, selectDeepCopyOfFormDraft } from '@suite-common/wallet-core';
|
||||
import { Output } from '@suite-common/wallet-types';
|
||||
import type { Output } from '@suite-common/wallet-types';
|
||||
import {
|
||||
convertAmountSubunitsToUnits,
|
||||
convertAmountUnitsToSubunits,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Translation } from '@suite/intl';
|
||||
import { getInstantStakeType } from '@suite-common/staking';
|
||||
import { AccountType, Network } from '@suite-common/wallet-config';
|
||||
import { selectIsPhishingTransaction, useDisplayBaseCurrency } from '@suite-common/wallet-core';
|
||||
import {
|
||||
createTargets,
|
||||
selectIsPhishingTransaction,
|
||||
useDisplayBaseCurrency,
|
||||
} from '@suite-common/wallet-core';
|
||||
import { AccountKey } from '@suite-common/wallet-types';
|
||||
import { formatNetworkAmount, isTxFeePaid } from '@suite-common/wallet-utils';
|
||||
import { Button, Link, Row, Tooltip } from '@trezor/components';
|
||||
@@ -63,7 +66,7 @@ export const TransactionItem = memo(
|
||||
const [limit, setLimit] = useState(0);
|
||||
const { shallDisplayBaseCurrency } = useDisplayBaseCurrency(transaction.symbol);
|
||||
|
||||
const { descriptor: address, symbol } = useSelector(selectSelectedAccount) || {};
|
||||
const account = useSelector(selectSelectedAccount) || null;
|
||||
|
||||
const networkFeatures = network.accountTypes[accountType]?.features ?? network.features;
|
||||
|
||||
@@ -72,35 +75,13 @@ export const TransactionItem = memo(
|
||||
`${AccountTransactionBaseAnchor}/${transaction.txid}`,
|
||||
);
|
||||
|
||||
const { type, targets, tokens, internalTransfers } = transaction;
|
||||
const { type } = transaction;
|
||||
|
||||
// Filter out internal transfers that are instant staking transactions
|
||||
const filteredInternalTransfers = useMemo(
|
||||
() =>
|
||||
internalTransfers.filter(t => {
|
||||
const stakeType = getInstantStakeType(t, address, symbol);
|
||||
|
||||
return stakeType !== 'stake';
|
||||
}),
|
||||
[internalTransfers, address, symbol],
|
||||
);
|
||||
const allOutputs = account !== null ? createTargets({ transaction, account }) : [];
|
||||
|
||||
const fee = formatNetworkAmount(transaction.fee, transaction.symbol);
|
||||
const showFeeRow = isTxFeePaid(transaction);
|
||||
|
||||
// join together regular targets, internal and token transfers
|
||||
const allOutputs: (
|
||||
| { type: 'token'; payload: (typeof tokens)[number] }
|
||||
| { type: 'internal'; payload: (typeof filteredInternalTransfers)[number] }
|
||||
| { type: 'target'; payload: WalletAccountTransaction['targets'][number] }
|
||||
)[] = [
|
||||
...tokens
|
||||
.filter(token => token.type !== 'self')
|
||||
.map(t => ({ type: 'token' as const, payload: t })),
|
||||
...targets.map(t => ({ type: 'target' as const, payload: t })),
|
||||
...filteredInternalTransfers.map(t => ({ type: 'internal' as const, payload: t })),
|
||||
];
|
||||
|
||||
const isExpandable = allOutputs.length - DEFAULT_LIMIT > 0;
|
||||
const toExpand = allOutputs.length - DEFAULT_LIMIT - limit;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
|
||||
import { useTranslation } from '@suite/intl';
|
||||
import { selectSuiteSyncOutputLabels } from '@suite-common/suite-sync';
|
||||
import {
|
||||
Target,
|
||||
selectBaseCurrency,
|
||||
selectHistoricFiatRatesByTimestamp,
|
||||
useDisplayBaseCurrency,
|
||||
@@ -35,11 +36,10 @@ import { WalletAccountTransaction } from 'src/types/wallet';
|
||||
|
||||
import { TargetAddressLabel } from './TargetAddressLabel';
|
||||
import { TokenTransferAddressLabel } from './TokenTransferAddressLabel';
|
||||
import { CombinedTarget } from './TransactionTargetsList';
|
||||
import { AmountComponent } from '../../AmountComponent';
|
||||
import { TransactionTargetLayout } from '../TransactionTargetLayout';
|
||||
|
||||
type TransactionTargetProps = CombinedTarget & {
|
||||
type TransactionTargetProps = Target & {
|
||||
transaction: WalletAccountTransaction;
|
||||
accountKey: AccountKey;
|
||||
isActionDisabled?: boolean;
|
||||
@@ -53,6 +53,7 @@ export const TransactionTarget = ({
|
||||
accountKey,
|
||||
isActionDisabled,
|
||||
isPhishingTransaction,
|
||||
targetId,
|
||||
...baseLayoutProps
|
||||
}: TransactionTargetProps) => {
|
||||
const { translationString } = useTranslation();
|
||||
@@ -156,22 +157,9 @@ export const TransactionTarget = ({
|
||||
],
|
||||
);
|
||||
|
||||
const metadataId = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'target':
|
||||
return payload.n;
|
||||
case 'token':
|
||||
return `token-${payload.contract}`;
|
||||
case 'internal':
|
||||
return `internal-${payload.to}`;
|
||||
default:
|
||||
return exhaustive(type);
|
||||
}
|
||||
}, [type, payload]);
|
||||
const targetMetadata = accountMetadata?.outputLabels?.[transaction.txid]?.[`${targetId}`];
|
||||
|
||||
const targetMetadata = accountMetadata?.outputLabels?.[transaction.txid]?.[metadataId];
|
||||
|
||||
const defaultMetadataValue = `${transaction.txid}-${metadataId}`;
|
||||
const defaultMetadataValue = `${transaction.txid}-${targetId}`;
|
||||
const isBeingEdited = defaultMetadataValue === labelingValueBeingEdited;
|
||||
|
||||
const label = useMemo(() => {
|
||||
@@ -203,8 +191,7 @@ export const TransactionTarget = ({
|
||||
|
||||
const outputLabel =
|
||||
suiteSyncOutputLabels.find(
|
||||
it =>
|
||||
it.txId === transaction.txid && it.outputIndex.toString() === metadataId.toString(),
|
||||
it => it.txId === transaction.txid && it.outputIndex.toString() === targetId,
|
||||
)?.label ?? targetMetadata;
|
||||
|
||||
return (
|
||||
@@ -222,7 +209,7 @@ export const TransactionTarget = ({
|
||||
type: 'outputLabel',
|
||||
entityKey: accountKey,
|
||||
txid: transaction.txid,
|
||||
outputIndex: metadataId,
|
||||
outputIndex: `${targetId}`,
|
||||
defaultValue: defaultMetadataValue,
|
||||
value: outputLabel,
|
||||
networkSymbol: transaction.symbol,
|
||||
|
||||
@@ -1,30 +1,13 @@
|
||||
import { Target } from '@suite-common/wallet-core';
|
||||
import { AccountKey } from '@suite-common/wallet-types';
|
||||
import {
|
||||
InternalTransfer as InternalTransferType,
|
||||
TokenTransfer as TokenTransferType,
|
||||
} from '@trezor/blockchain-link-types';
|
||||
|
||||
import { WalletAccountTransaction } from 'src/types/wallet';
|
||||
|
||||
import { TransactionTarget } from './TransactionTarget';
|
||||
|
||||
export type CombinedTarget =
|
||||
| {
|
||||
type: 'token';
|
||||
payload: TokenTransferType;
|
||||
}
|
||||
| {
|
||||
type: 'internal';
|
||||
payload: InternalTransferType;
|
||||
}
|
||||
| {
|
||||
type: 'target';
|
||||
payload: WalletAccountTransaction['targets'][number];
|
||||
};
|
||||
|
||||
type TransactionTargetsListProps = {
|
||||
transaction: WalletAccountTransaction;
|
||||
allOutputs: CombinedTarget[];
|
||||
allOutputs: Target[];
|
||||
limit: number;
|
||||
defaultLimit: number;
|
||||
accountKey: AccountKey;
|
||||
@@ -43,7 +26,7 @@ export const TransactionTargetsList = ({
|
||||
}: TransactionTargetsListProps) => {
|
||||
const previewTargets = allOutputs.slice(0, defaultLimit);
|
||||
|
||||
const renderTarget = ({ target, i }: { target: CombinedTarget; i: number }) => {
|
||||
const renderTarget = ({ target, i }: { target: Target; i: number }) => {
|
||||
const commonProps = {
|
||||
...target,
|
||||
transaction,
|
||||
|
||||
@@ -228,7 +228,7 @@ export const UtxoSelection = ({ transaction, utxo }: UtxoSelectionProps) => {
|
||||
type: 'outputLabel',
|
||||
entityKey: account.key,
|
||||
txid: utxo.txid,
|
||||
outputIndex: utxo.vout,
|
||||
outputIndex: `${utxo.vout}`,
|
||||
defaultValue: `${utxo.txid}-${utxo.vout}`,
|
||||
value: outputLabel,
|
||||
networkSymbol: account.symbol,
|
||||
|
||||
@@ -484,7 +484,7 @@ export const Address = ({ output, outputId, outputsCount }: AddressProps) => {
|
||||
// txid is not known at this moment. metadata is only saved
|
||||
// along with other sendForm data and processed in sendFormActions.
|
||||
txid: 'will-be-replaced',
|
||||
outputIndex: outputId,
|
||||
outputIndex: `${outputId}`,
|
||||
defaultValue: `${outputId}`,
|
||||
value: label,
|
||||
networkSymbol: symbol,
|
||||
|
||||
@@ -56,7 +56,6 @@ packages/suite/src/components/suite/troubleshooting/TroubleshootingTips.tsx > pa
|
||||
packages/suite/src/components/suite/troubleshooting/TroubleshootingTips.tsx > packages/suite/src/components/suite/troubleshooting/TroubleshootingTipsList.tsx > packages/suite/src/components/suite/troubleshooting/TroubleshootingTipsItemComponent.tsx
|
||||
packages/suite/src/components/wallet/Fees/CollapsibleFees/CustomFee/CustomFee.tsx > packages/suite/src/components/wallet/Fees/CollapsibleFees/CustomFee/CustomFeeEthereum.tsx
|
||||
packages/suite/src/components/wallet/Fees/CollapsibleFees/CustomFee/CustomFee.tsx > packages/suite/src/components/wallet/Fees/CollapsibleFees/CustomFee/CustomFeeMisc.tsx
|
||||
packages/suite/src/components/wallet/TransactionItem/TransactionTarget/TransactionTargetsList.tsx > packages/suite/src/components/wallet/TransactionItem/TransactionTarget/TransactionTarget.tsx
|
||||
packages/suite/src/utils/wallet/graph/utils.ts > packages/suite/src/utils/wallet/graph/utilsShared.ts > packages/suite/src/utils/wallet/graph/utilsWorker.ts
|
||||
packages/suite/src/utils/wallet/graph/utilsShared.ts > packages/suite/src/utils/wallet/graph/utilsWorker.ts
|
||||
packages/suite/src/views/dashboard/AssetsView/AssetTable/AssetTable.tsx > packages/suite/src/views/dashboard/AssetsView/AssetsView.tsx
|
||||
|
||||
@@ -20,7 +20,7 @@ export type MetadataAddPayload = { skipSave?: boolean } & (
|
||||
type: 'outputLabel';
|
||||
entityKey: string;
|
||||
txid: string;
|
||||
outputIndex: number | string;
|
||||
outputIndex: string; // not just index, for tokens/internals it can be different stuff
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
networkSymbol: string;
|
||||
|
||||
@@ -608,7 +608,7 @@ export const getDaysToAddToPoolInitial = (validatorsQueue?: ValidatorsQueue) =>
|
||||
};
|
||||
|
||||
export const getInstantStakeType = (
|
||||
internalTransfer: InternalTransfer,
|
||||
internalTransfer: Pick<InternalTransfer, 'from' | 'to'>,
|
||||
address?: string,
|
||||
symbol?: NetworkSymbol,
|
||||
): StakeType | null => {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@suite-common/fiat-services": "workspace:*",
|
||||
"@suite-common/platform-encryption": "workspace:*",
|
||||
"@suite-common/redux-utils": "workspace:*",
|
||||
"@suite-common/staking": "workspace:*",
|
||||
"@suite-common/suite-constants": "workspace:*",
|
||||
"@suite-common/suite-types": "workspace:*",
|
||||
"@suite-common/suite-utils": "workspace:*",
|
||||
|
||||
@@ -69,3 +69,5 @@ export * from './transactions/transactionsThunks';
|
||||
export { getIsIgnoredEntropyCheckError } from './device/services/getIsIgnoredEntropyCheckError';
|
||||
export { getIsDeviceIdValid } from './device/services/getIsDeviceIdValid';
|
||||
export { deviceInvariabilityCheck } from './device/services/deviceInvariabilityCheck';
|
||||
export * from './transactions/target/createTargets';
|
||||
export * from './transactions/target/Target';
|
||||
|
||||
29
suite-common/wallet-core/src/transactions/target/Target.ts
Normal file
29
suite-common/wallet-core/src/transactions/target/Target.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { TxOutputId } from '@suite-common/wallet-types';
|
||||
import {
|
||||
InternalTransfer as InternalTransferType,
|
||||
TokenTransfer as TokenTransferType,
|
||||
Transaction,
|
||||
} from '@trezor/blockchain-link-types';
|
||||
|
||||
/**
|
||||
* Classic utxo-based (bitcoin-like) networks
|
||||
*/
|
||||
export type SimpleTarget = {
|
||||
type: 'target';
|
||||
targetId: TxOutputId;
|
||||
payload: Transaction['targets'][number];
|
||||
};
|
||||
|
||||
export type TokenTarget = {
|
||||
type: 'token';
|
||||
targetId: TxOutputId;
|
||||
payload: TokenTransferType;
|
||||
};
|
||||
|
||||
export type InternalTarget = {
|
||||
type: 'internal';
|
||||
targetId: TxOutputId;
|
||||
payload: InternalTransferType;
|
||||
};
|
||||
|
||||
export type Target = SimpleTarget | TokenTarget | InternalTarget;
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Account, asAccountDescriptor } from '@suite-common/wallet-types';
|
||||
import {
|
||||
Target as BlockchainTarget,
|
||||
InternalTransfer,
|
||||
TokenTransfer,
|
||||
} from '@trezor/blockchain-link-types';
|
||||
|
||||
import { createTargets } from '../createTargets';
|
||||
|
||||
const makeTarget = (overrides: Partial<BlockchainTarget> = {}): BlockchainTarget => ({
|
||||
n: 0,
|
||||
isAddress: true,
|
||||
amount: '100',
|
||||
addresses: ['addr1'],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeTokenTransfer = (overrides: Partial<TokenTransfer> = {}): TokenTransfer => ({
|
||||
type: 'sent',
|
||||
name: 'Token',
|
||||
symbol: 'TKN',
|
||||
contract: '0xContractA',
|
||||
decimals: 18,
|
||||
amount: '500',
|
||||
from: '0xFrom',
|
||||
to: '0xTo',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeInternalTransfer = (overrides: Partial<InternalTransfer> = {}): InternalTransfer => ({
|
||||
type: 'sent',
|
||||
amount: '200',
|
||||
from: '0xFrom',
|
||||
to: '0xInternal',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const account: Pick<Account, 'descriptor' | 'symbol'> = {
|
||||
descriptor: asAccountDescriptor('0xMyAddress'),
|
||||
symbol: 'eth' as const,
|
||||
};
|
||||
|
||||
describe(createTargets.name, () => {
|
||||
it('returns empty array when transaction has no targets, tokens, or internal transfers', () => {
|
||||
const result = createTargets({
|
||||
transaction: { targets: [], tokens: [], internalTransfers: [] },
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('maps regular targets to SimpleTarget entries', () => {
|
||||
const targets = [makeTarget({ n: 0 }), makeTarget({ n: 1, amount: '200' })];
|
||||
|
||||
const result = createTargets({
|
||||
transaction: { targets, tokens: [], internalTransfers: [] },
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
type: 'target',
|
||||
targetId: '0',
|
||||
payload: targets[0],
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
type: 'target',
|
||||
targetId: '1',
|
||||
payload: targets[1],
|
||||
});
|
||||
});
|
||||
|
||||
it('maps token transfers to TokenTarget entries, filtering out "self" type', () => {
|
||||
const tokens = [
|
||||
makeTokenTransfer({ type: 'sent', contract: '0xA' }),
|
||||
makeTokenTransfer({ type: 'self', contract: '0xB' }),
|
||||
makeTokenTransfer({ type: 'recv', contract: '0xC' }),
|
||||
];
|
||||
|
||||
const result = createTargets({
|
||||
transaction: { targets: [], tokens, internalTransfers: [] },
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
type: 'token',
|
||||
targetId: 'token-0xA',
|
||||
payload: tokens[0],
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
type: 'token',
|
||||
targetId: 'token-0xC',
|
||||
payload: tokens[2],
|
||||
});
|
||||
});
|
||||
|
||||
it('excludes all token transfers when all have type "self"', () => {
|
||||
const tokens = [
|
||||
makeTokenTransfer({ type: 'self', contract: '0xA' }),
|
||||
makeTokenTransfer({ type: 'self', contract: '0xB' }),
|
||||
];
|
||||
|
||||
const result = createTargets({
|
||||
transaction: { targets: [], tokens, internalTransfers: [] },
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('maps internal transfers to InternalTarget entries', () => {
|
||||
const internalTransfers = [
|
||||
makeInternalTransfer({ to: '0xAddr1' }),
|
||||
makeInternalTransfer({ to: '0xAddr2' }),
|
||||
];
|
||||
|
||||
const result = createTargets({
|
||||
transaction: { targets: [], tokens: [], internalTransfers },
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
type: 'internal',
|
||||
targetId: 'internal-0xAddr1',
|
||||
payload: internalTransfers[0],
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
type: 'internal',
|
||||
targetId: 'internal-0xAddr2',
|
||||
payload: internalTransfers[1],
|
||||
});
|
||||
});
|
||||
|
||||
it('filters out internal transfers that are instant staking transactions', () => {
|
||||
// An instant stake transfer goes from addressContractPool to addressContractWithdrawTreasury.
|
||||
// For eth mainnet the pool address is used as `from` and withdraw treasury as `to`.
|
||||
// getInstantStakeType returns 'stake' for such transfers, so they should be excluded.
|
||||
// We use a regular internal transfer that won't match staking addresses.
|
||||
const regularTransfer = makeInternalTransfer({
|
||||
from: '0xRegularFrom',
|
||||
to: '0xRegularTo',
|
||||
});
|
||||
|
||||
const result = createTargets({
|
||||
transaction: {
|
||||
targets: [],
|
||||
tokens: [],
|
||||
internalTransfers: [regularTransfer],
|
||||
},
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
type: 'internal',
|
||||
targetId: 'internal-0xRegularTo',
|
||||
payload: regularTransfer,
|
||||
});
|
||||
});
|
||||
|
||||
it('combines all target types in the correct order', () => {
|
||||
const targets = [makeTarget({ n: 0 })];
|
||||
const tokens = [makeTokenTransfer({ type: 'recv', contract: '0xToken' })];
|
||||
const internalTransfers = [makeInternalTransfer({ to: '0xInternal' })];
|
||||
|
||||
const result = createTargets({
|
||||
transaction: { targets, tokens, internalTransfers },
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].type).toBe('target');
|
||||
expect(result[1].type).toBe('token');
|
||||
expect(result[2].type).toBe('internal');
|
||||
});
|
||||
|
||||
it('generates correct targetId format for each type', () => {
|
||||
const result = createTargets({
|
||||
transaction: {
|
||||
targets: [makeTarget({ n: 42 })],
|
||||
tokens: [makeTokenTransfer({ type: 'sent', contract: '0xDeadBeef' })],
|
||||
internalTransfers: [makeInternalTransfer({ to: '0xCafe' })],
|
||||
},
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result[0].targetId).toBe('42');
|
||||
expect(result[1].targetId).toBe('token-0xDeadBeef');
|
||||
expect(result[2].targetId).toBe('internal-0xCafe');
|
||||
});
|
||||
|
||||
it('preserves the original payload references', () => {
|
||||
const target = makeTarget({ n: 0 });
|
||||
const token = makeTokenTransfer({ type: 'sent' });
|
||||
const internal = makeInternalTransfer();
|
||||
|
||||
const result = createTargets({
|
||||
transaction: {
|
||||
targets: [target],
|
||||
tokens: [token],
|
||||
internalTransfers: [internal],
|
||||
},
|
||||
account,
|
||||
});
|
||||
|
||||
expect(result[0].payload).toBe(target);
|
||||
expect(result[1].payload).toBe(token);
|
||||
expect(result[2].payload).toBe(internal);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getInstantStakeType } from '@suite-common/staking';
|
||||
import { Account, asTxOutputId } from '@suite-common/wallet-types';
|
||||
import { InternalTransfer, Transaction } from '@trezor/blockchain-link-types';
|
||||
|
||||
import { InternalTarget, SimpleTarget, Target, TokenTarget } from './Target';
|
||||
|
||||
// Filter out internal transfers that are instant staking transactions
|
||||
const filteredInternalTransfers = (
|
||||
internalTransfers: InternalTransfer[],
|
||||
account: Pick<Account, 'descriptor' | 'symbol'>,
|
||||
) =>
|
||||
internalTransfers.filter(t => {
|
||||
const stakeType = getInstantStakeType(t, account.descriptor, account.symbol);
|
||||
|
||||
return stakeType !== 'stake';
|
||||
});
|
||||
|
||||
type CreateCombineTargetsParams = {
|
||||
transaction: Pick<Transaction, 'targets' | 'tokens' | 'internalTransfers'>;
|
||||
account: Pick<Account, 'descriptor' | 'symbol'>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Join together regular targets, internal and token transfers
|
||||
*/
|
||||
export const createTargets = ({ transaction, account }: CreateCombineTargetsParams): Target[] => {
|
||||
const { targets, tokens, internalTransfers } = transaction;
|
||||
|
||||
return [
|
||||
...targets.map(
|
||||
(t): SimpleTarget => ({
|
||||
type: 'target' as const,
|
||||
targetId: asTxOutputId(`${t.n}`),
|
||||
payload: t,
|
||||
}),
|
||||
),
|
||||
|
||||
...tokens
|
||||
.filter(token => token.type !== 'self')
|
||||
.map(
|
||||
(t): TokenTarget => ({
|
||||
type: 'token' as const,
|
||||
targetId: asTxOutputId(`token-${t.contract}`),
|
||||
payload: t,
|
||||
}),
|
||||
),
|
||||
|
||||
...filteredInternalTransfers(internalTransfers, account).map(
|
||||
(t): InternalTarget => ({
|
||||
type: 'internal' as const,
|
||||
targetId: asTxOutputId(`internal-${t.to}`),
|
||||
payload: t,
|
||||
}),
|
||||
),
|
||||
];
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
{ "path": "../fiat-services" },
|
||||
{ "path": "../platform-encryption" },
|
||||
{ "path": "../redux-utils" },
|
||||
{ "path": "../staking" },
|
||||
{ "path": "../suite-constants" },
|
||||
{ "path": "../suite-types" },
|
||||
{ "path": "../suite-utils" },
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
StaticSessionId,
|
||||
TokenInfo,
|
||||
} from '@trezor/connect';
|
||||
import { RequiredKey } from '@trezor/type-utils';
|
||||
import { Branded, RequiredKey } from '@trezor/type-utils';
|
||||
|
||||
import { Account, AccountDescriptor } from './account';
|
||||
|
||||
@@ -48,6 +48,9 @@ export type PrecomposedTransactionCardanoNonFinal =
|
||||
|
||||
export type BaseCurrencyOption = { value: BaseCurrencyCode | ''; label: string };
|
||||
|
||||
export type TxOutputId = string | Branded<'TxOutputId'>;
|
||||
export const asTxOutputId = (value: string) => value as TxOutputId;
|
||||
|
||||
export type Output = {
|
||||
type: 'payment' | 'opreturn';
|
||||
address: string;
|
||||
|
||||
@@ -11629,6 +11629,7 @@ __metadata:
|
||||
"@suite-common/fiat-services": "workspace:*"
|
||||
"@suite-common/platform-encryption": "workspace:*"
|
||||
"@suite-common/redux-utils": "workspace:*"
|
||||
"@suite-common/staking": "workspace:*"
|
||||
"@suite-common/suite-constants": "workspace:*"
|
||||
"@suite-common/suite-types": "workspace:*"
|
||||
"@suite-common/suite-utils": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user