Skip to content

Commit 862f43f

Browse files
committed
feat(web): sBTC enroll functionality, closes LEA-2456
1 parent d625cc0 commit 862f43f

File tree

16 files changed

+383
-128
lines changed

16 files changed

+383
-128
lines changed

.dependency-cruiser.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ module.exports = {
289289
'.storybook',
290290
'.*.js$',
291291
'.*.config.ts',
292+
'.wrangler',
293+
'.react-router',
292294
],
293295
},
294296

apps/web/app/app.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
44
import { Flex, styled } from 'leather-styles/jsx';
55

66
import type { LeatherProvider } from '@leather.io/rpc';
7+
import { Tooltip } from '@leather.io/ui';
78

89
import { Footer } from './layouts/footer/footer';
910
import { GlobalLoader } from './layouts/nav/global-loader';
@@ -40,16 +41,16 @@ export function Layout({ children }: { children: React.ReactNode }) {
4041

4142
export const queryClient = new QueryClient({
4243
defaultOptions: {
43-
queries: {
44-
gcTime: 1,
45-
},
44+
queries: { gcTime: 1 },
4645
},
4746
});
4847

4948
export default function App() {
5049
return (
5150
<QueryClientProvider client={queryClient}>
52-
<Outlet />
51+
<Tooltip.Provider delayDuration={320}>
52+
<Outlet />
53+
</Tooltip.Provider>
5354
</QueryClientProvider>
5455
);
5556
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { BasicTooltip, Button, ButtonProps } from '@leather.io/ui';
2+
3+
import { useEnrolledStatus, useSbtcEnroll } from './use-enroll-transaction';
4+
5+
export function EnrollButtonLayout(props: ButtonProps) {
6+
return <Button fullWidth size="xs" {...props} />;
7+
}
8+
9+
export function SbtcEnrollButton(props: ButtonProps) {
10+
const { data } = useEnrolledStatus();
11+
const { createSbtcYieldEnrollContractCall } = useSbtcEnroll();
12+
13+
if (data && data.isEnrolled)
14+
return (
15+
<BasicTooltip asChild label="You're already enrolled for sBTC rewards">
16+
<EnrollButtonLayout disabled {...props}>
17+
Enrolled
18+
</EnrollButtonLayout>
19+
</BasicTooltip>
20+
);
21+
22+
return (
23+
<EnrollButtonLayout onClick={createSbtcYieldEnrollContractCall} {...props}>
24+
Enroll
25+
</EnrollButtonLayout>
26+
);
27+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useMemo } from 'react';
2+
3+
import { StacksNetworkName } from '@stacks/network';
4+
import { Cl, fetchCallReadOnlyFunction, serializeCV } from '@stacks/transactions';
5+
import { useQuery } from '@tanstack/react-query';
6+
import { leather } from '~/helpers/leather-sdk';
7+
import { useLeatherConnect } from '~/store/addresses';
8+
9+
interface EnrollContractIdentifier {
10+
contractAddress: string;
11+
contractName: string;
12+
contract: string;
13+
}
14+
const sbtcEnrollContractMap = {
15+
testnet: {
16+
contractAddress: 'ST1SY0NMZMBSA28MH31T09KCQWPZ4H5HRMYRX4XW7',
17+
contractName: 'yield-rewards-testnet',
18+
contract: 'ST1SY0NMZMBSA28MH31T09KCQWPZ4H5HRMYRX4XW7.yield-rewards-testnet',
19+
},
20+
devnet: {
21+
contractAddress: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
22+
contractName: 'yield',
23+
contract: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.yield',
24+
},
25+
mainnet: {
26+
contractAddress: 'SP804CDG3KBN9M6E00AD744K8DC697G7HBCG520Q',
27+
contractName: 'sbtc-yield-rewards-v3',
28+
contract: 'SP804CDG3KBN9M6E00AD744K8DC697G7HBCG520Q.sbtc-yield-rewards-v3',
29+
},
30+
mocknet: {} as EnrollContractIdentifier,
31+
} as const satisfies Record<StacksNetworkName, EnrollContractIdentifier>;
32+
33+
function getEnrollContractCallByNetwork(network: StacksNetworkName) {
34+
return sbtcEnrollContractMap[network];
35+
}
36+
37+
async function fetchIsAddressEnrolled(
38+
address: string,
39+
contract: EnrollContractIdentifier,
40+
network: StacksNetworkName
41+
) {
42+
const resp = await fetchCallReadOnlyFunction({
43+
...contract,
44+
functionName: 'is-enrolled-in-next-cycle',
45+
functionArgs: [Cl.principal(address)],
46+
senderAddress: contract.contractAddress,
47+
network,
48+
});
49+
return { isEnrolled: resp.type === 'true', ...resp };
50+
}
51+
52+
// To do add network support
53+
const network = { networkName: 'mainnet' } as const;
54+
55+
export function useEnrolledStatus() {
56+
const { stacksAccount } = useLeatherConnect();
57+
58+
const query = useQuery({
59+
queryFn: () =>
60+
fetchIsAddressEnrolled(
61+
stacksAccount?.address ?? '',
62+
getEnrollContractCallByNetwork(network.networkName),
63+
network.networkName
64+
),
65+
queryKey: ['is-enrolled', stacksAccount?.address, network.networkName],
66+
});
67+
68+
return query;
69+
}
70+
71+
export function useSbtcEnroll() {
72+
const { stacksAccount } = useLeatherConnect();
73+
74+
return useMemo(
75+
() => ({
76+
async createSbtcYieldEnrollContractCall() {
77+
// if (network.networkName === 'mocknet') throw new Error('Mocknet not supported');
78+
79+
if (!stacksAccount) throw new Error('No address');
80+
81+
const contractDetails = getEnrollContractCallByNetwork(network.networkName);
82+
83+
try {
84+
const result = await leather.stxCallContract({
85+
contract: contractDetails.contract,
86+
functionName: 'enroll',
87+
functionArgs: [serializeCV(Cl.some(Cl.principal(stacksAccount.address)))],
88+
network: network.networkName,
89+
});
90+
// eslint-disable-next-line no-console
91+
console.log(result);
92+
} catch (e) {
93+
// eslint-disable-next-line no-console
94+
console.log('Error creating sbtc yield enroll contract call', e);
95+
}
96+
},
97+
}),
98+
[stacksAccount]
99+
);
100+
}

apps/web/app/features/sign-in-button/sign-in-button.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useLeatherConnect } from '~/store/addresses';
2+
import { openExternalLink } from '~/utils/external-links';
23

34
import { LEATHER_EXTENSION_CHROME_STORE_URL } from '@leather.io/constants';
45

56
import { ActiveAccountButtonLayout, SignInButtonLayout } from './sign-in-button.layout';
67

78
function InstallLeatherButton() {
89
return (
9-
<SignInButtonLayout onClick={() => window.open(LEATHER_EXTENSION_CHROME_STORE_URL, '_blank')}>
10+
<SignInButtonLayout onClick={() => openExternalLink(LEATHER_EXTENSION_CHROME_STORE_URL)}>
1011
Install
1112
</SignInButtonLayout>
1213
);

apps/web/app/features/stacking/components/choose-pooling-amount.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { microStxToStx } from '@leather.io/utils';
1616
export function ChoosePoolingAmount() {
1717
const { setValue, control } = useFormContext<StackingPoolFormSchema>();
1818

19-
const { stxAddress } = useLeatherConnect();
19+
const { stacksAccount: stxAddress } = useLeatherConnect();
2020

2121
if (!stxAddress) throw new Error('No stx address available');
2222

apps/web/app/features/stacking/providers/stacking-client-provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface Props {
1717
}
1818

1919
export function StackingClientProvider({ children }: Props) {
20-
const { stxAddress } = useLeatherConnect();
20+
const { stacksAccount: stxAddress } = useLeatherConnect();
2121
const { network } = useStacksNetwork();
2222

2323
const client = useMemo<StackingClient | null>(() => {

apps/web/app/features/stacking/start-pooled-stacking.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,9 @@ import { PoolName, WrapperPrincipal } from './utils/types-preset-pools';
4848
interface StartPooledStackingProps {
4949
poolName: PoolName;
5050
}
51-
5251
export function StartPooledStacking({ poolName }: StartPooledStackingProps) {
5352
const { client } = useStackingClient();
54-
const { stxAddress } = useLeatherConnect();
53+
const { stacksAccount: stxAddress } = useLeatherConnect();
5554

5655
if (!stxAddress || !client) {
5756
return 'You should connect STX wallet';
@@ -75,7 +74,7 @@ interface StartPooledStackingLayoutProps {
7574
}
7675

7776
function StartPooledStackingLayout({ poolName, client }: StartPooledStackingLayoutProps) {
78-
const { stxAddress, btcAddressP2wpkh } = useLeatherConnect();
77+
const { stacksAccount, btcAddressP2wpkh } = useLeatherConnect();
7978
const { network, networkInstance, networkPreference } = useStacksNetwork();
8079
const poxContracts = useMemo(() => getPoxContracts(network), [network]);
8180

@@ -87,23 +86,23 @@ function StartPooledStackingLayout({ poolName, client }: StartPooledStackingLayo
8786
contractAddress,
8887
contractName,
8988
callingContract: poxContracts['WrapperFastPool'],
90-
senderAddress: stxAddress ? stxAddress.address : null,
89+
senderAddress: stacksAccount ? stacksAccount.address : null,
9190
network,
9291
});
9392

9493
const getAllowanceContractCallersRestakeQuery = useGetAllowanceContractCallersQuery({
9594
contractAddress,
9695
contractName,
9796
callingContract: poxContracts['WrapperRestake'],
98-
senderAddress: stxAddress ? stxAddress.address : null,
97+
senderAddress: stacksAccount ? stacksAccount.address : null,
9998
network,
10099
});
101100

102101
const getAllowanceContractCallersOneCycleQuery = useGetAllowanceContractCallersQuery({
103102
contractAddress,
104103
contractName,
105104
callingContract: poxContracts['WrapperOneCycle'],
106-
senderAddress: stxAddress ? stxAddress.address : null,
105+
senderAddress: stacksAccount ? stacksAccount.address : null,
107106
network,
108107
});
109108

apps/web/app/helpers/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ export function isLeatherInstalled() {
66
}
77

88
export type ExtensionState = 'missing' | 'detected' | 'connected';
9+
10+
export function whenExtensionState(state: ExtensionState) {
11+
return <T>(cases: { missing: T; detected: T; connected: T }): T => cases[state];
12+
}

apps/web/app/pages/sbtc-rewards/components/acquire-sbtc-grid.tsx

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { Box, Flex, GridProps, styled } from 'leather-styles/jsx';
2+
import { WhenClient } from '~/components/client-only';
23
import { DummyIcon } from '~/components/dummy';
34
import { BitcoinIcon } from '~/components/icons/bitcoin-icon';
45
import { StacksIcon } from '~/components/icons/stacks-icon';
56
import { AcquireSbtcGridLayout } from '~/pages/sbtc-rewards/components/acquire-sbtc-grid.layout';
67

7-
import { Button } from '@leather.io/ui';
8+
import { Badge, Button } from '@leather.io/ui';
9+
10+
import { BridgingStatus, useSbtcRewardContext } from '../sbtc-rewards-context';
811

912
function AcquireSbtcInstructions() {
1013
return (
@@ -14,6 +17,7 @@ function AcquireSbtcInstructions() {
1417
<styled.h3 textStyle="heading.05" mt="space.05">
1518
Choose to bridge or swap
1619
</styled.h3>
20+
1721
<styled.p textStyle="caption.01" mt="space.01">
1822
sBTC can be acquired either by bridging BTC to the Stacks blockchain or swapping another
1923
asset on Stacks on the L2 itself.
@@ -23,46 +27,85 @@ function AcquireSbtcInstructions() {
2327
);
2428
}
2529

30+
function MaxCapacity({ bridgingStatus }: { bridgingStatus: BridgingStatus }) {
31+
if (bridgingStatus !== 'disabled') return null;
32+
return <Badge mt="space.04" variant="info" label="Max bridging capacity reached" />;
33+
}
34+
2635
function BridgeToSbtcCell() {
36+
const { onBridgeSbtc, bridgingStatus, whenExtensionState } = useSbtcRewardContext();
2737
return (
2838
<Flex flexDir={['column', 'row', 'column', 'row']} justifyContent="space-between" p="space.05">
29-
<Box>
39+
<Flex flexDir="column" flex={1} justifyContent="space-between">
3040
<BitcoinIcon size={32} />
3141

32-
<styled.h4 textStyle="heading.05" mt="space.05">
33-
Bridge BTC to sBTC
34-
</styled.h4>
35-
<styled.p textStyle="caption.01" mt="space.01" mr="space.05">
36-
Bitcoin to Stacks via a trust-minimizing protocol
37-
</styled.p>
38-
</Box>
42+
<Box mt="space.04">
43+
<styled.h4 textStyle="heading.05">Bridge BTC to sBTC</styled.h4>
44+
<styled.p textStyle="caption.01" mt="space.01" mr="space.05">
45+
Bitcoin to Stacks via a trust-minimizing protocol
46+
</styled.p>
47+
<MaxCapacity bridgingStatus={bridgingStatus} />
48+
</Box>
49+
</Flex>
3950
<Flex alignItems="flex-end">
40-
<Button mt="space.04" size="xs">
41-
Bridge
42-
</Button>
51+
<WhenClient>
52+
<Button
53+
onClick={onBridgeSbtc}
54+
disabled={bridgingStatus !== 'enabled'}
55+
mt="space.04"
56+
size="xs"
57+
>
58+
{whenExtensionState({
59+
connected: 'Bridge',
60+
detected: 'Sign in to bridge',
61+
missing: 'Install Leather to bridge',
62+
})}
63+
</Button>
64+
</WhenClient>
4365
</Flex>
4466
</Flex>
4567
);
4668
}
4769

4870
function SwapStxToSbtcCell() {
71+
const { onSwapStxSbtc, whenExtensionState } = useSbtcRewardContext();
4972
return (
50-
<Flex flexDir={['column', 'row', 'column', 'row']} justifyContent="space-between" p="space.05">
51-
<Box>
73+
<Flex
74+
flexDir={['column', 'row', 'column', 'row']}
75+
flex={1}
76+
justifyContent="space-between"
77+
p="space.05"
78+
>
79+
<Flex flexDir="column" flex={1} justifyContent="space-between">
5280
<StacksIcon size={32} />
5381

54-
<styled.h4 textStyle="heading.05" mt="space.05">
55-
Swap Stacks tokens for sBTC
56-
</styled.h4>
82+
<Box mt="space.04">
83+
<styled.h4 textStyle="heading.05">Swap Stacks tokens for sBTC</styled.h4>
5784

58-
<styled.p textStyle="caption.01" mt="space.01" mr="space.05">
59-
On Stacks via decentralized liquidity pools
60-
</styled.p>
61-
</Box>
85+
<styled.p textStyle="caption.01" mt="space.01" mr="space.05">
86+
On Stacks via decentralized liquidity pools
87+
</styled.p>
88+
</Box>
89+
</Flex>
6290
<Flex alignItems="flex-end">
63-
<Button mt="space.04" size="xs">
64-
Swap
65-
</Button>
91+
<WhenClient fallback={<Button width="52px" size="xs" aria-busy />}>
92+
<Button
93+
disabled={whenExtensionState({
94+
connected: false,
95+
detected: true,
96+
missing: true,
97+
})}
98+
onClick={onSwapStxSbtc}
99+
mt="space.04"
100+
size="xs"
101+
>
102+
{whenExtensionState({
103+
connected: 'Swap',
104+
detected: 'Sign in to swap',
105+
missing: 'Install Leather to swap',
106+
})}
107+
</Button>
108+
</WhenClient>
66109
</Flex>
67110
</Flex>
68111
);

0 commit comments

Comments
 (0)