Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions app/features/idl/ui/IdlCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { cn } from '@shared/utils';
import { useEffect, useMemo, useState } from 'react';
import { AlertTriangle, ExternalLink } from 'react-feather';

import { clusterSlug } from '@/app/utils/cluster';

import { BaseWarningCard } from '../interactive-idl/ui/BaseWarningCard';
import { IdlVariant, useIdlLastTransactionDate } from '../model/use-idl-last-transaction-date';
import { IdlInstructionSection } from './IdlInstructionSection';
Expand All @@ -21,6 +23,7 @@ type IdlTab = {

export function IdlCard({ programId }: { programId: string }) {
const { url, cluster } = useCluster();
const network = clusterSlug(cluster);
const { idl } = useAnchorProgram(programId, url, cluster);
const { programMetadataIdl } = useProgramMetadataIdl(programId, url, cluster);
const [activeTabIndex, setActiveTabIndex] = useState<number>();
Expand Down Expand Up @@ -149,6 +152,8 @@ export function IdlCard({ programId }: { programId: string }) {
</Badge>
}
idl={activeTab.idl}
idlSource={activeTab.id}
network={network}
programId={programId}
searchStr={searchStr}
onSearchChange={setSearchStr}
Expand Down
109 changes: 103 additions & 6 deletions app/features/idl/ui/IdlSection.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,89 @@
import type { SupportedIdl } from '@entities/idl';
import { Button } from '@shared/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@shared/ui/dialog';
import { Input } from '@shared/ui/input';
import { Label } from '@shared/ui/label';
import { Switch } from '@shared/ui/switch';
import { useMemo, useState } from 'react';
import { Code, Download, Search } from 'react-feather';
import { useEffect, useMemo, useRef, useState } from 'react';
import { AlertCircle, Code, Download, ExternalLink, Search } from 'react-feather';

import { WalletProvider } from '@/app/providers/wallet-provider';
import { triggerDownload } from '@/app/shared/lib/triggerDownload';

import { type IdlVariant } from '../model/use-idl-last-transaction-date';
import { IdlRenderer } from './IdlRenderer';

export function IdlSection({
idl,
badge,
programId,
idlSource,
network,
searchStr,
onSearchChange,
}: {
idl: SupportedIdl;
badge: React.ReactNode;
programId: string;
idlSource: IdlVariant;
network: string;
searchStr: string;
onSearchChange: (str: string) => void;
}) {
const [isExpanded, setIsExpanded] = useState(false);
const [isRawIdlView, setIsRawIdlView] = useState(false);
const [isCastawayDialogOpen, setIsCastawayDialogOpen] = useState(false);
const downloadDropdownRef = useRef<HTMLButtonElement>(null);

const idlBase64 = useMemo(() => {
return Buffer.from(JSON.stringify(idl, null, 2)).toString('base64');
}, [idl]);
const castawayUrl = useMemo(() => {
const params = new URLSearchParams({ idlSource, network, program: programId });
return `https://www.castaway.lol/?${params.toString()}`;
}, [idlSource, network, programId]);

useEffect(() => {
if (!downloadDropdownRef.current) {
return;
}

let isMounted = true;
let dropdown: { dispose: () => void } | null = null;

void import('bootstrap/js/dist/dropdown').then(module => {
if (!isMounted || !downloadDropdownRef.current) {
return;
}

const BsDropdown = module.default;
dropdown = new BsDropdown(downloadDropdownRef.current, {
popperConfig() {
return { strategy: 'fixed' as const };
},
});
});

return () => {
isMounted = false;
dropdown?.dispose();
};
}, []);

const handleDownloadIdl = () => triggerDownload(idlBase64, `${programId}-idl.json`);
const handleOpenCastawayDialog = () => setIsCastawayDialogOpen(true);
const handleCastawayContinue = () => {
window.open(castawayUrl, '_blank', 'noopener,noreferrer');
setIsCastawayDialogOpen(false);
};

return (
<>
Expand All @@ -58,10 +110,55 @@ export function IdlSection({
</div>
)}
<div className="e-flex e-items-center e-gap-2">
<Button variant="outline" size="sm" onClick={handleDownloadIdl}>
<Download size={12} />
Download
</Button>
<div className="dropdown e-overflow-visible">
<Button
variant="outline"
size="sm"
ref={downloadDropdownRef}
data-bs-toggle="dropdown"
type="button"
aria-label="Download"
>
<Download size={12} />
Download
</Button>
<div className="dropdown-menu-end dropdown-menu e-z-10">
<div className="d-flex e-flex-col">
<Button onClick={handleDownloadIdl}>Download IDL</Button>
<Button onClick={handleOpenCastawayDialog}>Generate SDK</Button>
</div>
</div>
</div>

<Dialog open={isCastawayDialogOpen} onOpenChange={setIsCastawayDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle className="e-flex e-items-center e-gap-2">
<AlertCircle className="e-text-destructive" size={16} />
Leaving Solana Explorer
</DialogTitle>
</DialogHeader>
<div className="e-space-y-2 e-pl-6">
<DialogDescription>
You are now leaving Explorer and going to Castaway.
</DialogDescription>
<DialogDescription className="e-break-all e-font-mono e-text-xs">
{castawayUrl}
</DialogDescription>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" size="sm">
Cancel
</Button>
</DialogClose>
<Button variant="default" size="sm" onClick={handleCastawayContinue}>
Continue
<ExternalLink size={12} />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

<Button
variant={isRawIdlView ? 'accent' : 'outline'}
Expand Down
46 changes: 46 additions & 0 deletions app/features/idl/ui/__tests__/IdlCard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,29 @@ describe('IdlCard', () => {
});
expect(screen.getByText(/Program Metadata IDL/)).toBeInTheDocument();
expect(screen.queryByText(/Anchor IDL/)).not.toBeInTheDocument();

const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
const downloadButton = screen.getByRole('button', { name: 'Download' });
fireEvent.click(downloadButton);
const generateSdkButton = screen.getByRole('button', { name: 'Generate SDK' });
fireEvent.click(generateSdkButton);

expect(screen.getByText('Leaving Solana Explorer')).toBeInTheDocument();
expect(screen.getByText('You are now leaving Explorer and going to Castaway.')).toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: 'Continue' }));

expect(windowOpenSpy).toHaveBeenCalledTimes(1);
const [openedUrl, target, features] = windowOpenSpy.mock.calls[0]!;
const castawayUrl = new URL(openedUrl as string);
expect(castawayUrl.origin).toBe('https://www.castaway.lol');
expect(castawayUrl.pathname).toBe('/');
expect(castawayUrl.searchParams.get('program')).toBe(programId);
expect(castawayUrl.searchParams.get('idlSource')).toBe('program-metadata');
expect(castawayUrl.searchParams.get('network')).toBe('mainnet-beta');
expect(target).toBe('_blank');
expect(features).toBe('noopener,noreferrer');
windowOpenSpy.mockRestore();
});

test('should render IdlCard with Anchor IDL when anchorIdl exists', async () => {
Expand All @@ -149,6 +172,29 @@ describe('IdlCard', () => {
});
expect(screen.getByText(/Anchor IDL/)).toBeInTheDocument();
expect(screen.queryByText(/Program Metadata IDL/)).not.toBeInTheDocument();

const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
const downloadButton = screen.getByRole('button', { name: 'Download' });
fireEvent.click(downloadButton);
const generateSdkButton = screen.getByRole('button', { name: 'Generate SDK' });
fireEvent.click(generateSdkButton);

expect(screen.getByText('Leaving Solana Explorer')).toBeInTheDocument();
expect(screen.getByText('You are now leaving Explorer and going to Castaway.')).toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: 'Continue' }));

expect(windowOpenSpy).toHaveBeenCalledTimes(1);
const [openedUrl, target, features] = windowOpenSpy.mock.calls[0]!;
const castawayUrl = new URL(openedUrl as string);
expect(castawayUrl.origin).toBe('https://www.castaway.lol');
expect(castawayUrl.pathname).toBe('/');
expect(castawayUrl.searchParams.get('program')).toBe(programId);
expect(castawayUrl.searchParams.get('idlSource')).toBe('anchor');
expect(castawayUrl.searchParams.get('network')).toBe('mainnet-beta');
expect(target).toBe('_blank');
expect(features).toBe('noopener,noreferrer');
windowOpenSpy.mockRestore();
});

test('should render IdlCard tabs when both IDLs exist', async () => {
Expand Down