Skip to content

Commit fe9454f

Browse files
committed
feat: add sbtc homepage section
1 parent 9b765f6 commit fe9454f

8 files changed

Lines changed: 590 additions & 22 deletions

File tree

public/sbtc-bubbles.svg

Lines changed: 214 additions & 0 deletions
Loading
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use client';
2+
3+
import { formatNumber, formatUsdValue } from '@/common/utils/string-utils';
4+
import { Text } from '@/ui/Text';
5+
import { Box, Flex, Icon, Link, Stack } from '@chakra-ui/react';
6+
import { ArrowUpRight } from '@phosphor-icons/react';
7+
import Image from 'next/image';
8+
9+
interface SBTCOverviewProps {
10+
totalSupply: number;
11+
totalSupplyUsd: number;
12+
}
13+
14+
export function SBTCOverview({ totalSupply, totalSupplyUsd }: SBTCOverviewProps) {
15+
return (
16+
<Box
17+
bg="surfaceSecondary"
18+
borderRadius="3xl"
19+
p={8}
20+
position="relative"
21+
overflow="hidden"
22+
minH="283px"
23+
>
24+
<Stack gap={6} position="relative" zIndex={1} maxW="280px">
25+
<Stack gap={3}>
26+
<Text textStyle="text-medium-lg" color="textPrimary">
27+
Total supply
28+
</Text>
29+
<Text textStyle="heading-lg" color="textPrimary" lineHeight="redesign.none">
30+
{formatNumber(totalSupply, 0, 0)} sBTC
31+
</Text>
32+
<Text textStyle="heading-sm" color="textSecondary" lineHeight="redesign.tighter">
33+
({formatUsdValue(totalSupplyUsd, 0, 0)})
34+
</Text>
35+
</Stack>
36+
<Link
37+
href="https://sbtc.stacks.co/"
38+
target="_blank"
39+
rel="noopener noreferrer"
40+
bg="accent.stacks-500"
41+
color="neutral.sand-1000"
42+
h={10}
43+
px={4}
44+
py={1.5}
45+
borderRadius="redesign.lg"
46+
fontWeight="medium"
47+
fontSize="sm"
48+
display="inline-flex"
49+
alignItems="center"
50+
gap={1}
51+
width="fit-content"
52+
textDecoration="none"
53+
boxShadow="0 6px 20px rgba(252, 100, 50, 0.5)"
54+
_hover={{ opacity: 0.9, textDecoration: 'none' }}
55+
>
56+
Get sBTC
57+
<Icon w={4} h={4}>
58+
<ArrowUpRight weight="bold" />
59+
</Icon>
60+
</Link>
61+
</Stack>
62+
<Flex position="absolute" bottom={0} right={0} pointerEvents="none">
63+
<Image src="/sbtc-bubbles.svg" alt="" width={262} height={170} />
64+
</Flex>
65+
</Box>
66+
);
67+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use client';
2+
3+
import { useGlobalContext } from '@/common/context/useGlobalContext';
4+
import { buildUrl } from '@/common/utils/buildUrl';
5+
import { formatNumber, formatUsdValue } from '@/common/utils/string-utils';
6+
import { formatTimestampToRelativeTime } from '@/common/utils/time-utils';
7+
import { truncateMiddle } from '@/common/utils/utils';
8+
import { Badge, DefaultBadgeIcon, DefaultBadgeLabel } from '@/ui/Badge';
9+
import { NextLink } from '@/ui/NextLink';
10+
import { Text } from '@/ui/Text';
11+
import SBTCIcon from '@/ui/icons/sBTCIcon';
12+
import { Flex, Icon, Stack } from '@chakra-ui/react';
13+
import { ArrowDown, ArrowUp } from '@phosphor-icons/react';
14+
15+
import { SBTCTransaction } from './types';
16+
17+
function TransactionRow({ tx }: { tx: SBTCTransaction }) {
18+
const network = useGlobalContext().activeNetwork;
19+
const isDeposit = tx.type === 'deposit';
20+
21+
return (
22+
<Flex py={3} alignItems="center" gap={2} flexWrap={{ base: 'wrap', md: 'nowrap' }}>
23+
<Flex flex="0 0 auto" minW="100px">
24+
<Badge variant="outline" content="iconAndLabel" type="transactionType">
25+
<Flex alignItems="center" gap={1.5}>
26+
<DefaultBadgeIcon
27+
icon={isDeposit ? <ArrowDown weight="bold" /> : <ArrowUp weight="bold" />}
28+
bg={isDeposit ? 'feedback.green-500' : 'feedback.red-500'}
29+
/>
30+
<DefaultBadgeLabel label={isDeposit ? 'Deposit' : 'Withdrawal'} />
31+
</Flex>
32+
</Badge>
33+
</Flex>
34+
35+
<Flex flex={1} minW="80px">
36+
<NextLink href={buildUrl(`/txid/${tx.txId}`, network)} variant="tableLink">
37+
<Text textStyle="text-regular-xs" fontFamily="var(--font-matter-mono)">
38+
{truncateMiddle(tx.txId, 4, 5)}
39+
</Text>
40+
</NextLink>
41+
</Flex>
42+
43+
<Flex flex={1} minW="80px">
44+
<NextLink href={buildUrl(`/address/${tx.address}`, network)} variant="tableLink">
45+
<Text textStyle="text-regular-xs" fontFamily="var(--font-matter-mono)">
46+
{truncateMiddle(tx.address, 4, 5)}
47+
</Text>
48+
</NextLink>
49+
</Flex>
50+
51+
<Flex flex="0 0 auto" minW="100px" justifyContent="flex-end">
52+
<Stack gap={0} alignItems="flex-end">
53+
<Flex alignItems="center" gap={1}>
54+
<Icon w={3.5} h={3.5} color="accent.bitcoin-500">
55+
<SBTCIcon />
56+
</Icon>
57+
<Text textStyle="text-medium-xs" color="textPrimary">
58+
{formatNumber(tx.amount, 0, 6)} sBTC
59+
</Text>
60+
</Flex>
61+
<Text textStyle="text-regular-xs" color="textSecondary" pl={4.5}>
62+
{formatUsdValue(tx.amountUsd, 0, 0)}
63+
</Text>
64+
</Stack>
65+
</Flex>
66+
67+
<Flex flex="0 0 auto" minW="80px" justifyContent="flex-end">
68+
<Badge variant="solid" type="tag">
69+
<Text textStyle="text-mono-xs" color="textSecondary">
70+
{formatTimestampToRelativeTime(tx.blockTime)}
71+
</Text>
72+
</Badge>
73+
</Flex>
74+
</Flex>
75+
);
76+
}
77+
78+
export function SBTCTransactions({ transactions }: { transactions: SBTCTransaction[] }) {
79+
if (transactions.length === 0) {
80+
return (
81+
<Flex bg="surfaceSecondary" borderRadius="xl" p={6} justifyContent="center">
82+
<Text textStyle="text-regular-sm" color="textSecondary">
83+
No recent sBTC transactions
84+
</Text>
85+
</Flex>
86+
);
87+
}
88+
89+
return (
90+
<Stack gap={3}>
91+
<Text textStyle="heading-xs" color="textPrimary">
92+
Recent sBTC transactions
93+
</Text>
94+
<Flex
95+
bg="surfaceTertiary"
96+
borderRadius="redesign.xl"
97+
border="1px solid"
98+
borderColor="redesignBorderSecondary"
99+
px={3}
100+
pb={3}
101+
flexDirection="column"
102+
>
103+
<Flex
104+
py={3}
105+
gap={2}
106+
borderBottom="1px solid"
107+
borderColor="redesignBorderSecondary"
108+
display={{ base: 'none', md: 'flex' }}
109+
>
110+
<Text flex="0 0 auto" minW="100px" textStyle="text-medium-sm" color="textSecondary">
111+
Type
112+
</Text>
113+
<Text flex={1} minW="80px" textStyle="text-medium-sm" color="textSecondary">
114+
ID
115+
</Text>
116+
<Text flex={1} minW="80px" textStyle="text-medium-sm" color="textSecondary">
117+
From
118+
</Text>
119+
<Text
120+
flex="0 0 auto"
121+
minW="100px"
122+
textAlign="right"
123+
textStyle="text-medium-sm"
124+
color="textSecondary"
125+
>
126+
Amount
127+
</Text>
128+
<Text
129+
flex="0 0 auto"
130+
minW="80px"
131+
textAlign="right"
132+
textStyle="text-medium-sm"
133+
color="textSecondary"
134+
>
135+
Timestamp
136+
</Text>
137+
</Flex>
138+
{transactions.map(tx => (
139+
<TransactionRow key={tx.txId} tx={tx} />
140+
))}
141+
</Flex>
142+
</Stack>
143+
);
144+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { fetchHolders, fetchRecentTransactions } from '@/api/data-fetchers';
2+
import { getCurrentBtcPrice } from '@/app/getTokenPriceInfo';
3+
import {
4+
SBTC_ASSET_ID,
5+
SBTC_DECIMALS,
6+
sbtcDepositContractAddress,
7+
sbtcWidthdrawlContractAddress,
8+
} from '@/app/token/[tokenId]/consts';
9+
import { logError } from '@/common/utils/error-utils';
10+
import { getFtDecimalAdjustedBalance } from '@/common/utils/utils';
11+
12+
import { Transaction } from '@stacks/stacks-blockchain-api-types';
13+
14+
import { SBTCData, SBTCTransaction } from './types';
15+
16+
function extractAmount(tx: Transaction): number {
17+
if (tx.tx_type !== 'contract_call') return 0;
18+
const amountArg = tx.contract_call?.function_args?.find(a => a.name === 'amount');
19+
if (!amountArg) return 0;
20+
const raw = amountArg.repr.replace(/^u/, '');
21+
return getFtDecimalAdjustedBalance(raw, SBTC_DECIMALS);
22+
}
23+
24+
function extractDepositRecipient(tx: Transaction): string {
25+
if (tx.tx_type !== 'contract_call') return tx.sender_address;
26+
const recipientArg = tx.contract_call?.function_args?.find(a => a.name === 'recipient');
27+
if (!recipientArg) return tx.sender_address;
28+
return recipientArg.repr.replace(/^'/, '');
29+
}
30+
31+
export async function fetchSBTCData(apiUrl: string): Promise<SBTCData | undefined> {
32+
try {
33+
const [holdersResult, btcPriceResult, depositsResult, withdrawalsResult] =
34+
await Promise.allSettled([
35+
fetchHolders(apiUrl, SBTC_ASSET_ID, 1, 0),
36+
getCurrentBtcPrice(),
37+
fetchRecentTransactions(apiUrl, sbtcDepositContractAddress),
38+
fetchRecentTransactions(apiUrl, sbtcWidthdrawlContractAddress),
39+
]);
40+
41+
const holders = holdersResult.status === 'fulfilled' ? holdersResult.value : undefined;
42+
const btcPrice = btcPriceResult.status === 'fulfilled' ? btcPriceResult.value : 0;
43+
const deposits = depositsResult.status === 'fulfilled' ? depositsResult.value : undefined;
44+
const withdrawals =
45+
withdrawalsResult.status === 'fulfilled' ? withdrawalsResult.value : undefined;
46+
47+
const totalSupply = getFtDecimalAdjustedBalance(holders?.total_supply || '0', SBTC_DECIMALS);
48+
const totalSupplyUsd = totalSupply * btcPrice;
49+
50+
const depositTxs: SBTCTransaction[] = (deposits?.results ?? [])
51+
.filter((tx): tx is Transaction => 'block_height' in tx && tx.tx_status === 'success')
52+
.map(tx => {
53+
const amount = extractAmount(tx);
54+
return {
55+
txId: tx.tx_id,
56+
type: 'deposit' as const,
57+
address: extractDepositRecipient(tx),
58+
amount,
59+
amountUsd: amount * btcPrice,
60+
blockTime: tx.block_time,
61+
};
62+
});
63+
64+
const withdrawalTxs: SBTCTransaction[] = (withdrawals?.results ?? [])
65+
.filter(
66+
(tx): tx is Transaction =>
67+
'block_height' in tx &&
68+
tx.tx_status === 'success' &&
69+
tx.tx_type === 'contract_call' &&
70+
tx.contract_call.function_name === 'initiate-withdrawal-request'
71+
)
72+
.map(tx => {
73+
const amount = extractAmount(tx);
74+
return {
75+
txId: tx.tx_id,
76+
type: 'withdrawal' as const,
77+
address: tx.sender_address,
78+
amount,
79+
amountUsd: amount * btcPrice,
80+
blockTime: tx.block_time,
81+
};
82+
});
83+
84+
const recentTransactions = [...depositTxs, ...withdrawalTxs]
85+
.sort((a, b) => b.blockTime - a.blockTime)
86+
.slice(0, 3);
87+
88+
return {
89+
totalSupply,
90+
totalSupplyUsd,
91+
btcPrice,
92+
recentTransactions,
93+
};
94+
} catch (error) {
95+
logError(error as Error, 'fetchSBTCData', { apiUrl }, 'error');
96+
return undefined;
97+
}
98+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use client';
2+
3+
import { SBTC_ASSET_ID } from '@/app/token/[tokenId]/consts';
4+
import { useGlobalContext } from '@/common/context/useGlobalContext';
5+
import { buildUrl } from '@/common/utils/buildUrl';
6+
import { ButtonLink } from '@/ui/ButtonLink';
7+
import { Text } from '@/ui/Text';
8+
import { Flex, Stack } from '@chakra-ui/react';
9+
10+
import { SBTCOverview } from './SBTCOverview';
11+
import { SBTCTransactions } from './SBTCTransactions';
12+
import { SBTCData } from './types';
13+
14+
interface SBTCSectionProps {
15+
sbtcData?: SBTCData;
16+
}
17+
18+
export function SBTCSection({ sbtcData }: SBTCSectionProps) {
19+
const network = useGlobalContext().activeNetwork;
20+
21+
return (
22+
<Flex direction="column" flex={1} width="100%">
23+
<Flex justifyContent="space-between" alignItems="center" mb={3.5}>
24+
<Text textStyle="heading-md" color="textPrimary">
25+
sBTC
26+
</Text>
27+
<ButtonLink href={buildUrl(`/token/${SBTC_ASSET_ID}`, network)} buttonLinkSize="big" mr={2}>
28+
Explore sBTC
29+
</ButtonLink>
30+
</Flex>
31+
<SBTCOverview
32+
totalSupply={sbtcData?.totalSupply ?? 0}
33+
totalSupplyUsd={sbtcData?.totalSupplyUsd ?? 0}
34+
/>
35+
<Stack mt={8}>
36+
<SBTCTransactions transactions={sbtcData?.recentTransactions ?? []} />
37+
</Stack>
38+
</Flex>
39+
);
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export interface SBTCData {
2+
totalSupply: number;
3+
totalSupplyUsd: number;
4+
btcPrice: number;
5+
recentTransactions: SBTCTransaction[];
6+
}
7+
8+
export interface SBTCTransaction {
9+
txId: string;
10+
type: 'deposit' | 'withdrawal';
11+
address: string;
12+
amount: number;
13+
amountUsd: number;
14+
blockTime: number;
15+
}

0 commit comments

Comments
 (0)