Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0ca73fd
chore: extract shared collectible code
pete-watters Dec 5, 2025
84ef46b
feat(extension): add token details to extension
pete-watters Dec 15, 2025
a8fe44d
chore(extension): fix text and html inscriptions
pete-watters Dec 16, 2025
7ff1a4f
chore(extension): add missing sbtc activity
pete-watters Dec 16, 2025
20cfb66
chore(extension): integrate sbtc activity with activity service to sh…
pete-watters Dec 16, 2025
336fe9d
chore: try out rate limits for gamma
pete-watters Dec 16, 2025
f53ef06
chore(extension): fix type error with inscriptions
pete-watters Dec 17, 2025
3f707c8
chore(extension): add throttling of gamma to avoid problems on extension
pete-watters Dec 17, 2025
06d4a67
chore(extension): integrate sbtc activity with activity service to sh…
pete-watters Dec 17, 2025
faf438a
feat(extension): integrate submitted activity
pete-watters Dec 17, 2025
eee664f
fix: add more claude instructions
pete-watters Dec 17, 2025
38a7b97
fix: wip
pete-watters Dec 17, 2025
3da2592
feat(extension): token details screen improvements
pete-watters Dec 18, 2025
286c854
feat(extension): add TokenDetails route and refactor Sip10TokenDetail…
pete-watters Dec 18, 2025
502db0a
fix: update layout of collectibles screen and add marketplaces and learn
pete-watters Dec 18, 2025
a31e497
feat: enhance IPFS URL handling in SIP9 asset utility functions
pete-watters Dec 18, 2025
5fcd1a9
refactor: improve price change calculations in token details components
pete-watters Dec 18, 2025
c997f5c
feat(extension): integrate SBTC deposit transactions into activity list
pete-watters Dec 18, 2025
360637c
fix(mobile): make getActivity() fault-tolerant - use Promise.allSettl…
pete-watters Dec 18, 2025
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
18 changes: 18 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
- Use object method shorthand syntax in objects and interfaces.
constants.ts files.
- Prefer constants over magic numbers or strings.
- Always use `function` declarations for components, never const + arrow component definitions.

## Imports

- Never duplicate imports from the same module; consolidate named imports into a single statement.
- When adding a new import from a module that is already imported, always merge it into the existing import statement instead of creating a new one.

## Naming

Expand Down Expand Up @@ -43,6 +49,13 @@
ComponentNameProps.
- Destructure props directly in the signature: `function Component({ propA, propB }: ComponentProps)`

## React hooks

- Always follow the Rules of Hooks: call React hooks (including custom hooks like `useUserSettings`) only at the top level of React function components or other hooks.
- Never call hooks conditionally, inside loops, inside nested functions, or after early returns that would change the order of hook calls between renders.
- If you need conditional behavior, call the hook unconditionally and branch on its returned values, or extract the conditional logic into a separate component or custom hook.
- Avoid `useMemo` and `useCallback` unless they provide a clear, measurable benefit (e.g., preventing an expensive computation on every render or avoiding re-renders of heavy child components). Favor simple code over premature memoization.

## File naming

- Use snake case file names
Expand All @@ -53,6 +66,11 @@
- Required by file-based router
- use \*.spec.ts(x) for tests

## Architecture

- When generating new UI, use a container/presentational (dumb) component pattern with separate layout components for view-only concerns.
- Keep file names and folder structure consistent across platforms (web, extension, mobile) for equivalent features and components.

## Use Remeda for functional utilities

Prefer [Remeda](https://remedajs.com) when working with non-trivial data transformations that benefit from strong typing, immutability, and composability.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import DOMPurify from 'dompurify';
import { Box } from 'leather-styles/jsx';
import { Box, styled } from 'leather-styles/jsx';

interface CollectibleTextLayoutProps {
children: string;
Expand All @@ -25,7 +25,9 @@ export function CollectibleTextLayout({ children }: CollectibleTextLayoutProps)
textAlign="left"
width="100%"
>
<pre>{DOMPurify.sanitize(children)}</pre>
<styled.pre fontFamily="mono" fontSize="sm">
{DOMPurify.sanitize(children)}
</styled.pre>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import DOMPurify from 'dompurify';
import { Box } from 'leather-styles/jsx';
import { Box, styled } from 'leather-styles/jsx';

import { parseJson } from '@app/components/json';
import { LoadingSpinner } from '@app/components/loading-spinner';
Expand Down Expand Up @@ -28,14 +28,15 @@ export function InscriptionText(props: InscriptionTextProps) {
}}
color="white"
fontSize="9px"
fontFamily="mono"
height="100%"
p="space.03"
position="relative"
overflow="hidden"
textAlign="left"
width="100%"
>
<pre>{DOMPurify.sanitize(parseJson(query.data))}</pre>
<styled.pre>{DOMPurify.sanitize(parseJson(query.data))}</styled.pre>
</Box>
);
}
78 changes: 70 additions & 8 deletions apps/extension/src/app/features/activity-list/activity-list.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
import { useCallback } from 'react';
import { useEffect } from 'react';
import { Outlet } from 'react-router';
import { Virtuoso } from 'react-virtuoso';

import { type ActivityView } from '@leather.io/features';

import { formatCurrency } from '@app/common/currency-formatter';
import { SbtcDepositTransactionItem } from '@app/components/sbtc-deposit-status-item/sbtc-deposit-status-item';
import { useUserSettings } from '@app/hooks/use-user-settings';
import { useActivity } from '@app/query/activity/activity.query';
import { useSbtcPendingDeposits } from '@app/query/sbtc/sbtc-deposits.query';
import { useStacksPendingTransactions } from '@app/query/stacks/mempool/mempool.hooks';
import { useAccountAddresses } from '@app/services/accounts/use-account-addresses';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useUpdateSubmittedTransactions } from '@app/store/submitted-transactions/submitted-transactions.hooks';
import { useSubmittedTransactions } from '@app/store/submitted-transactions/submitted-transactions.selectors';

import { ActivityItem } from './components/activity-item';
import { ActivityListLayout } from './components/activity-list.layout';
import { createSubmittedActivityViews } from './submitted-activity-view';

type ActivityListRow =
| ActivityView
| {
key: string;
kind: 'sbtc-deposit';
deposit: import('@app/query/sbtc/sbtc-deposits.query').SbtcDeposit;
};

function isSbtcDepositRow(
item: ActivityListRow
): item is Extract<ActivityListRow, { kind: 'sbtc-deposit' }> {
return (item as any).kind === 'sbtc-deposit';
}

/*
* Infinite scroll support is built into this component via the onLoadMore prop,
Expand All @@ -24,20 +45,61 @@ import { ActivityListLayout } from './components/activity-list.layout';
export function ActivityList() {
const accountIndex = useCurrentAccountIndex();
const accountAddresses = useAccountAddresses(accountIndex);
const { network } = useUserSettings();
const activityQuery = useActivity(accountAddresses);
const submittedTransactions = useSubmittedTransactions();
const updateSubmittedTransactions = useUpdateSubmittedTransactions();

const stacksAddress = accountAddresses.stacks?.stxAddress ?? '';
const { transactions: stacksPendingTransactions } = useStacksPendingTransactions(stacksAddress);

const { pendingSbtcDeposits } = useSbtcPendingDeposits(stacksAddress);

const activity = activityQuery.data ?? [];
useEffect(() => {
if (!stacksAddress) return;
updateSubmittedTransactions(stacksPendingTransactions);
}, [stacksAddress, stacksPendingTransactions, updateSubmittedTransactions]);

const historicalActivity = activityQuery.data ?? [];
const submittedActivity = createSubmittedActivityViews({ submittedTransactions, network });
const sbtcPendingActivity: ActivityListRow[] = pendingSbtcDeposits.map(deposit => ({
key: `sbtc-deposit-${deposit.bitcoinTxid}-${deposit.bitcoinTxOutputIndex}`,
kind: 'sbtc-deposit',
deposit,
}));
const activity: ActivityListRow[] = [
...submittedActivity,
...sbtcPendingActivity,
...historicalActivity,
];

const hasActivity = activity.length > 0;
const isLoading = activityQuery.isLoading;

const itemContent = useCallback(
(_: number, item: ActivityView) => <ActivityItem item={item} formatCurrency={formatCurrency} />,
[]
);
function itemContent(_: number, item: ActivityListRow) {
if (isSbtcDepositRow(item)) {
return <SbtcDepositTransactionItem deposit={item.deposit} />;
}
return <ActivityItem item={item} formatCurrency={formatCurrency} />;
}

function computeItemKey(_: number, item: ActivityListRow) {
return item.key;
}

const computeItemKey = useCallback((_: number, item: ActivityView) => item.key, []);
if (activityQuery.isError) {
return (
<ActivityListLayout isLoading={false} hasActivity>
<div style={{ padding: '16px', textAlign: 'center' }}>
Unable to load activity. Please try again later.
</div>
<Outlet />
</ActivityListLayout>
);
}

return (
<ActivityListLayout isLoading={isLoading} hasActivity={activity.length > 0}>
<ActivityListLayout isLoading={isLoading} hasActivity={hasActivity}>
<Virtuoso
style={{ height: '100%' }}
data={activity}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { PayloadType, deserializeTransaction } from '@stacks/transactions';

import { type ActivityView, getStacksExplorerLink } from '@leather.io/features';
import type { NetworkConfiguration } from '@leather.io/models';

import { safelyFormatHexTxid } from '@app/common/utils/safe-handle-txid';
import type { SubmittedTransaction } from '@app/store/submitted-transactions/submitted-transactions.slice';

interface CreateSubmittedActivityViewsArgs {
submittedTransactions: SubmittedTransaction[];
network: NetworkConfiguration;
}

export function createSubmittedActivityViews({
submittedTransactions,
network,
}: CreateSubmittedActivityViewsArgs): ActivityView[] {
return submittedTransactions
.map(submittedTx => {
const tx = deserializeTransaction(submittedTx.rawTx);
const txid = safelyFormatHexTxid(submittedTx.txid);
const activityLink = getStacksExplorerLink({
mode: network.chain.bitcoin.mode,
type: 'txid',
value: txid,
searchParams: undefined,
isNakamoto: false,
});

const baseView: Omit<ActivityView, 'title' | 'caption'> = {
key: `submitted-${txid}`,
asset: undefined,
fromAsset: undefined,
toAsset: undefined,
activityLink,
balances: {},
activityAvatar: 'fallback',
statusIndicator: 'pending',
statusLabel: 'Submitted',
};

switch (tx.payload.payloadType) {
case PayloadType.TokenTransfer:
return {
...baseView,
title: 'Stacks',
caption: 'Submitted',
};
case PayloadType.ContractCall:
return {
...baseView,
title: tx.payload.functionName.content,
caption: 'Submitted',
};
case PayloadType.SmartContract:
return {
...baseView,
title: tx.payload.contractName.content,
caption: 'Submitted',
};
default:
return {
...baseView,
title: 'Submitted transaction',
caption: 'Submitted',
};
}
})
.filter(view => view !== null);
}
7 changes: 7 additions & 0 deletions apps/extension/src/app/features/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Dispatch, SetStateAction } from 'react';

import { Stack } from 'leather-styles/jsx';

import type { TokenDetailsProps } from '@leather.io/features';
import { BtcAvatarIcon, StxAvatarIcon } from '@leather.io/ui';

import { Brc20TokensLoader } from '@app/components/loaders/brc20-tokens-loader';
Expand Down Expand Up @@ -37,11 +38,13 @@ interface AssetListProps {
assetRightElementVariant?: AssetRightElementVariant;
showUnmanageableTokens?: boolean;
onSelectAsset?(symbol: string, contractId?: string): void;
onOpenToken?(tokenDetails: TokenDetailsProps): void;
setHasManageableTokens?: Dispatch<SetStateAction<boolean>>;
}

export function AssetList({
onSelectAsset,
onOpenToken,
variant = 'read-only',
assetRightElementVariant = 'balance',
showUnmanageableTokens = true,
Expand All @@ -67,6 +70,7 @@ export function AssetList({
balance={balance}
isLoading={isLoading}
onSelectAsset={onSelectAsset}
onOpenToken={onOpenToken}
isLoadingAdditionalData={isLoadingAdditionalData}
/>
)}
Expand All @@ -91,6 +95,7 @@ export function AssetList({
isLoading={isLoading}
isPrivate={isPrivate}
onSelectAsset={onSelectAsset}
onOpenToken={onOpenToken}
/>
)}
</StxAssetItemBalanceLoader>
Expand Down Expand Up @@ -124,6 +129,7 @@ export function AssetList({
accountIndex={currentAccountIndex}
assetFilter={filter}
onSelectAsset={onSelectAsset}
onOpenToken={onOpenToken}
assetRightElementVariant={assetRightElementVariant}
setHasManageableTokens={setHasManageableTokens}
/>
Expand Down Expand Up @@ -169,6 +175,7 @@ export function AssetList({
accountIndex={currentAccountIndex}
filter={filter}
assetRightElementVariant={assetRightElementVariant}
onOpenToken={onOpenToken}
setHasManageableTokens={setHasManageableTokens}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { btcAsset } from '@leather.io/constants';
import type { TokenDetailsProps } from '@leather.io/features';
import type { AccountQuotedBtcBalance } from '@leather.io/services';
import { BtcAvatarIcon } from '@leather.io/ui';
import { getAssetId, serializeAssetId } from '@leather.io/utils';

import { formatCurrency } from '@app/common/currency-formatter';
import { CryptoAssetItemLayout } from '@app/components/crypto-asset-item/crypto-asset-item.layout';
Expand All @@ -10,15 +13,29 @@ interface BtcCryptoAssetItemProps {
isLoading: boolean;
isLoadingAdditionalData?: boolean;
onSelectAsset?(symbol: string): void;
onOpenToken?(details: TokenDetailsProps): void;
}
export function BtcCryptoAssetItem({
balance,
isLoading,
onSelectAsset,
onOpenToken,
isLoadingAdditionalData,
}: BtcCryptoAssetItemProps) {
const isPrivate = useIsPrivateMode();

function handleSelectAsset(symbol: string) {
if (onOpenToken) {
onOpenToken({
assetId: serializeAssetId(getAssetId(btcAsset)),
});
return;
}
onSelectAsset?.(symbol);
}

const onSelectAssetProp = onOpenToken || onSelectAsset ? handleSelectAsset : undefined;

return (
<CryptoAssetItemLayout
availableBalance={balance.btc.totalBalance}
Expand All @@ -28,7 +45,7 @@ export function BtcCryptoAssetItem({
isLoading={isLoading}
isLoadingAdditionalData={isLoadingAdditionalData}
isPrivate={isPrivate}
onSelectAsset={onSelectAsset}
onSelectAsset={onSelectAssetProp}
titleLeft="Bitcoin"
dataTestId="BTC"
/>
Expand Down
Loading
Loading