Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/track txs locally #1726

Merged
merged 14 commits into from
Sep 28, 2021
5 changes: 5 additions & 0 deletions .changeset/rare-cameras-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@stacks/wallet-web': minor
---

This update improves the way in which the wallet persists user activity. When a user sends a transaction, the wallet will store a version of it locally. This improves the performance and feedback of the application.
5 changes: 4 additions & 1 deletion .dependency-cruiser.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ module.exports = {
name: 'only-import-state-via-hooks',
severity: 'error',
from: { path: '^src/*', pathNot: ['^src/store/*'] },
to: { path: ['^src/store/*'], pathNot: [`src.*\.hooks\.ts`, `src.*\.models\.ts`] },
to: {
path: ['^src/store/*'],
pathNot: [`src.*\.hooks\.ts`, `src.*\.models\.ts`, `src.*\.utils\.ts`],
},
},
{
name: 'ban-jotai-outside-store',
Expand Down
24 changes: 16 additions & 8 deletions src/common/hooks/account/use-api-nonce.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ describe(correctNextNonce.name, () => {
possible_next_nonce: 54,
detected_missing_nonces: [],
};
expect(correctNextNonce(response1)).toEqual(54);
expect(correctNextNonce(response1)?.nonce).toEqual(54);
expect(correctNextNonce(response1)?.isMissing).toEqual(false);
});

test('with a missing nonce', () => {
Expand All @@ -19,7 +20,8 @@ describe(correctNextNonce.name, () => {
possible_next_nonce: 54,
detected_missing_nonces: [49],
};
expect(correctNextNonce(response1)).toEqual(49);
expect(correctNextNonce(response1)?.nonce).toEqual(49);
expect(correctNextNonce(response1)?.isMissing).toEqual(true);
});

test('possible_next_nonce is less than missing nonce', () => {
Expand All @@ -29,7 +31,8 @@ describe(correctNextNonce.name, () => {
possible_next_nonce: 24,
detected_missing_nonces: [49],
};
expect(correctNextNonce(response1)).toEqual(49);
expect(correctNextNonce(response1)?.nonce).toEqual(49);
expect(correctNextNonce(response1)?.isMissing).toEqual(true);
});

test('invalid state: last_executed_tx_nonce is more or equal than missing nonce', () => {
Expand All @@ -39,7 +42,8 @@ describe(correctNextNonce.name, () => {
possible_next_nonce: 50,
detected_missing_nonces: [49],
};
expect(correctNextNonce(response1)).toEqual(50); // fallback to possible_next_nonce
expect(correctNextNonce(response1)?.nonce).toEqual(50); // fallback to possible_next_nonce
expect(correctNextNonce(response1)?.isMissing).toEqual(false);
});

test('Initial state', () => {
Expand All @@ -49,7 +53,8 @@ describe(correctNextNonce.name, () => {
possible_next_nonce: 0,
detected_missing_nonces: [],
};
expect(correctNextNonce(response1)).toEqual(0); // fallback to possible_next_nonce
expect(correctNextNonce(response1)?.nonce).toEqual(0); // fallback to possible_next_nonce
expect(correctNextNonce(response1)?.isMissing).toEqual(false);
});

test('With last_mempool_tx_nonce', () => {
Expand All @@ -59,7 +64,8 @@ describe(correctNextNonce.name, () => {
possible_next_nonce: 73,
detected_missing_nonces: [71],
};
expect(correctNextNonce(response1)).toEqual(71);
expect(correctNextNonce(response1)?.nonce).toEqual(71);
expect(correctNextNonce(response1)?.isMissing).toEqual(true);
});

test('With many missing nonce and handling order', () => {
Expand All @@ -69,14 +75,16 @@ describe(correctNextNonce.name, () => {
last_mempool_tx_nonce: 74,
possible_next_nonce: 75,
};
expect(correctNextNonce(response1)).toEqual(71);
expect(correctNextNonce(response1)?.nonce).toEqual(71);
expect(correctNextNonce(response1)?.isMissing).toEqual(true);

const response2: AddressNonces = {
detected_missing_nonces: [71, 73],
last_executed_tx_nonce: 70,
last_mempool_tx_nonce: 74,
possible_next_nonce: 75,
};
expect(correctNextNonce(response2)).toEqual(71);
expect(correctNextNonce(response2)?.nonce).toEqual(71);
expect(correctNextNonce(response2)?.isMissing).toEqual(true);
});
});
20 changes: 14 additions & 6 deletions src/common/hooks/account/use-next-tx-nonce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { useGetAccountNonce } from '@common/hooks/account/use-get-account-nonce'
import { UseQueryOptions } from 'react-query';
import { useLastApiNonceState } from '@store/accounts/nonce.hooks';

export function correctNextNonce(apiNonce: AddressNonces): number | undefined {
export function correctNextNonce(
apiNonce: AddressNonces
): { nonce: number; isMissing?: boolean } | undefined {
if (!apiNonce) return;

const missingNonces = apiNonce.detected_missing_nonces;
Expand All @@ -12,17 +14,23 @@ export function correctNextNonce(apiNonce: AddressNonces): number | undefined {
missingNonces.length > 0 &&
missingNonces[0] > (apiNonce.last_executed_tx_nonce || 0)
) {
return missingNonces.sort()[0];
return {
nonce: missingNonces.sort()[0],
isMissing: true,
};
}
return apiNonce.possible_next_nonce;
return {
nonce: apiNonce.possible_next_nonce,
isMissing: false,
};
}

export function useNextTxNonce() {
const [nonce, setLastApiNonce] = useLastApiNonceState();
const [lastApiNonce, setLastApiNonce] = useLastApiNonceState();
const onSuccess = (data: AddressNonces) => {
const nextNonce = data && correctNextNonce(data);
if (typeof nextNonce === 'number') setLastApiNonce(nextNonce);
if (nextNonce) setLastApiNonce(nextNonce);
};
useGetAccountNonce({ onSuccess } as UseQueryOptions);
return nonce;
return lastApiNonce?.nonce;
}
3 changes: 2 additions & 1 deletion src/common/hooks/use-explorer-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { useCurrentNetwork } from '@common/hooks/use-current-network';
export function useExplorerLink() {
const { mode } = useCurrentNetwork();
const handleOpenTxLink = useCallback(
(txid: string) => window.open(makeTxExplorerLink(txid, mode), '_blank'),
(txid: string, suffix?: string) =>
window.open(makeTxExplorerLink(txid, mode, suffix), '_blank'),
[mode]
);

Expand Down
4 changes: 2 additions & 2 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ export function validateAndCleanRecoveryInput(value: string) {

export const hasLineReturn = (input: string) => input.includes('\n');

export function makeTxExplorerLink(txid: string, chain: 'mainnet' | 'testnet') {
return `https://explorer.stacks.co/txid/${txid}?chain=${chain}`;
export function makeTxExplorerLink(txid: string, chain: 'mainnet' | 'testnet', suffix = '') {
return `https://explorer.stacks.co/txid/${txid}?chain=${chain}${suffix}`;
}

export function truncateString(str: string, maxLength: number) {
Expand Down
8 changes: 4 additions & 4 deletions src/components/popup/tx-item.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import type {
Transaction,
CoinbaseTransaction,
MempoolTransaction,
Transaction,
TransactionEventFungibleAsset,
CoinbaseTransaction,
} from '@stacks/stacks-blockchain-api-types';
import { Box, BoxProps, Button, color, Stack } from '@stacks/ui';
import { getContractName, isPendingTx, truncateMiddle } from '@stacks/ui-utils';
Expand Down Expand Up @@ -47,7 +47,7 @@ interface TxItemProps {
transaction: Tx;
}

const getTxCaption = (transaction: Tx) => {
export const getTxCaption = (transaction: Tx) => {
if (!transaction) return null;
switch (transaction.tx_type) {
case 'smart_contract':
Expand Down Expand Up @@ -173,8 +173,8 @@ export const TxItem: React.FC<TxItemProps & BoxProps> = ({ transaction, ...rest
{getTxTitle(transaction as any)}
</Title>
<Stack isInline flexWrap="wrap">
<Status transaction={transaction} />
<Caption variant="c2">{getTxCaption(transaction)}</Caption>
<Status transaction={transaction} />
</Stack>
</Stack>
<Stack alignItems="flex-end" spacing="base-tight">
Expand Down
48 changes: 47 additions & 1 deletion src/components/tx-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import FunctionIcon from 'mdi-react/FunctionIcon';
import { useWallet } from '@common/hooks/use-wallet';
import { StxIcon } from './icons/stx-icon';
import { MicroblockIcon } from '@components/icons/microblock';
import { Tx, Status, statusFromTx } from '@common/api/transactions';
import { Status, statusFromTx, Tx } from '@common/api/transactions';
import { addressToString, PayloadType, StacksTransaction } from '@stacks/transactions';
import { getTxSenderAddress } from '@store/accounts/account-activity.utils';

interface TypeIconWrapperProps extends BoxProps {
icon: React.FC<any>;
Expand Down Expand Up @@ -143,3 +145,47 @@ export const TxItemIcon: React.FC<{ transaction: Tx }> = ({ transaction, ...rest
return null;
}
};

export const StacksTransactionItemIcon: React.FC<{ transaction: StacksTransaction }> = ({
transaction,
...rest
}) => {
switch (transaction.payload.payloadType) {
case PayloadType.SmartContract:
return (
<DynamicColorCircle
position="relative"
string={`${getTxSenderAddress(transaction)}.${transaction.payload.contractName.content}`}
backgroundSize="200%"
size="36px"
{...rest}
>
<TypeIcon transaction={{ tx_type: 'smart_contract', tx_status: 'pending' } as Tx} />
</DynamicColorCircle>
);
case PayloadType.ContractCall:
return (
<DynamicColorCircle
position="relative"
string={`${addressToString(transaction.payload.contractAddress)}.${
transaction.payload.contractName.content
}::${transaction.payload.functionName.content}`}
backgroundSize="200%"
size="36px"
{...rest}
>
<TypeIcon transaction={{ tx_type: 'contract_call', tx_status: 'pending' } as Tx} />
</DynamicColorCircle>
);
case PayloadType.TokenTransfer:
return (
<ItemIconWrapper
icon={StxIcon}
transaction={{ tx_type: 'token_transfer', tx_status: 'pending' } as Tx}
{...rest}
/>
);
default:
return null;
}
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { memo, useMemo } from 'react';
import { Box, Stack, SlideFade, Flex, Spinner, color } from '@stacks/ui';
import type { StackProps } from '@stacks/ui';
import { Box, color, Flex, SlideFade, Spinner, Stack } from '@stacks/ui';
import { TokenAssets } from '@components/popup/token-assets';

import { Caption } from '@components/typography';
Expand All @@ -9,9 +9,11 @@ import { Tabs } from '@components/tabs';

import { useAccountActivity } from '@store/accounts/account.hooks';
import { useHomeTabs } from '@common/hooks/use-home-tabs';
import { createTxDateFormatList } from '@common/group-txs-by-date';
import { TransactionList } from './transaction-list';
import { HomePageSelectors } from '@tests/page-objects/home-page.selectors';
import { useCurrentAccountLocalTxids } from '@store/accounts/account-activity.hooks';
import { TransactionList } from '@components/popup/transaction-list';
import { createTxDateFormatList } from '@common/group-txs-by-date';
import { LocalTxList } from '@features/local-transaction-activity/local-tx-list';

function EmptyActivity() {
return (
Expand All @@ -33,10 +35,15 @@ const ActivityList = () => {
() => (transactions ? createTxDateFormatList(transactions) : []),
[transactions]
);
return !transactions || transactions.length === 0 ? (
const txids = useCurrentAccountLocalTxids();
const hasTxs = txids.length > 0 || transactions.length > 0;
return !hasTxs ? (
<EmptyActivity />
) : (
<TransactionList txsByDate={groupedTxs} />
<>
{txids.length > 0 && <LocalTxList txids={txids} />}
{transactions.length > 0 && <TransactionList txsByDate={groupedTxs} />}
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
import { Box, Button, Spinner, Stack } from '@stacks/ui';
import { Button, Flex, Spinner, Stack } from '@stacks/ui';
import { ControlledDrawer } from '@components/drawer/controlled';
import { Caption } from '@components/typography';
import {
Expand Down Expand Up @@ -170,9 +170,9 @@ export const SpeedUpTransactionDrawer: React.FC = () => {
{rawTxId ? (
<React.Suspense
fallback={
<Box p="extra-loose">
<Flex alignItems="center" justifyContent="center" p="extra-loose">
<Spinner />
</Box>
</Flex>
}
>
<Content />
Expand Down
Loading