Skip to content

Commit 33baea4

Browse files
VanishMaxvacekj
andauthored
feat: quests page (#30)
* feat: prepare for the quests page * feat: quests setup and port wallet install page * feat: port the rest of the pages and install and use biome * feat: add tailwindˆ * feat: remove chakra & add swap component * feat: remove chakra & merge pages * feat: fix styles everywhere * fix: build * fixes * fix: open links in new tab * feat: show balances * fix: deposit styling in dark mode * fix: deposits monitoring * fix: deposits display --------- Co-authored-by: Atris <[email protected]>
1 parent 45cedd9 commit 33baea4

31 files changed

+6895
-1323
lines changed

biome.json

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
3+
"organizeImports": {
4+
"enabled": true
5+
},
6+
"vcs": {
7+
"enabled": true,
8+
"clientKind": "git",
9+
"useIgnoreFile": true
10+
},
11+
"formatter": {
12+
"indentStyle": "space",
13+
"enabled": true
14+
},
15+
"javascript": {
16+
"formatter": {
17+
"quoteStyle": "single"
18+
}
19+
},
20+
"linter": {
21+
"enabled": false,
22+
"rules": {
23+
"recommended": true,
24+
"style": {
25+
"noNonNullAssertion": "off"
26+
}
27+
}
28+
}
29+
}

components/Balances.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useBalances } from '@/components/hooks';
2+
import type { ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
3+
import { ValueViewComponent } from '@penumbra-zone/ui/ValueViewComponent';
4+
import type React from 'react';
5+
6+
export function Balances() {
7+
const { data: balances } = useBalances();
8+
9+
return balances
10+
? balances
11+
.map((bal) => bal.balanceView)
12+
.map((balanceView) => (
13+
<BalanceRow
14+
key={balanceView!.toJsonString()}
15+
balance={balanceView!}
16+
/>
17+
))
18+
: null;
19+
}
20+
21+
function BalanceRow({
22+
balance,
23+
}: {
24+
balance: ValueView;
25+
}) {
26+
return (
27+
<div
28+
className="mt-3 flex gap-3 items-center bg-gray-700 text-white p-3"
29+
key={balance.toJsonString()}
30+
>
31+
<ValueViewComponent valueView={balance} />
32+
</div>
33+
);
34+
}

components/Deposit.tsx

+253
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { client } from '@/components/penumbra';
2+
import { useQuestStore } from '@/components/store';
3+
import { ViewService } from '@penumbra-zone/protobuf';
4+
import { ValueView } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
5+
import type { CommitmentSource_Ics20Transfer } from '@penumbra-zone/protobuf/penumbra/core/component/sct/v1/sct_pb';
6+
import { AddressView } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
7+
import type { NotesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
8+
import { AddressViewComponent } from '@penumbra-zone/ui/AddressViewComponent';
9+
import { ValueViewComponent } from '@penumbra-zone/ui/ValueViewComponent';
10+
import { useQuery } from '@tanstack/react-query';
11+
import { capitalize } from 'es-toolkit';
12+
import { ChevronRightIcon } from 'lucide-react';
13+
import React, { useState } from 'react';
14+
import {
15+
useConnect,
16+
useCurrentChainStatus,
17+
useEphemeralAddress,
18+
useNotes,
19+
useSetScanSinceBlock,
20+
useSwaps,
21+
useWalletManifests,
22+
} from './hooks';
23+
24+
const Deposit: React.FC = () => {
25+
useSetScanSinceBlock();
26+
const [showOld, setShowOld] = useState(false);
27+
28+
const { data } = useNotes();
29+
const { connected, onConnect, connectionLoading } = useConnect();
30+
const { data: wallets, isLoading } = useWalletManifests();
31+
32+
const { data: status } = useCurrentChainStatus();
33+
const currentBlock = BigInt(status?.syncInfo?.latestBlockHeight ?? 0);
34+
const depositNotes =
35+
data?.filter(
36+
(note) => note?.noteRecord?.source?.source.case === 'ics20Transfer',
37+
) ?? [];
38+
39+
const { scanSinceBlockHeight } = useQuestStore();
40+
console.log(showOld);
41+
const { data: notesWithMetadata } = useQuery({
42+
queryKey: [
43+
'notesWithMetadata',
44+
currentBlock.toString(),
45+
showOld,
46+
depositNotes,
47+
connected,
48+
],
49+
staleTime: 0,
50+
initialData: [],
51+
queryFn: async () => {
52+
console.log('refetch');
53+
const deposits = await Promise.all(
54+
depositNotes.map(async (note) => {
55+
const metadata = await client.service(ViewService).assetMetadataById({
56+
assetId: note.noteRecord?.note?.value?.assetId!,
57+
});
58+
59+
return {
60+
metadata,
61+
note,
62+
valueView: new ValueView({
63+
valueView: {
64+
case: 'knownAssetId',
65+
value: {
66+
metadata: metadata.denomMetadata!,
67+
amount: note?.noteRecord?.note?.value?.amount!,
68+
},
69+
},
70+
}),
71+
};
72+
}),
73+
);
74+
return deposits.filter(
75+
(d) =>
76+
Number(d.note!.noteRecord!.heightCreated) >
77+
(showOld ? 0 : scanSinceBlockHeight),
78+
);
79+
},
80+
});
81+
82+
const { data: ibcInAddress } = useEphemeralAddress({
83+
index: 0,
84+
});
85+
86+
return (
87+
<div className="py-3 flex flex-col gap-8">
88+
<div>
89+
Now it's time to shield your funds and transfer them into Penumbra.
90+
We've displayed one of your IBC Deposit addresses for you convenience
91+
below. Copy it using the button on the right.
92+
</div>
93+
94+
{!isLoading &&
95+
wallets &&
96+
!connected &&
97+
Object.entries(wallets).map(([origin, manifest]) => (
98+
<button
99+
// type={'button'}
100+
key={origin}
101+
onClick={() => onConnect(origin)}
102+
disabled={connectionLoading}
103+
className="bg-blue-700 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
104+
>
105+
{connectionLoading
106+
? 'Connecting...'
107+
: `Connect to ${manifest.name}`}
108+
</button>
109+
))}
110+
111+
{ibcInAddress?.address && connected && (
112+
<div className={'bg-gray-700 p-3'}>
113+
<AddressViewComponent
114+
addressView={
115+
new AddressView({
116+
addressView: {
117+
case: 'decoded',
118+
value: {
119+
address: ibcInAddress.address,
120+
},
121+
},
122+
})
123+
}
124+
/>
125+
</div>
126+
)}
127+
128+
<div
129+
className="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4"
130+
role="alert"
131+
>
132+
<p className="font-bold">Info</p>
133+
<p>
134+
IBC Deposit addresses exist because source chains record the deposit
135+
address. They serve as an additional layer of anonymity to not link
136+
your deposit and actual addresses.
137+
</p>
138+
</div>
139+
140+
<div>
141+
We will use&nbsp;
142+
<a
143+
href="https://go.skip.build/"
144+
className="font-medium underline"
145+
target="_blank"
146+
rel="noopener noreferrer"
147+
>
148+
Skip Protocol
149+
</a>
150+
&nbsp; to bridge funds into Penumbra. Go to the Skip app, and input your
151+
IBC Deposit address. Select your source chain and asset (we recommend
152+
USDC, but any common asset is fine) and select Penumbra and USDC as the
153+
destination chain. Then initiate the deposit and come back to this page.
154+
</div>
155+
156+
<div
157+
className="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4"
158+
role="alert"
159+
>
160+
<p className="font-bold">Info</p>
161+
<p>
162+
Penumbra supports paying fees in multiple tokens, including USDC. Prax
163+
will always choose the best token to pay fees with depending on your
164+
balances.
165+
</p>
166+
</div>
167+
168+
{notesWithMetadata.length === 0 && (
169+
<div className="w-full bg-white text-black shadow-md rounded-lg p-4">
170+
<div className="flex flex-row gap-3 items-center">
171+
<div>Waiting for a deposit to occur</div>
172+
<div className="animate-spin h-5 w-5 border-2 border-blue-500 rounded-full border-t-transparent" />
173+
</div>
174+
</div>
175+
)}
176+
177+
{notesWithMetadata.length > 0 && (
178+
<div className="border-0">
179+
<div className="mt-3 group-open:animate-fadeIn">
180+
{notesWithMetadata.length > 0 &&
181+
notesWithMetadata?.map(({ note, valueView }) => (
182+
<DepositRow
183+
key={note.toJsonString()}
184+
note={note}
185+
valueView={valueView}
186+
/>
187+
))}
188+
</div>
189+
</div>
190+
)}
191+
192+
<div className={'flex items-center'}>
193+
<input
194+
id="default-checkbox"
195+
checked={showOld}
196+
type={'checkbox'}
197+
onChange={(e) => setShowOld((old) => !old)}
198+
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
199+
/>
200+
<label
201+
htmlFor="default-checkbox"
202+
className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300"
203+
>
204+
Show old deposits
205+
</label>
206+
</div>
207+
</div>
208+
);
209+
};
210+
211+
function DepositRow({
212+
note,
213+
valueView,
214+
}: {
215+
note: NotesResponse;
216+
valueView: ValueView;
217+
}) {
218+
const source = note.noteRecord?.source?.source
219+
?.value as CommitmentSource_Ics20Transfer;
220+
const chainId = source.sender.replace(/^(\D+)(\d).*$/, '$1-$2');
221+
222+
const chainName = capitalize(source.sender.replace(/^(\D+).*$/, '$1'));
223+
return (
224+
<div
225+
className="mt-3 flex gap-3 items-center bg-gray-700 text-white p-3"
226+
key={note.toJsonString()}
227+
>
228+
Deposited
229+
<ValueViewComponent key={note.toJsonString()} valueView={valueView} />
230+
from {chainName}
231+
<ChevronRightIcon className="h-4 w-4" />
232+
<a
233+
className="underline"
234+
target="_blank"
235+
rel="noopener noreferrer"
236+
href={`https://ibc.range.org/ibc/status?id=${chainIdToExplorerChainName(chainId)}/${source.channelId}/${source.packetSeq}`}
237+
>
238+
Inspect deposit
239+
</a>
240+
</div>
241+
);
242+
}
243+
244+
function chainIdToExplorerChainName(chainId: string) {
245+
switch (chainId) {
246+
case 'osmo-1':
247+
return 'osmosis-1';
248+
default:
249+
return chainId;
250+
}
251+
}
252+
253+
export default Deposit;

components/Disconnect.tsx

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type React from 'react';
2+
import { useEffect, useRef } from 'react';
3+
import { useBalances, useConnect } from './hooks';
4+
5+
const Disconnect: React.FC = () => {
6+
const { connected } = useConnect();
7+
8+
return (
9+
<div className="py-3 flex flex-col gap-8">
10+
<div>
11+
Once you are done working with a page, you can disconnect your wallet.
12+
To do this in Prax. You can go to the Settings, click Connected sites,
13+
and click the trash button next to the site URL. This will disconnect
14+
the extension from the site, after which the site can no longer access
15+
your data.
16+
</div>
17+
{connected && (
18+
<div className="w-full bg-white shadow-md rounded-lg p-4">
19+
<div className="flex flex-row gap-3 items-center">
20+
<div>
21+
Waiting for extension to disconnect. Refresh the page once
22+
disconnected.
23+
</div>
24+
<div className="animate-spin h-5 w-5 border-2 border-blue-500 rounded-full border-t-transparent" />
25+
</div>
26+
</div>
27+
)}
28+
{!connected && (
29+
<div>Congratulations. The site can no longer access your data.</div>
30+
)}
31+
</div>
32+
);
33+
};
34+
35+
type IntervalFunction = () => void;
36+
37+
function useInterval(callback: IntervalFunction, delay: number | null) {
38+
const savedCallback = useRef<IntervalFunction>();
39+
40+
// Remember the latest callback
41+
useEffect(() => {
42+
savedCallback.current = callback;
43+
}, [callback]);
44+
45+
// Set up the interval
46+
useEffect(() => {
47+
function tick() {
48+
savedCallback.current?.();
49+
}
50+
if (delay !== null) {
51+
const id = setInterval(tick, delay);
52+
return () => clearInterval(id);
53+
}
54+
}, [delay]);
55+
}
56+
57+
export default Disconnect;

0 commit comments

Comments
 (0)