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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Fill these out with a custom RPC url to test the explorer locally without getting rate-limited
## Configuration for RPC enpoints
### Fill these out with a custom RPC url to test the explorer locally without getting rate-limited
NEXT_PUBLIC_MAINNET_RPC_URL=
NEXT_PUBLIC_DEVNET_RPC_URL=
NEXT_PUBLIC_TESTNET_RPC_URL=
Expand All @@ -13,12 +14,15 @@ NEXT_PUBLIC_METADATA_USER_AGENT="Solana Explorer"
NEXT_PUBLIC_PMP_IDL_ENABLED=true
## Configuration for "security.txt" feature enabled
NEXT_PUBLIC_PMP_SECURITY_TXT_ENABLED=true
## Configuration for "interactive IDL" feature enabled
NEXT_PUBLIC_INTERACTIVE_IDL_ENABLED=true
NEXT_PUBLIC_BAD_TOKENS=
## Configuration for "View CNFT's Original Resource"
NEXT_PUBLIC_VIEW_ORIGINAL_DISPLAY_ENABLED=
## Configuration for Sentry
### Flag whether to catch exceptions with the boundary on the client or not
NEXT_PUBLIC_ENABLE_CATCH_EXCEPTIONS=1
### Flag that allows to not collect errors globally (allows to track for errors that we need at the moment
SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1
SENTRY_ORG=
SENTRY_DSN=
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
node-version: ${{ steps.nvmrc.outputs.NODE_VERSION }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
sparse-checkout: |
.nvmrc
Expand All @@ -43,7 +43,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Install package manager
uses: pnpm/action-setup@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-feature-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v4
Expand Down
8 changes: 7 additions & 1 deletion app/entities/cluster/model/use-explorer-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ export function useExplorerLink(path: string) {
// Add query parameters if any
const queryString = params.toString();
if (queryString) {
url += `?${queryString}`;
if (url.indexOf('?') === -1) {
url += `?${queryString}`;
} else {
// change order for additional params as having ?message at the url and placing it first, breaks input at the Inspector
const [path, qs] = url.split('?');
url = `${path}?${queryString}&${qs}`;
}
}

return { link: url };
Expand Down
3 changes: 2 additions & 1 deletion app/entities/idl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export type {
InstructionData,
NestedInstructionAccountsData,
} from './model/formatters/formatted-idl';
export { getIdlVersion } from './model/idl-version';
export { getIdlSpec, getIdlVersion, type AnchorIdl, type CodamaIdl, type SupportedIdl } from './model/idl-version';
export { isInteractiveIdlSupported } from './model/interactive-idl';

export { getIdlSpecType as getDisplayIdlSpecType } from './model/converters/convert-display-idl';
export { formatDisplayIdl, formatSerdeIdl, getFormattedIdl } from './model/formatters/format';
Expand Down
70 changes: 70 additions & 0 deletions app/entities/idl/model/__tests__/idl-version.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { getIdlVersion, type SupportedIdl } from '../idl-version';
import { isInteractiveIdlSupported } from '../interactive-idl';

const createMockIdl = (type: 'legacy' | 'anchor' | 'codama', specVersion?: string): SupportedIdl => {
if (type === 'codama') {
return {
standard: 'codama',
version: specVersion || '1.0.0',
} as unknown as SupportedIdl;
}

if (type === 'legacy') {
return {
instructions: [],
name: 'test',
version: '0.0.0',
} as unknown as SupportedIdl;
}

// Modern Anchor IDL with metadata.spec
return {
address: 'test',
instructions: [],
metadata: { name: 'test', spec: specVersion || '0.1.0', version: '0.1.0' },
} as unknown as SupportedIdl;
};

describe('getIdlVersion', () => {
it('should return "Legacy" for legacy IDL', () => {
const idl = createMockIdl('legacy');
expect(getIdlVersion(idl)).toBe('Legacy');
});

it('should return "0.30.1" for modern Anchor IDL', () => {
const idl = createMockIdl('anchor', '0.30.1');
expect(getIdlVersion(idl)).toBe('0.30.1');
});

it('should return codama version for Codama IDL', () => {
const idl = createMockIdl('codama', '1.2.3');
expect(getIdlVersion(idl)).toBe('1.2.3');
});
});

describe('isInteractiveIdlSupported', () => {
it('should return false for legacy IDL', () => {
const idl = createMockIdl('legacy');
expect(isInteractiveIdlSupported(idl)).toBe(false);
});

it('should return false for Codama IDL', () => {
const idl = createMockIdl('codama');
expect(isInteractiveIdlSupported(idl)).toBe(false);
});

it('should return false for Anchor with spec < 0.1.0', () => {
expect(isInteractiveIdlSupported(createMockIdl('anchor', '0.0.9'))).toBe(false);
});

it('should return true for Anchor with spec 0.1.0', () => {
const idl = createMockIdl('anchor', '0.1.0');
expect(isInteractiveIdlSupported(idl)).toBe(true);
});

it('should return true for Anchor with spec > 0.1.0', () => {
expect(isInteractiveIdlSupported(createMockIdl('anchor', '0.1.1'))).toBe(true);
expect(isInteractiveIdlSupported(createMockIdl('anchor', '0.2.0'))).toBe(true);
expect(isInteractiveIdlSupported(createMockIdl('anchor', '1.0.0'))).toBe(true);
});
});
29 changes: 27 additions & 2 deletions app/entities/idl/model/idl-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,40 @@ import type { RootNode } from 'codama';
import { getIdlSpecType as getSerdeIdlSpecType } from './converters/convert-legacy-idl';

export type IdlVersion = 'Legacy' | '0.30.1' | RootNode['version'];
export type CodamaIdl = RootNode;
export type AnchorIdl = Idl;
export type SupportedIdl = CodamaIdl | AnchorIdl;

export function getIdlVersion(idl: RootNode | Idl): IdlVersion {
/**
* Wildcard label used for all modern Anchor IDL versions (>= 0.30.1).
* This is a label representing the modern Anchor IDL standard, not a specific version.
*/
export const MODERN_ANCHOR_IDL_WILDCARD = '0.30.1';

/**
* Returns the IDL specification identifier.
*
* Note: '0.30.1' is used as a label for modern Anchor IDL specification (version >= 0.30.1).
* It represents the modern Anchor IDL specification, not a specific version number.
*/
export function getIdlVersion(idl: SupportedIdl): IdlVersion {
const spec = getSerdeIdlSpecType(idl);
switch (spec) {
case 'legacy':
return 'Legacy';
case 'codama':
return (idl as RootNode).version;
default:
return '0.30.1';
return MODERN_ANCHOR_IDL_WILDCARD;
}
}

/**
* Returns the IDL spec from metadata.spec for Anchor IDLs.
* Returns null for legacy or codama IDLs.
*/
export function getIdlSpec(idl: SupportedIdl): string | null {
const spec = getSerdeIdlSpecType(idl);
if (spec === 'legacy' || spec === 'codama') return null;
return spec;
}
26 changes: 26 additions & 0 deletions app/entities/idl/model/interactive-idl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getIdlSpecType as getSerdeIdlSpecType } from './converters/convert-legacy-idl';
import { getIdlVersion, MODERN_ANCHOR_IDL_WILDCARD, type SupportedIdl } from './idl-version';

/**
* Checks if the IDL version is supported for interactive features.
* Supports modern Anchor IDL with specVersion '0.30.1' and spec >= '0.1.0'
*/
export function isInteractiveIdlSupported(idl: SupportedIdl): boolean {
const specVersion = getIdlVersion(idl);

// Only modern Anchor IDL (specVersion '0.30.1') is supported
if (specVersion !== MODERN_ANCHOR_IDL_WILDCARD) return false;

// Check if spec is >= 0.1.0
const spec = getSerdeIdlSpecType(idl);
const match = spec.match(/^(\d+)\.(\d+)\.(\d+)/);
if (!match) return false;

const [, major, minor, patch] = match.map(Number);
// >= 0.1.0
if (major > 0) return true;
if (major === 0 && minor > 1) return true;
if (major === 0 && minor === 1 && patch >= 0) return true;

return false;
}
2 changes: 1 addition & 1 deletion app/features/idl/formatted-idl/lib/invariant.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function invariant(cond: any, message?: string): asserts cond is NonNullable<unknown> {
export function invariant(cond: unknown, message?: string): asserts cond is NonNullable<unknown> {
if (cond === undefined) throw new Error(message ?? 'invariant violated');
}
4 changes: 2 additions & 2 deletions app/features/idl/formatted-idl/ui/AnchorFormattedIdl.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Idl } from '@coral-xyz/anchor';
import type { AnchorIdl } from '@entities/idl';
import { formatDisplayIdl, getFormattedIdl, useFormatAnchorIdl } from '@entities/idl';

import { invariant } from '../lib/invariant';
import { useSearchIdl } from '../model/search';
import { BaseFormattedIdl } from './BaseFormattedIdl';
import type { StandardFormattedIdlProps } from './types';

export function AnchorFormattedIdl({ idl, programId, searchStr = '' }: StandardFormattedIdlProps<Idl>) {
export function AnchorFormattedIdl({ idl, programId, searchStr = '' }: StandardFormattedIdlProps<AnchorIdl>) {
invariant(idl, 'IDL is absent');
const formattedIdl = getFormattedIdl(formatDisplayIdl, idl, programId);
const anchorFormattedIdl = useFormatAnchorIdl(idl ? formattedIdl : idl);
Expand Down
14 changes: 9 additions & 5 deletions app/features/idl/formatted-idl/ui/BaseFormattedIdl.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
'use client';

import { cn } from '@components/shared/utils';
import type { Idl } from '@coral-xyz/anchor';
import { cn } from '@shared/utils';
import type { RootNode } from 'codama';
import { useEffect, useState } from 'react';

import { useTabs } from '../../model/use-tabs';
import { SearchHighlightProvider } from './SearchHighlightContext';
import { FormattedIdlViewProps } from './types';
import type { FormattedIdlViewProps } from './types';

export function BaseFormattedIdl({ idl, searchStr }: FormattedIdlViewProps<Idl> | FormattedIdlViewProps<RootNode>) {
export function BaseFormattedIdl({
idl,
originalIdl,
searchStr,
}: FormattedIdlViewProps<Idl> | FormattedIdlViewProps<RootNode>) {
const [activeTabIndex, setActiveTabIndex] = useState<number | null>(null);
const tabs = useTabs(idl, searchStr);
const tabs = useTabs(idl, originalIdl, searchStr);

useEffect(() => {
if (typeof activeTabIndex === 'number') return;
Expand Down Expand Up @@ -40,7 +44,7 @@ export function BaseFormattedIdl({ idl, searchStr }: FormattedIdlViewProps<Idl>
</button>
))}
</div>
<div className="table-responsive e-mb-0 e-min-h-96">
<div className={activeTab.id !== 'interact' ? 'e-overflow-x-auto md:e-overflow-x-scroll' : ''}>
<ActiveTab activeTab={activeTab} />
</div>
</div>
Expand Down
7 changes: 4 additions & 3 deletions app/features/idl/formatted-idl/ui/CodamaFormattedIdl.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { CodamaIdl } from '@entities/idl';
import { useFormatCodamaIdl } from '@entities/idl';
import type { RootNode } from 'codama';

import { invariant } from '../lib/invariant';
import { useSearchIdl } from '../model/search';
import { BaseFormattedIdl } from './BaseFormattedIdl';
import type { StandardFormattedIdlProps } from './types';

export function CodamaFormattedIdl({ idl, searchStr = '' }: { idl?: RootNode; searchStr?: string }) {
export function CodamaFormattedIdl({ idl, searchStr = '' }: StandardFormattedIdlProps<CodamaIdl>) {
invariant(idl, 'IDL is absent');
const formattedIdl = useFormatCodamaIdl(idl);
const searchResults = useSearchIdl(formattedIdl, searchStr);
return <BaseFormattedIdl idl={searchResults} searchStr={searchStr} />;
return <BaseFormattedIdl idl={searchResults} originalIdl={idl} searchStr={searchStr} />;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import codamaIdlMock from '@entities/idl/mocks/codama/codama-1.0.0-ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S.json';
import convertedFromAnchorIdlMock from '@entities/idl/mocks/codama/[email protected]';
import { Keypair } from '@solana/web3.js';
import type { Meta, StoryObj } from '@storybook/react';
import type { RootNode } from 'codama';

Expand Down Expand Up @@ -27,11 +28,13 @@ type Story = StoryObj<typeof meta>;
export const DisplayCodamaIdl: Story = {
args: {
idl: codamaIdlMock as unknown as RootNode,
programId: Keypair.generate().publicKey.toBase58(),
},
};

export const DisplayConvertedAnchorIdl: Story = {
args: {
idl: convertedFromAnchorIdlMock as unknown as RootNode,
programId: Keypair.generate().publicKey.toBase58(),
},
};
2 changes: 1 addition & 1 deletion app/features/idl/formatted-idl/ui/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type StandardFormattedIdlProps<T> = {

export type FormattedIdlViewProps<T> = {
idl: FormattedIdl | null;
originalIdl?: T;
originalIdl: T;
searchStr?: string;
};

Expand Down
Loading