Skip to content

Commit affafa8

Browse files
authored
Merge pull request #13 from leandrogavidia/small-additions-to-react-hook-examples
Enhance React Hooks Example Components
2 parents 3fb459d + 2b7a902 commit affafa8

File tree

6 files changed

+110
-30
lines changed

6 files changed

+110
-30
lines changed

examples/react-hooks/src/components/AccountInspectorCard.tsx

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useAccount } from '@solana/react-hooks';
22
import { type ChangeEvent, useMemo, useState } from 'react';
33

4+
import { formatAccountData } from '../lib/utils';
45
import { Button } from './ui/button';
56
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
67
import { Input } from './ui/input';
@@ -15,16 +16,7 @@ export function AccountInspectorCard() {
1516
watch,
1617
});
1718

18-
const formattedData = useMemo(() => {
19-
if (!account?.data) {
20-
return 'No account data fetched yet.';
21-
}
22-
try {
23-
return JSON.stringify(account.data, null, 2);
24-
} catch {
25-
return String(account.data);
26-
}
27-
}, [account]);
19+
const formattedData = useMemo(() => formatAccountData(account?.data), [account]);
2820

2921
const statusLabel = (() => {
3022
if (target === '') {
@@ -33,10 +25,10 @@ export function AccountInspectorCard() {
3325
if (!account) {
3426
return 'Loading account data…';
3527
}
36-
if (account.error) {
37-
return `Fetch error: ${formatError(account.error)}`;
28+
if (account?.error) {
29+
return `Fetch error: ${formatError(account?.error)}`;
3830
}
39-
return `Lamports: ${account.lamports ?? 'unknown'} · Slot: ${account.slot ?? 'unknown'}`;
31+
return `Lamports: ${account?.lamports ?? 'unknown'} · Slot: ${account?.slot ?? 'unknown'}`;
4032
})();
4133

4234
const handleAddressChange = (event: ChangeEvent<HTMLInputElement>) => {

examples/react-hooks/src/components/SplTokenPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useSplToken, useWalletSession } from '@solana/react-hooks';
22
import { type FormEvent, useState } from 'react';
33

44
import { computeSplAmountStep, formatSplBalanceStatus, formatSplTransferStatus } from './demoUi';
5+
import { UsdcFaucetButton } from './UsdcFaucetButton';
56
import { Button } from './ui/button';
67
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './ui/card';
78
import { Input } from './ui/input';
@@ -72,6 +73,7 @@ export function SplTokenPanel() {
7273
<CardTitle>USDC (Devnet)</CardTitle>
7374
<CardDescription>
7475
Inspect the balance helper and send SPL transfers using the <code>useSplToken</code> hook.
76+
<UsdcFaucetButton className="mt-4" />
7577
</CardDescription>
7678
</div>
7779
</CardHeader>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { cn } from '../lib/utils';
2+
import { DollarIcon } from './icons/DollarIcon';
3+
4+
interface UsdcFaucetLinkProps {
5+
className?: string;
6+
}
7+
8+
export function UsdcFaucetButton({ className }: UsdcFaucetLinkProps) {
9+
return (
10+
<div className={cn('flex items-center justify-start gap-2', className)}>
11+
<span className="text-sm text-muted-foreground">Need test USDC?</span>
12+
<a
13+
href="https://faucet.circle.com/"
14+
target="_blank"
15+
rel="noopener noreferrer"
16+
className="inline-flex items-center gap-1.5 rounded bg-[#2775CA] px-3 py-1 text-xs font-medium text-white transition-colors hover:bg-[#1e5ba8] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#2775CA] focus-visible:ring-offset-1"
17+
>
18+
<DollarIcon className="h-3 w-3" />
19+
Get from Faucet
20+
</a>
21+
</div>
22+
);
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
interface DollarIconProps {
2+
className?: string;
3+
}
4+
5+
export function DollarIcon({ className }: DollarIconProps) {
6+
return (
7+
<svg
8+
className={className}
9+
fill="currentColor"
10+
viewBox="0 0 24 24"
11+
xmlns="http://www.w3.org/2000/svg"
12+
aria-hidden="true"
13+
>
14+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.31-8.86c-1.77-.45-2.34-.94-2.34-1.67 0-.84.79-1.43 2.1-1.43 1.38 0 1.9.66 1.94 1.64h1.71c-.05-1.34-.87-2.57-2.49-2.97V5H10.9v1.69c-1.51.32-2.72 1.3-2.72 2.81 0 1.79 1.49 2.69 3.66 3.21 1.95.46 2.34 1.15 2.34 1.87 0 .53-.39 1.39-2.1 1.39-1.6 0-2.23-.72-2.32-1.64H8.04c.1 1.7 1.36 2.66 2.86 2.97V19h2.34v-1.67c1.52-.29 2.72-1.16 2.72-2.84 0-2.27-1.72-3.12-3.65-3.55z" />
15+
</svg>
16+
);
17+
}

examples/react-hooks/src/lib/utils.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,43 @@ import { twMerge } from 'tailwind-merge';
44
export function cn(...inputs: ClassValue[]): string {
55
return twMerge(clsx(inputs));
66
}
7+
8+
/**
9+
* Formats account data for display purposes.
10+
* Handles Uint8Array, bigint, and complex objects with special formatting.
11+
*
12+
* @param data - The account data to format
13+
* @returns A formatted string representation of the data
14+
*/
15+
export function formatAccountData(data: unknown): string {
16+
if (!data) {
17+
return 'No account data fetched yet.';
18+
}
19+
20+
try {
21+
if (data instanceof Uint8Array) {
22+
return `Uint8Array(${data.length} bytes):\n${Array.from(data.slice(0, 100))
23+
.map((byte) => byte.toString(16).padStart(2, '0'))
24+
.join(' ')}${data.length > 100 ? '\n... (truncated)' : ''}`;
25+
}
26+
27+
return JSON.stringify(
28+
data,
29+
(_, value) => {
30+
if (typeof value === 'bigint') {
31+
return value.toString();
32+
}
33+
if (value instanceof Uint8Array) {
34+
return `Uint8Array(${value.length})`;
35+
}
36+
return value;
37+
},
38+
2,
39+
);
40+
} catch {
41+
if (typeof data === 'object' && data !== null) {
42+
return `[Complex Object]\nType: ${data.constructor?.name || 'Unknown'}\nKeys: ${Object.keys(data).join(', ')}`;
43+
}
44+
return String(data);
45+
}
46+
}

packages/client/src/client/actions.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -463,24 +463,30 @@ export function createActions({ connectors, logger: inputLogger, runtime, store
463463
* @returns Promise resolving with the signature for the airdrop transaction.
464464
*/
465465
async function requestAirdrop(address: Address, lamports: Lamports) {
466-
if (!('requestAirdrop' in runtime.rpc)) {
467-
throw new Error('The current RPC endpoint does not support airdrops.');
466+
try {
467+
const factory = airdropFactory({
468+
rpc: runtime.rpc,
469+
rpcSubscriptions: runtime.rpcSubscriptions,
470+
} as Parameters<typeof airdropFactory>[0]);
471+
const signature = await factory({
472+
commitment: getCommitment('confirmed'),
473+
lamports,
474+
recipientAddress: address,
475+
});
476+
logger({
477+
data: { address: address.toString(), lamports: lamports.toString(), signature },
478+
level: 'info',
479+
message: 'airdrop requested',
480+
});
481+
return signature;
482+
} catch (error) {
483+
logger({
484+
data: { address: address.toString(), lamports: lamports.toString(), ...formatError(error) },
485+
level: 'error',
486+
message: 'airdrop request failed',
487+
});
488+
throw error;
468489
}
469-
const factory = airdropFactory({
470-
rpc: runtime.rpc,
471-
rpcSubscriptions: runtime.rpcSubscriptions,
472-
} as Parameters<typeof airdropFactory>[0]);
473-
const signature = await factory({
474-
commitment: getCommitment('confirmed'),
475-
lamports,
476-
recipientAddress: address,
477-
});
478-
logger({
479-
data: { address: address.toString(), lamports: lamports.toString(), signature },
480-
level: 'info',
481-
message: 'airdrop requested',
482-
});
483-
return signature;
484490
}
485491

486492
return {

0 commit comments

Comments
 (0)