├── .browserslistrc ├── .env.example ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yaml │ └── update-feature-gates.yml ├── .gitignore ├── .npmrc ├── .pnpmfile.cjs ├── .prettierignore ├── .prettierrc.cjs ├── .storybook ├── dashkit-polyfill.css ├── layout.min.css ├── main.ts ├── manager.ts ├── preview-head.html ├── preview.tsx └── vitest.setup.ts ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── @analytics │ └── default.js ├── __tests__ │ ├── mock-coingecko.ts │ ├── mock-parsed-extensions-stubs.ts │ ├── mock-stubs.ts │ └── mocks.ts ├── address │ └── [address] │ │ ├── anchor-account │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── anchor-program │ │ └── page.tsx │ │ ├── attributes │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── blockhashes │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── compression │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── concurrent-merkle-tree │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── domains │ │ └── page.tsx │ │ ├── entries │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── feature-gate │ │ └── page.tsx │ │ ├── instructions │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── metadata │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── nftoken-collection-nfts │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── program-multisig │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── rewards │ │ └── page.tsx │ │ ├── security │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── slot-hashes │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── stake-history │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── styles.css │ │ ├── token-extensions │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── tokens │ │ └── page.tsx │ │ ├── transfers │ │ └── page.tsx │ │ ├── verified-build │ │ ├── page-client.tsx │ │ └── page.tsx │ │ └── vote-history │ │ ├── page-client.tsx │ │ └── page.tsx ├── api │ ├── anchor │ │ └── route.ts │ ├── codama │ │ └── route.ts │ ├── domain-info │ │ └── [domain] │ │ │ └── route.ts │ ├── metadata │ │ └── proxy │ │ │ ├── __tests__ │ │ │ ├── endpoint.test.ts │ │ │ ├── fetch-resource.spec.ts │ │ │ └── ip.spec.ts │ │ │ ├── feature │ │ │ ├── errors.ts │ │ │ ├── index.ts │ │ │ ├── ip.ts │ │ │ └── processors.ts │ │ │ └── route.ts │ └── ping │ │ └── [network] │ │ └── route.ts ├── block │ └── [slot] │ │ ├── accounts │ │ ├── page-client.tsx │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page-client.tsx │ │ ├── page.tsx │ │ ├── programs │ │ ├── page-client.tsx │ │ └── page.tsx │ │ └── rewards │ │ ├── page-client.tsx │ │ └── page.tsx ├── components │ ├── ClusterModal.tsx │ ├── ClusterModalDeveloperSettings.tsx │ ├── ClusterStatusButton.tsx │ ├── DeveloperResources.tsx │ ├── Header.tsx │ ├── LiveTransactionStatsCard.tsx │ ├── MessageBanner.tsx │ ├── Navbar.tsx │ ├── ProgramLogsCardBody.tsx │ ├── SearchBar.tsx │ ├── StatsNotReady.tsx │ ├── SupplyCard.tsx │ ├── TopAccountsCard.tsx │ ├── account │ │ ├── AccountHeader.tsx │ │ ├── AnchorAccountCard.tsx │ │ ├── AnchorProgramCard.tsx │ │ ├── BlockhashesCard.tsx │ │ ├── CompressedNFTInfoCard.tsx │ │ ├── CompressedNftCard.tsx │ │ ├── ConcurrentMerkleTreeCard.tsx │ │ ├── ConfigAccountSection.tsx │ │ ├── DomainsCard.tsx │ │ ├── FeatureAccountSection.tsx │ │ ├── FeatureGateCard.tsx │ │ ├── HistoryCardComponents.tsx │ │ ├── MetaplexMetadataCard.tsx │ │ ├── MetaplexNFTAttributesCard.tsx │ │ ├── MetaplexNFTHeader.tsx │ │ ├── NonceAccountSection.tsx │ │ ├── OwnedTokensCard.tsx │ │ ├── ParsedAccountRenderer.tsx │ │ ├── ProgramMultisigCard.tsx │ │ ├── RewardsCard.tsx │ │ ├── SecurityCard.tsx │ │ ├── SlotHashesCard.tsx │ │ ├── StakeAccountSection.tsx │ │ ├── StakeHistoryCard.tsx │ │ ├── SysvarAccountSection.tsx │ │ ├── TokenAccountSection.tsx │ │ ├── TokenExtensionsCard.tsx │ │ ├── TokenExtensionsSection.tsx │ │ ├── TokenHistoryCard.tsx │ │ ├── UnknownAccountCard.tsx │ │ ├── UpgradeableLoaderAccountSection.tsx │ │ ├── VerifiedBuildCard.tsx │ │ ├── VoteAccountSection.tsx │ │ ├── VotesCard.tsx │ │ ├── __test__ │ │ │ └── TokenExtensionRow.spec.tsx │ │ ├── address-lookup-table │ │ │ ├── AddressLookupTableAccountSection.tsx │ │ │ ├── LookupTableEntriesCard.tsx │ │ │ └── types.ts │ │ ├── history │ │ │ ├── TokenInstructionsCard.tsx │ │ │ ├── TokenTransfersCard.tsx │ │ │ ├── TransactionHistoryCard.tsx │ │ │ └── common.tsx │ │ ├── nftoken │ │ │ ├── NFTokenAccountHeader.tsx │ │ │ ├── NFTokenAccountSection.tsx │ │ │ ├── NFTokenCollectionNFTGrid.tsx │ │ │ ├── README.md │ │ │ ├── __tests__ │ │ │ │ └── isNFTokenAccount-test.ts │ │ │ ├── isNFTokenAccount.ts │ │ │ ├── nftoken-hooks.tsx │ │ │ ├── nftoken-types.ts │ │ │ └── nftoken.ts │ │ ├── token-extensions │ │ │ ├── TokenExtensionsStatusRow.tsx │ │ │ └── stories │ │ │ │ └── TokenExtensionsStatusRow.stories.tsx │ │ └── types.ts │ ├── block │ │ ├── BlockAccountsCard.tsx │ │ ├── BlockHistoryCard.tsx │ │ ├── BlockProgramsCard.tsx │ │ └── BlockRewardsCard.tsx │ ├── common │ │ ├── Account.tsx │ │ ├── Address.tsx │ │ ├── BalanceDelta.tsx │ │ ├── BaseInstructionCard.tsx │ │ ├── BaseRawDetails.tsx │ │ ├── BaseRawParsedDetails.tsx │ │ ├── Copyable.tsx │ │ ├── Downloadable.tsx │ │ ├── Epoch.tsx │ │ ├── ErrorCard.tsx │ │ ├── HexData.tsx │ │ ├── IDLBadge.tsx │ │ ├── Identicon.tsx │ │ ├── InfoTooltip.tsx │ │ ├── InspectorInstructionCard.tsx │ │ ├── InstructionDetails.tsx │ │ ├── LoadingArtPlaceholder.tsx │ │ ├── LoadingCard.tsx │ │ ├── NFTArt.tsx │ │ ├── Overlay.tsx │ │ ├── SecurityTXTBadge.tsx │ │ ├── Signature.tsx │ │ ├── Slot.tsx │ │ ├── SolBalance.tsx │ │ ├── TableCardBody.tsx │ │ ├── TimestampToggle.tsx │ │ ├── TokenExtensionBadge.tsx │ │ ├── TokenExtensionBadges.tsx │ │ ├── TokenMarketData.tsx │ │ ├── VerifiedBadge.tsx │ │ ├── VerifiedProgramBadge.tsx │ │ ├── __tests__ │ │ │ ├── BaseInstructionCard.spec.tsx │ │ │ └── VerifiedProgramBadge.spec.tsx │ │ ├── inspector │ │ │ ├── AddressTableLookupAddress.tsx │ │ │ └── UnknownDetailsCard.tsx │ │ ├── stories │ │ │ ├── TokenExtensionBadge.stories.tsx │ │ │ ├── TokenExtensionBadges.stories.tsx │ │ │ └── TokenMarketData.stories.tsx │ │ └── token-market-data │ │ │ ├── MarketData.tsx │ │ │ └── stories │ │ │ ├── MarketData.s.tsx │ │ │ └── MarketDataSeries.s.tsx │ ├── inspector │ │ ├── AccountsCard.tsx │ │ ├── AddressTableLookupsCard.tsx │ │ ├── AddressWithContext.tsx │ │ ├── InspectorPage.tsx │ │ ├── InstructionsSection.tsx │ │ ├── RawInputCard.tsx │ │ ├── SignaturesCard.tsx │ │ ├── SimulatorCard.tsx │ │ ├── UnknownDetailsCard.tsx │ │ ├── __tests__ │ │ │ ├── AssociatedTokenDetailsCard.spec.tsx │ │ │ ├── InspectorPage.spec.tsx │ │ │ └── into-parsed-data.spec.ts │ │ ├── associated-token │ │ │ ├── AssociatedTokenDetailsCard.tsx │ │ │ ├── CreateDetailsCard.tsx │ │ │ ├── CreateIdempotentDetailsCard.tsx │ │ │ └── RecoverNestedDetailsCard.tsx │ │ ├── into-parsed-data.ts │ │ └── utils.ts │ ├── instruction │ │ ├── AddressLookupTableDetailsCard.tsx │ │ ├── AnchorDetailsCard.tsx │ │ ├── ComputeBudgetDetailsCard.tsx │ │ ├── InstructionCard.tsx │ │ ├── MangoDetails.tsx │ │ ├── MemoDetailsCard.tsx │ │ ├── SerumDetailsCard.tsx │ │ ├── SignatureContext.tsx │ │ ├── TokenLendingDetailsCard.tsx │ │ ├── TokenSwapDetailsCard.tsx │ │ ├── UnknownDetailsCard.tsx │ │ ├── WormholeDetailsCard.tsx │ │ ├── __tests__ │ │ │ ├── AssociatedTokenDetailsCard.spec.tsx │ │ │ └── ComputeBudgetDetailsCard.spec.tsx │ │ ├── address-lookup-table │ │ │ ├── CloseLookupTableDetails.tsx │ │ │ ├── CreateLookupTableDetails.tsx │ │ │ ├── DeactivateLookupTableDetails.tsx │ │ │ ├── ExtendLookupTableDetails.tsx │ │ │ ├── FreezeLookupTableDetails.tsx │ │ │ └── types.ts │ │ ├── associated-token │ │ │ ├── AssociatedTokenDetailsCard.tsx │ │ │ ├── CreateDetailsCard.tsx │ │ │ ├── CreateIdempotentDetailsCard.tsx │ │ │ ├── RecoverNestedDetailsCard.tsx │ │ │ └── types.ts │ │ ├── bpf-loader │ │ │ ├── BpfLoaderDetailsCard.tsx │ │ │ └── types.ts │ │ ├── bpf-upgradeable-loader │ │ │ ├── BpfUpgradeableLoaderDetailsCard.tsx │ │ │ └── types.ts │ │ ├── codama │ │ │ ├── CodamaInstructionDetailsCard.tsx │ │ │ ├── codamaUtils.tsx │ │ │ └── getCodamaIdl.ts │ │ ├── ed25519 │ │ │ ├── Ed25519DetailsCard.tsx │ │ │ ├── __tests__ │ │ │ │ └── Ed25519DetailsCard.test.tsx │ │ │ └── types.ts │ │ ├── lighthouse │ │ │ ├── LighthouseDetailsCard.tsx │ │ │ ├── __tests__ │ │ │ │ └── LighthouseDetailsCard.test.tsx │ │ │ └── types.ts │ │ ├── mango │ │ │ ├── AddOracleDetailsCard.tsx │ │ │ ├── AddPerpMarketDetailsCard.tsx │ │ │ ├── AddSpotMarketDetailsCard.tsx │ │ │ ├── CancelPerpOrderDetailsCard.tsx │ │ │ ├── CancelSpotOrderDetailsCard.tsx │ │ │ ├── ChangePerpMarketParamsDetailsCard.tsx │ │ │ ├── ConsumeEventsDetailsCard.tsx │ │ │ ├── GenericMngoAccountDetailsCard.tsx │ │ │ ├── GenericPerpMngoDetailsCard.tsx │ │ │ ├── GenericSpotMngoDetailsCard.tsx │ │ │ ├── PlacePerpOrder2DetailsCard.tsx │ │ │ ├── PlacePerpOrderDetailsCard.tsx │ │ │ ├── PlaceSpotOrderDetailsCard.tsx │ │ │ └── types.ts │ │ ├── pyth │ │ │ ├── AddMappingDetailsCard.tsx │ │ │ ├── AddPriceDetailsCard.tsx │ │ │ ├── AddProductDetailsCard.tsx │ │ │ ├── AggregatePriceDetailsCard.tsx │ │ │ ├── BasePublisherOperationCard.tsx │ │ │ ├── InitMappingDetailsCard.tsx │ │ │ ├── InitPriceDetailsCard.tsx │ │ │ ├── PythDetailsCard.tsx │ │ │ ├── SetMinPublishersDetailsCard.tsx │ │ │ ├── UpdatePriceDetailsCard.tsx │ │ │ ├── UpdateProductDetailsCard.tsx │ │ │ ├── program.ts │ │ │ └── types.ts │ │ ├── serum │ │ │ ├── CancelOrderByClientIdDetails.tsx │ │ │ ├── CancelOrderByClientIdV2Details.tsx │ │ │ ├── CancelOrderDetails.tsx │ │ │ ├── CancelOrderV2Details.tsx │ │ │ ├── CloseOpenOrdersDetails.tsx │ │ │ ├── ConsumeEventsDetails.tsx │ │ │ ├── ConsumeEventsPermissionedDetails.tsx │ │ │ ├── DisableMarketDetails.tsx │ │ │ ├── InitOpenOrdersDetails.tsx │ │ │ ├── InitializeMarketDetailsCard.tsx │ │ │ ├── MatchOrdersDetailsCard.tsx │ │ │ ├── NewOrderDetailsCard.tsx │ │ │ ├── NewOrderV3DetailsCard.tsx │ │ │ ├── PruneDetails.tsx │ │ │ ├── SettleFundsDetailsCard.tsx │ │ │ ├── SweepFeesDetails.tsx │ │ │ └── types.ts │ │ ├── stake │ │ │ ├── AuthorizeDetailsCard.tsx │ │ │ ├── DeactivateDetailsCard.tsx │ │ │ ├── DelegateDetailsCard.tsx │ │ │ ├── InitializeDetailsCard.tsx │ │ │ ├── MergeDetailsCard.tsx │ │ │ ├── SplitDetailsCard.tsx │ │ │ ├── StakeDetailsCard.tsx │ │ │ ├── WithdrawDetailsCard.tsx │ │ │ └── types.ts │ │ ├── system │ │ │ ├── AllocateDetailsCard.tsx │ │ │ ├── AllocateWithSeedDetailsCard.tsx │ │ │ ├── AssignDetailsCard.tsx │ │ │ ├── AssignWithSeedDetailsCard.tsx │ │ │ ├── CreateDetailsCard.tsx │ │ │ ├── CreateWithSeedDetailsCard.tsx │ │ │ ├── NonceAdvanceDetailsCard.tsx │ │ │ ├── NonceAuthorizeDetailsCard.tsx │ │ │ ├── NonceInitializeDetailsCard.tsx │ │ │ ├── NonceWithdrawDetailsCard.tsx │ │ │ ├── SystemDetailsCard.tsx │ │ │ ├── TransferDetailsCard.tsx │ │ │ ├── TransferWithSeedDetailsCard.tsx │ │ │ ├── UpgradeNonceDetailsCard.tsx │ │ │ └── types.ts │ │ ├── token-lending │ │ │ └── types.ts │ │ ├── token-swap │ │ │ └── types.ts │ │ ├── token │ │ │ ├── TokenDetailsCard.tsx │ │ │ └── types.ts │ │ ├── vote │ │ │ ├── VoteDetailsCard.tsx │ │ │ └── types.ts │ │ └── wormhole │ │ │ └── types.ts │ ├── shared │ │ ├── ErrorCard.tsx │ │ ├── LoadingCard.tsx │ │ ├── StatusBadge.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── badge.tsx │ │ │ └── tooltip.tsx │ │ └── utils.ts │ └── transaction │ │ ├── InstructionsSection.tsx │ │ ├── ProgramLogSection.tsx │ │ └── TokenBalancesCard.tsx ├── epoch │ └── [epoch] │ │ ├── layout.tsx │ │ ├── page-client.tsx │ │ └── page.tsx ├── feature-gates │ ├── page-client.tsx │ └── page.tsx ├── features │ ├── feature-gate │ │ ├── __tests__ │ │ │ └── feature-gate.spec.ts │ │ └── index.ts │ ├── metadata │ │ ├── __tests__ │ │ │ └── utils.spec.ts │ │ └── utils.ts │ └── token-extensions │ │ └── use-token-extension-navigation.ts ├── globals.css ├── img │ └── logos-solana │ │ ├── dark-explorer-logo.svg │ │ ├── dark-solana-logo.svg │ │ └── light-explorer-logo.svg ├── layout.tsx ├── not-found.tsx ├── opengraph-image.png ├── page.tsx ├── providers │ ├── accounts │ │ ├── flagged-accounts.tsx │ │ ├── history.tsx │ │ ├── index.tsx │ │ ├── rewards.tsx │ │ ├── tokens.tsx │ │ ├── utils │ │ │ ├── getEditionInfo.ts │ │ │ ├── isMetaplexNFT.ts │ │ │ └── stake.ts │ │ └── vote-accounts.tsx │ ├── anchor.tsx │ ├── block.tsx │ ├── cache.tsx │ ├── cluster.tsx │ ├── compressed-nft.tsx │ ├── epoch.tsx │ ├── richList.tsx │ ├── scroll-anchor.tsx │ ├── squadsMultisig.tsx │ ├── stats │ │ ├── index.tsx │ │ ├── solanaClusterStats.tsx │ │ ├── solanaDashboardInfo.tsx │ │ └── solanaPerformanceInfo.tsx │ ├── supply.tsx │ ├── transactions │ │ ├── index.tsx │ │ ├── parsed.tsx │ │ └── raw.tsx │ └── useCodamaIdl.tsx ├── scss │ ├── _solana-dark-overrides.scss │ ├── _solana-variables-dark.scss │ ├── _solana-variables.scss │ ├── _solana.scss │ ├── dashkit │ │ ├── _alert.scss │ │ ├── _avatar.scss │ │ ├── _badge.scss │ │ ├── _breadcrumb.scss │ │ ├── _buttons.scss │ │ ├── _card.scss │ │ ├── _chart.scss │ │ ├── _checklist.scss │ │ ├── _close.scss │ │ ├── _comment.scss │ │ ├── _dropdown.scss │ │ ├── _forms.scss │ │ ├── _header.scss │ │ ├── _icon.scss │ │ ├── _kanban.scss │ │ ├── _list-group.scss │ │ ├── _main-content.scss │ │ ├── _mixins.scss │ │ ├── _modal.scss │ │ ├── _nav.scss │ │ ├── _navbar.scss │ │ ├── _offcanvas.scss │ │ ├── _pagination.scss │ │ ├── _popover.scss │ │ ├── _progress.scss │ │ ├── _reboot.scss │ │ ├── _root.scss │ │ ├── _tables.scss │ │ ├── _theme.scss │ │ ├── _type.scss │ │ ├── _utilities.scss │ │ ├── _variables.scss │ │ ├── _vendor.scss │ │ ├── dark │ │ │ ├── _overrides-dark.scss │ │ │ └── _variables-dark.scss │ │ ├── forms │ │ │ ├── _form-check.scss │ │ │ ├── _form-control.scss │ │ │ ├── _form-group.scss │ │ │ ├── _form-text.scss │ │ │ ├── _input-group.scss │ │ │ └── _validation.scss │ │ ├── mixins │ │ │ ├── _badge.scss │ │ │ └── _breakpoints.scss │ │ ├── utilities │ │ │ ├── _background.scss │ │ │ └── _lift.scss │ │ └── vendor │ │ │ ├── _choices.scss │ │ │ ├── _dropzone.scss │ │ │ ├── _flatpickr.scss │ │ │ ├── _highlight.scss │ │ │ ├── _list.scss │ │ │ └── _quill.scss │ ├── theme-dark.scss │ └── theme.scss ├── styles.css ├── supply │ ├── layout.tsx │ ├── page-client.tsx │ └── page.tsx ├── tx │ ├── (inspector) │ │ ├── [signature] │ │ │ └── inspect │ │ │ │ └── page.tsx │ │ ├── inspector │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── [signature] │ │ ├── page-client.tsx │ │ └── page.tsx │ └── layout.tsx ├── types │ └── react-json-view.d.ts ├── utils │ ├── __tests__ │ │ ├── epoch-schedule.ts │ │ ├── lamportsToSol-test.ts │ │ ├── math-test.ts │ │ ├── parseFeatureAccount-test.ts │ │ └── use-tab-visibility.test.tsx │ ├── anchor.tsx │ ├── ans-domains.tsx │ ├── cluster.ts │ ├── coingecko.tsx │ ├── convertLegacyIdl.ts │ ├── date.ts │ ├── domain-info.ts │ ├── epoch-schedule.ts │ ├── feature-gate │ │ ├── UpcomingFeatures.tsx │ │ ├── featureGates.json │ │ ├── types.ts │ │ └── utils.ts │ ├── get-instruction-card-scroll-anchor-id.ts │ ├── get-readable-title-from-address.ts │ ├── index.ts │ ├── instruction.ts │ ├── local-storage.ts │ ├── logger.ts │ ├── math.ts │ ├── name-service.tsx │ ├── parseFeatureAccount.ts │ ├── program-err.ts │ ├── program-logs.ts │ ├── program-verification.tsx │ ├── programs.ts │ ├── security-txt.ts │ ├── serumMarketRegistry.ts │ ├── token-extension.ts │ ├── token-info.ts │ ├── token-search.ts │ ├── tx.ts │ ├── types │ │ └── elfy.d.ts │ ├── url.ts │ ├── use-debounce-async.ts │ ├── use-tab-visibility.ts │ └── verified-builds.tsx └── validators │ ├── accounts │ ├── address-lookup-table.ts │ ├── config.ts │ ├── nonce.ts │ ├── stake.ts │ ├── sysvar.ts │ ├── token-extension.ts │ ├── token.ts │ ├── upgradeable-program.ts │ └── vote.ts │ ├── index.ts │ ├── number.ts │ └── pubkey.ts ├── cache └── config.json ├── components.json ├── docs └── development.md ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── apple-touch-icon.png ├── favicon.png ├── favicon.svg ├── icon-192.png ├── icon-512.png ├── img │ └── dashkit │ │ └── masks │ │ ├── avatar-group-hover-last.svg │ │ ├── avatar-group-hover.svg │ │ ├── avatar-group.svg │ │ ├── avatar-status.svg │ │ └── icon-status.svg └── manifest.json ├── scripts ├── fetch_mainnet_activations.py └── parse_feature_gates.py ├── tailwind.config.ts ├── test-setup.ts ├── tsconfig.json ├── vite.config.mts └── vitest.workspace.ts /.browserslistrc: -------------------------------------------------------------------------------- 1 | [production] 2 | >0.2% and supports bigint and not dead 3 | not op_mini all 4 | 5 | [development] 6 | last 1 chrome version 7 | last 1 firefox version 8 | last 1 safari version 9 | supports bigint 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Fill these out with a custom RPC url to test the explorer locally without getting rate-limited 2 | NEXT_PUBLIC_MAINNET_RPC_URL= 3 | NEXT_PUBLIC_DEVNET_RPC_URL= 4 | NEXT_PUBLIC_TESTNET_RPC_URL= 5 | # Configuration for "metadata" service. set "ENABLED" to true to use it 6 | NEXT_PUBLIC_METADATA_ENABLED=false 7 | NEXT_PUBLIC_METADATA_TIMEOUT= 8 | NEXT_PUBLIC_METADATA_MAX_CONTENT_SIZE= 9 | NEXT_PUBLIC_METADATA_USER_AGENT="Solana Explorer" 10 | NEXT_LOG_LEVEL=0 11 | NEXT_PUBLIC_PMP_IDL_ENABLED=true -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "@solana/eslint-config-solana"], 3 | "plugins": ["testing-library"], 4 | "rules": { 5 | "semi": ["error", "always"], 6 | // Temporary rule to ignore any explicit any type warnings in TypeScript files 7 | "@typescript-eslint/no-explicit-any": "off", 8 | "object-curly-spacing": ["error", "always"], 9 | // Do not rely on non-null assertions, please. Make it off to see other errors. 10 | "@typescript-eslint/no-non-null-assertion": "off" 11 | }, 12 | "overrides": [ 13 | // Only uses Testing Library lint rules in test files 14 | { 15 | "files": ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"], 16 | "extends": ["plugin:testing-library/react"] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[Bug]' 5 | labels: bug 6 | assignees: ngundotra 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Example Links** 21 | 22 | - [Please provide at least 1 link here](https://explorer.solana.com) 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Type of change 6 | 7 | 8 | 9 | - [ ] Bug fix 10 | - [ ] New feature 11 | - [ ] Protocol integration 12 | - [ ] Documentation update 13 | - [ ] Other (please describe): 14 | 15 | ## Screenshots 16 | 17 | 18 | 19 | 20 | ## Testing 21 | 22 | 23 | 24 | 25 | ## Related Issues 26 | 27 | 28 | 29 | 30 | ## Checklist 31 | 32 | 33 | 34 | - [ ] My code follows the project's style guidelines 35 | - [ ] I have added tests that prove my fix/feature works 36 | - [ ] All tests pass locally and in CI 37 | - [ ] I have updated documentation as needed 38 | - [ ] CI/CD checks pass 39 | - [ ] I have included screenshots for protocol screens (if applicable) 40 | - [ ] For security-related features, I have included links to related information 41 | 42 | ## Additional Notes 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/update-feature-gates.yml: -------------------------------------------------------------------------------- 1 | name: Update Feature Gates 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '23 3 * * *' # Daily at 3:23 am UTC 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | update: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install requests beautifulsoup4 solders solana pydantic 25 | 26 | - name: Scrape and generate features 27 | run: python scripts/parse_feature_gates.py 28 | 29 | - name: Fetch mainnet activations 30 | run: python scripts/fetch_mainnet_activations.py 31 | 32 | - name: Create Pull Request 33 | uses: peter-evans/create-pull-request@v7 34 | with: 35 | branch-token: ${{ secrets.GITHUB_TOKEN }} 36 | sign-commits: true 37 | commit-message: 'Update feature gates from Agave wiki' 38 | title: 'Automated Feature Gates Update' 39 | body: 'Auto-generated PR from daily feature gates check' 40 | branch: 'feature-gates-update' 41 | base: master 42 | maintainer-can-modify: true 43 | delete-branch: true 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # Speedy Web Compiler 38 | .swc/ 39 | 40 | # vim 41 | .editorconfig 42 | *storybook.log 43 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | .vscode 3 | cache/ 4 | coverage/ 5 | node_modules/ 6 | dist/ 7 | build/ 8 | .pnpm-store/ 9 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | const prettierConfigSolana = require('@solana/prettier-config-solana'); 2 | 3 | /** @type {import("prettier").Config} */ 4 | module.exports = { 5 | ...prettierConfigSolana, 6 | plugins: [prettierConfigSolana.plugins ?? []].concat(['prettier-plugin-tailwindcss']), 7 | endOfLine: 'lf', 8 | overrides: [ 9 | ...(prettierConfigSolana.overrides ?? []), 10 | { 11 | files: '*.{ts,tsx,mts,mjs}', 12 | options: { 13 | parser: 'typescript', 14 | }, 15 | }, 16 | { 17 | files: '*.{json,md}', 18 | options: { 19 | singleQuote: false, 20 | }, 21 | }, 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /.storybook/dashkit-polyfill.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bs-body-bg-rgb: 22, 27, 25; 3 | --bs-body-bg: #161a19; 4 | --bs-body-color-rgb: 255, 255, 255; 5 | --bs-body-color: #fff; 6 | --bs-body-font-family: var(--explorer-default-font); 7 | --bs-body-font-size: 0.9375rem; 8 | --bs-body-font-weight: 400; 9 | --bs-body-line-height: 1.5; 10 | } 11 | 12 | body { 13 | background-color: var(--bs-body-bg); 14 | color: var(--bs-body-color); 15 | font-family: var(--bs-body-font-family); 16 | font-size: var(--bs-body-font-size); 17 | font-weight: var(--bs-body-font-weight); 18 | line-height: var(--bs-body-line-height); 19 | } 20 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/experimental-nextjs-vite'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../app/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | addons: ['@storybook/addon-essentials', '@storybook/experimental-addon-test'], 6 | framework: { 7 | name: '@storybook/experimental-nextjs-vite', 8 | options: {}, 9 | }, 10 | staticDirs: ['../public'], 11 | }; 12 | export default config; 13 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/manager-api'; 2 | import { themes } from '@storybook/theming'; 3 | 4 | addons.setConfig({ 5 | theme: themes.light, 6 | }); 7 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | import React, { useEffect } from 'react'; 3 | 4 | import { Rubik } from 'next/font/google'; 5 | import './layout.min.css'; // uncomment this line to see Dashkit styles. TODO: remove upon migrating from Dashkit to Tailwind 6 | import './dashkit-polyfill.css'; 7 | import '@/app/styles.css'; 8 | 9 | // Load font with display: swap for better loading behavior 10 | const rubikFont = Rubik({ 11 | display: 'swap', 12 | preload: true, 13 | subsets: ['latin'], 14 | variable: '--explorer-default-font', 15 | weight: ['300', '400', '700'], 16 | }); 17 | 18 | const preview: Preview = { 19 | parameters: { 20 | backgrounds: { 21 | values: [{ name: 'Dark', value: '#161a19' }], 22 | default: 'Dark', 23 | }, 24 | controls: { 25 | matchers: { 26 | color: /(background|color)$/i, 27 | date: /Date$/i, 28 | }, 29 | }, 30 | }, 31 | decorators: [ 32 | Story => { 33 | // Add useEffect to ensure font is properly loaded 34 | useEffect(() => { 35 | document.getElementById('storybook-outer')?.classList.add(rubikFont.className); 36 | }, [rubikFont]); 37 | 38 | return ( 39 |
40 | 41 |
42 | ); 43 | }, 44 | ], 45 | }; 46 | 47 | export default preview; 48 | -------------------------------------------------------------------------------- /.storybook/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll } from 'vitest'; 2 | import { setProjectAnnotations } from '@storybook/experimental-nextjs-vite'; 3 | import * as projectAnnotations from './preview'; 4 | 5 | // This is an important step to apply the right configuration when testing your stories. 6 | // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations 7 | const project = setProjectAnnotations([projectAnnotations]); 8 | 9 | beforeAll(project.beforeAll); 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Solana Labs, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/@analytics/default.js: -------------------------------------------------------------------------------- 1 | import Script from 'next/script'; 2 | 3 | export default function Analytics() { 4 | const safeAnalyticsId = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID?.replace("'", "\\'"); 5 | if (!safeAnalyticsId) { 6 | return null; 7 | } 8 | return ( 9 | <> 10 | {/* Global site tag (gtag.js) - Google Analytics */} 11 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/__tests__/mock-parsed-extensions-stubs.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | import { TokenExtension } from '../validators/accounts/token-extension'; 4 | 5 | export const transferFeeConfig0 = { 6 | extension: 'transferFeeConfig', 7 | state: { 8 | newerTransferFee: { 9 | epoch: 200, 10 | maximumFee: 2000000, 11 | transferFeeBasisPoints: 200, 12 | }, 13 | olderTransferFee: { 14 | epoch: 100, 15 | maximumFee: 1000000, 16 | transferFeeBasisPoints: 100, 17 | }, 18 | transferFeeConfigAuthority: new PublicKey('2apBGMsS6ti9RyF5TwQTDswXBWskiJP2LD4cUEDqYJjk'), 19 | withdrawWithheldAuthority: new PublicKey('3apBGMsS6ti9RyF5TwQTDswXBWskiJP2LD4cUEDqYJjk'), 20 | withheldAmount: 500000, 21 | }, 22 | } as TokenExtension; 23 | -------------------------------------------------------------------------------- /app/address/[address]/anchor-account/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AnchorAccountCard } from '@components/account/AnchorAccountCard'; 4 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 5 | import { LoadingCard } from '@components/common/LoadingCard'; 6 | import { Suspense } from 'react'; 7 | import React from 'react'; 8 | 9 | type Props = Readonly<{ 10 | params: { 11 | address: string; 12 | }; 13 | }>; 14 | 15 | function AnchorAccountCardRenderer({ 16 | account, 17 | onNotFound, 18 | }: React.ComponentProps['renderComponent']>) { 19 | if (!account) { 20 | return onNotFound(); 21 | } 22 | return ( 23 | }> 24 | 25 | 26 | ); 27 | } 28 | 29 | export default function AnchorAccountPageClient({ params: { address } }: Props) { 30 | return ; 31 | } 32 | -------------------------------------------------------------------------------- /app/address/[address]/anchor-account/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import AnchorAccountPageClient from './page-client'; 5 | 6 | type Props = Readonly<{ 7 | params: { 8 | address: string; 9 | }; 10 | }>; 11 | 12 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 13 | return { 14 | description: `Contents of the Anchor Account at address ${props.params.address} on Solana`, 15 | title: `Anchor Account Data | ${await getReadableTitleFromAddress(props)} | Solana`, 16 | }; 17 | } 18 | 19 | export default function AnchorAccountPage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/address/[address]/anchor-program/page.tsx: -------------------------------------------------------------------------------- 1 | import { AnchorProgramCard } from '@components/account/AnchorProgramCard'; 2 | import { LoadingCard } from '@components/common/LoadingCard'; 3 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 4 | import { Metadata } from 'next/types'; 5 | import { Suspense } from 'react'; 6 | 7 | type Props = Readonly<{ 8 | params: { 9 | address: string; 10 | }; 11 | }>; 12 | 13 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 14 | return { 15 | description: `The Interface Definition Language (IDL) file for the Anchor program at address ${props.params.address} on Solana`, 16 | title: `Anchor Program IDL | ${await getReadableTitleFromAddress(props)} | Solana`, 17 | }; 18 | } 19 | 20 | export default function AnchorProgramIDLPage({ params: { address } }: Props) { 21 | return ( 22 | }> 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/address/[address]/attributes/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MetaplexNFTAttributesCard } from '@components/account/MetaplexNFTAttributesCard'; 4 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 5 | import React, { Suspense } from 'react'; 6 | 7 | import { LoadingCard } from '@/app/components/common/LoadingCard'; 8 | 9 | type Props = Readonly<{ 10 | params: { 11 | address: string; 12 | }; 13 | }>; 14 | 15 | function MetaplexNFTAttributesCardRenderer({ 16 | account, 17 | onNotFound, 18 | }: React.ComponentProps['renderComponent']>) { 19 | return ( 20 | }> 21 | {} 22 | 23 | ); 24 | } 25 | 26 | export default function MetaplexNFTAttributesPageClient({ params: { address } }: Props) { 27 | return ; 28 | } 29 | -------------------------------------------------------------------------------- /app/address/[address]/attributes/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import NFTAttributesPageClient from './page-client'; 5 | 6 | type Props = Readonly<{ 7 | params: { 8 | address: string; 9 | }; 10 | }>; 11 | 12 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 13 | return { 14 | description: `Attributes of the Metaplex NFT with address ${props.params.address} on Solana`, 15 | title: `Metaplex NFT Attributes | ${await getReadableTitleFromAddress(props)} | Solana`, 16 | }; 17 | } 18 | 19 | export default function MetaplexNFTAttributesPage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/address/[address]/blockhashes/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { BlockhashesCard } from '@components/account/BlockhashesCard'; 4 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 5 | import React from 'react'; 6 | 7 | type Props = Readonly<{ 8 | params: { 9 | address: string; 10 | }; 11 | }>; 12 | 13 | function BlockhashesCardRenderer({ 14 | account, 15 | onNotFound, 16 | }: React.ComponentProps['renderComponent']>) { 17 | const parsedData = account?.data?.parsed; 18 | if (!parsedData || parsedData.program !== 'sysvar' || parsedData.parsed.type !== 'recentBlockhashes') { 19 | return onNotFound(); 20 | } 21 | return ; 22 | } 23 | 24 | export default function RecentBlockhashesPageClient({ params: { address } }: Props) { 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /app/address/[address]/blockhashes/page.tsx: -------------------------------------------------------------------------------- 1 | import RecentBlockhashesPageClient from './page-client'; 2 | 3 | type Props = Readonly<{ 4 | params: { 5 | address: string; 6 | }; 7 | }>; 8 | 9 | export const metadata = { 10 | description: `Recent blockhashes on Solana`, 11 | title: `Recent Blockhashes | Solana`, 12 | }; 13 | 14 | export default function RecentBlockhashesPage(props: Props) { 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /app/address/[address]/compression/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 4 | import React, { Suspense } from 'react'; 5 | import { ErrorBoundary } from 'react-error-boundary'; 6 | 7 | import { CompressedNFTInfoCard } from '@/app/components/account/CompressedNFTInfoCard'; 8 | import { ErrorCard } from '@/app/components/common/ErrorCard'; 9 | import { LoadingCard } from '@/app/components/common/LoadingCard'; 10 | 11 | type Props = Readonly<{ 12 | params: { 13 | address: string; 14 | }; 15 | }>; 16 | 17 | function CompressionCardRenderer({ 18 | account, 19 | onNotFound, 20 | }: React.ComponentProps['renderComponent']>) { 21 | return ( 22 | ( 24 | 25 | )} 26 | > 27 | }> 28 | {} 29 | 30 | 31 | ); 32 | } 33 | 34 | export default function CompressionPageClient({ params: { address } }: Props) { 35 | return ; 36 | } 37 | -------------------------------------------------------------------------------- /app/address/[address]/compression/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import CompressionPageClient from './page-client'; 5 | 6 | type Props = Readonly<{ 7 | params: { 8 | address: string; 9 | }; 10 | }>; 11 | 12 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 13 | return { 14 | description: `Information about the Compressed NFT with address ${props.params.address} on Solana`, 15 | title: `Compression Information | ${await getReadableTitleFromAddress(props)} | Solana`, 16 | }; 17 | } 18 | 19 | export default function CompressionPage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/address/[address]/concurrent-merkle-tree/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ConcurrentMerkleTreeCard } from '@components/account/ConcurrentMerkleTreeCard'; 4 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 5 | import { PROGRAM_ID } from '@solana/spl-account-compression'; 6 | import React from 'react'; 7 | 8 | type Props = Readonly<{ 9 | params: { 10 | address: string; 11 | }; 12 | }>; 13 | 14 | function ConcurrentMerkleTreeCardRenderer({ 15 | account, 16 | onNotFound, 17 | }: React.ComponentProps['renderComponent']>) { 18 | const rawData = account?.data?.raw; 19 | if (!rawData || account.owner.toBase58() !== PROGRAM_ID.toBase58()) { 20 | return onNotFound(); 21 | } 22 | return ; 23 | } 24 | 25 | export default function MetaplexNFTMetadataPageClient({ params: { address } }: Props) { 26 | return ; 27 | } 28 | -------------------------------------------------------------------------------- /app/address/[address]/concurrent-merkle-tree/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import ConcurrentMerkleTreePageClient from './page-client'; 5 | 6 | type Props = Readonly<{ 7 | params: { 8 | address: string; 9 | }; 10 | }>; 11 | 12 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 13 | return { 14 | description: `Contents of the SPL Concurrent Merkle Tree at address ${props.params.address} on Solana`, 15 | title: `Concurrent Merkle Tree | ${await getReadableTitleFromAddress(props)} | Solana`, 16 | }; 17 | } 18 | 19 | export default function ConcurrentMerkleTreePage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/address/[address]/domains/page.tsx: -------------------------------------------------------------------------------- 1 | import { DomainsCard } from '@components/account/DomainsCard'; 2 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 3 | import { Metadata } from 'next/types'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | address: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 12 | return { 13 | description: `Domain names owned by the address ${props.params.address} on Solana`, 14 | title: `Domains | ${await getReadableTitleFromAddress(props)} | Solana`, 15 | }; 16 | } 17 | 18 | export default function OwnedDomainsPage({ params: { address } }: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/address/[address]/entries/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LookupTableEntriesCard } from '@components/account/address-lookup-table/LookupTableEntriesCard'; 4 | import { isAddressLookupTableAccount } from '@components/account/address-lookup-table/types'; 5 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 6 | import React from 'react'; 7 | import { Address } from 'web3js-experimental'; 8 | 9 | type Props = Readonly<{ 10 | params: { 11 | address: string; 12 | }; 13 | }>; 14 | 15 | function AddressLookupTableEntriesRenderer({ 16 | account, 17 | onNotFound, 18 | }: React.ComponentProps['renderComponent']>) { 19 | const parsedData = account?.data.parsed; 20 | const rawData = account?.data.raw; 21 | if (parsedData && parsedData.program === 'address-lookup-table' && parsedData.parsed.type === 'lookupTable') { 22 | return ; 23 | } else if (rawData && isAddressLookupTableAccount(account.owner.toBase58() as Address, rawData)) { 24 | return ; 25 | } else { 26 | return onNotFound(); 27 | } 28 | } 29 | 30 | export default function AddressLookupTableEntriesPageClient({ params: { address } }: Props) { 31 | return ; 32 | } 33 | -------------------------------------------------------------------------------- /app/address/[address]/entries/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import AddressLookupTableEntriesPageClient from './page-client'; 5 | 6 | type Props = Readonly<{ 7 | params: { 8 | address: string; 9 | }; 10 | }>; 11 | 12 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 13 | return { 14 | description: `Entries of the address lookup table at ${props.params.address} on Solana`, 15 | title: `Address Lookup Table Entries | ${await getReadableTitleFromAddress(props)} | Solana`, 16 | }; 17 | } 18 | 19 | export default function AddressLookupTableEntriesPage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/address/[address]/feature-gate/page.tsx: -------------------------------------------------------------------------------- 1 | import { FeatureGateCard } from '@components/account/FeatureGateCard'; 2 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 3 | import { Metadata } from 'next/types'; 4 | import ReactMarkdown from 'react-markdown'; 5 | import remarkFrontmatter from 'remark-frontmatter'; 6 | import remarkGFM from 'remark-gfm'; 7 | 8 | import { fetchFeatureGateInformation } from '@/app/features/feature-gate'; 9 | import { getFeatureInfo } from '@/app/utils/feature-gate/utils'; 10 | 11 | type Props = Readonly<{ 12 | params: { 13 | address: string; 14 | }; 15 | }>; 16 | 17 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 18 | return { 19 | description: `Feature information for address ${props.params.address} on Solana`, 20 | title: `Feature Gate | ${await getReadableTitleFromAddress(props)} | Solana`, 21 | }; 22 | } 23 | 24 | export default async function FeatureGatePage({ params: { address } }: Props) { 25 | const feature = getFeatureInfo(address); 26 | const data = await fetchFeatureGateInformation(feature); 27 | 28 | // remark-gfm won't handle github-flavoured-markdown with a table present at it 29 | // TODO: figure out a configuration to render GFM table correctly 30 | return ( 31 | 32 | {data[0]} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/address/[address]/instructions/page.tsx: -------------------------------------------------------------------------------- 1 | import { TokenInstructionsCard } from '@components/account/history/TokenInstructionsCard'; 2 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 3 | import { Metadata } from 'next/types'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | address: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 12 | return { 13 | description: `A list of transactions that include an instruction involving the token with address ${props.params.address} on Solana`, 14 | title: `Token Instructions | ${await getReadableTitleFromAddress(props)} | Solana`, 15 | }; 16 | } 17 | 18 | export default function TokenInstructionsPage({ params: { address } }: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/address/[address]/metadata/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { MetaplexMetadataCard } from '@components/account/MetaplexMetadataCard'; 4 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 5 | import React, { Suspense } from 'react'; 6 | 7 | import { LoadingCard } from '@/app/components/common/LoadingCard'; 8 | 9 | type Props = Readonly<{ 10 | params: { 11 | address: string; 12 | }; 13 | }>; 14 | 15 | function MetaplexMetadataCardRenderer({ 16 | account, 17 | onNotFound, 18 | }: React.ComponentProps['renderComponent']>) { 19 | return ( 20 | }> 21 | {} 22 | 23 | ); 24 | } 25 | 26 | export default function MetaplexNFTMetadataPageClient({ params: { address } }: Props) { 27 | return ; 28 | } 29 | -------------------------------------------------------------------------------- /app/address/[address]/metadata/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import MetaplexNFTMetadataPageClient from './page-client'; 5 | 6 | type Props = Readonly<{ 7 | params: { 8 | address: string; 9 | }; 10 | }>; 11 | 12 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 13 | return { 14 | description: `Metadata for the Metaplex NFT with address ${props.params.address} on Solana`, 15 | title: `Metaplex NFT Metadata | ${await getReadableTitleFromAddress(props)} | Solana`, 16 | }; 17 | } 18 | 19 | export default function MetaplexNFTMetadataPage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/address/[address]/nftoken-collection-nfts/page.tsx: -------------------------------------------------------------------------------- 1 | import { NFTokenCollectionNFTGrid } from '@components/account/nftoken/NFTokenCollectionNFTGrid'; 2 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 3 | import { Metadata } from 'next/types'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | address: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 12 | return { 13 | description: `NFToken NFTs belonging to the collection ${props.params.address} on Solana`, 14 | title: `NFToken Collection NFTs | ${await getReadableTitleFromAddress(props)} | Solana`, 15 | }; 16 | } 17 | 18 | export default function NFTokenCollectionPage({ params: { address } }: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/address/[address]/page.tsx: -------------------------------------------------------------------------------- 1 | import { TransactionHistoryCard } from '@components/account/history/TransactionHistoryCard'; 2 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 3 | import { Metadata } from 'next/types'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | address: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 12 | return { 13 | description: `History of all transactions involving the address ${props.params.address} on Solana`, 14 | title: `Transaction History | ${await getReadableTitleFromAddress(props)} | Solana`, 15 | }; 16 | } 17 | 18 | export default function TransactionHistoryPage({ params: { address } }: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/address/[address]/program-multisig/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 4 | import React from 'react'; 5 | 6 | import { ProgramMultisigCard } from '@/app/components/account/ProgramMultisigCard'; 7 | 8 | type Props = Readonly<{ 9 | params: { 10 | address: string; 11 | }; 12 | }>; 13 | 14 | function ProgramMultisigCardRenderer({ 15 | account, 16 | onNotFound, 17 | }: React.ComponentProps['renderComponent']>) { 18 | const parsedData = account?.data?.parsed; 19 | if (!parsedData || parsedData?.program !== 'bpf-upgradeable-loader') { 20 | return onNotFound(); 21 | } 22 | return ; 23 | } 24 | 25 | export default function ProgramMultisigPageClient({ params: { address } }: Props) { 26 | return ; 27 | } 28 | -------------------------------------------------------------------------------- /app/address/[address]/program-multisig/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import ProgramMultisigPageClient from './page-client'; 5 | 6 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 7 | return { 8 | description: `Multisig information for the upgrade authority of the program with address ${props.params.address} on Solana`, 9 | title: `Upgrade Authority Multisig | ${await getReadableTitleFromAddress(props)} | Solana`, 10 | }; 11 | } 12 | 13 | type Props = Readonly<{ 14 | params: { 15 | address: string; 16 | }; 17 | }>; 18 | 19 | export default function ProgramMultisigPage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/address/[address]/rewards/page.tsx: -------------------------------------------------------------------------------- 1 | import { RewardsCard } from '@components/account/RewardsCard'; 2 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 3 | import { Metadata } from 'next/types'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | address: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 12 | return { 13 | description: `Rewards due to the address ${props.params.address} by epoch on Solana`, 14 | title: `Address Rewards | ${await getReadableTitleFromAddress(props)} | Solana`, 15 | }; 16 | } 17 | 18 | export default function BlockRewardsPage({ params: { address } }: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/address/[address]/security/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 4 | import { SecurityCard } from '@components/account/SecurityCard'; 5 | import React from 'react'; 6 | 7 | type Props = Readonly<{ 8 | params: { 9 | address: string; 10 | }; 11 | }>; 12 | 13 | function SecurityCardRenderer({ 14 | account, 15 | onNotFound, 16 | }: React.ComponentProps['renderComponent']>) { 17 | const parsedData = account?.data?.parsed; 18 | if (!parsedData || parsedData?.program !== 'bpf-upgradeable-loader') { 19 | return onNotFound(); 20 | } 21 | return ; 22 | } 23 | 24 | export default function SecurityPageClient({ params: { address } }: Props) { 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /app/address/[address]/security/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import SecurityPageClient from './page-client'; 5 | 6 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 7 | return { 8 | description: `Contents of the security.txt for the program with address ${props.params.address} on Solana`, 9 | title: `Security | ${await getReadableTitleFromAddress(props)} | Solana`, 10 | }; 11 | } 12 | 13 | type Props = Readonly<{ 14 | params: { 15 | address: string; 16 | }; 17 | }>; 18 | 19 | export default function SecurityPage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/address/[address]/slot-hashes/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 4 | import { SlotHashesCard } from '@components/account/SlotHashesCard'; 5 | import React from 'react'; 6 | 7 | type Props = Readonly<{ 8 | params: { 9 | address: string; 10 | }; 11 | }>; 12 | 13 | function SlotHashesCardRenderer({ 14 | account, 15 | onNotFound, 16 | }: React.ComponentProps['renderComponent']>) { 17 | const parsedData = account?.data?.parsed; 18 | if (!parsedData || parsedData.program !== 'sysvar' || parsedData.parsed.type !== 'slotHashes') { 19 | return onNotFound(); 20 | } 21 | return ; 22 | } 23 | 24 | export default function SlotHashesPageClient({ params: { address } }: Props) { 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /app/address/[address]/slot-hashes/page.tsx: -------------------------------------------------------------------------------- 1 | import SlotHashesPageClient from './page-client'; 2 | 3 | type Props = Readonly<{ 4 | params: { 5 | address: string; 6 | }; 7 | }>; 8 | 9 | export const metadata = { 10 | description: `Hashes of each slot on Solana`, 11 | title: `Slot Hashes | Solana`, 12 | }; 13 | 14 | export default function SlotHashesPage(props: Props) { 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /app/address/[address]/stake-history/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 4 | import { StakeHistoryCard } from '@components/account/StakeHistoryCard'; 5 | import React from 'react'; 6 | 7 | type Props = Readonly<{ 8 | params: { 9 | address: string; 10 | }; 11 | }>; 12 | 13 | function StakeHistoryCardRenderer({ 14 | account, 15 | onNotFound, 16 | }: React.ComponentProps['renderComponent']>) { 17 | const parsedData = account?.data?.parsed; 18 | if (!parsedData || parsedData.program !== 'sysvar' || parsedData.parsed.type !== 'stakeHistory') { 19 | return onNotFound(); 20 | } 21 | return ; 22 | } 23 | 24 | export default function StakeHistoryPageClient({ params: { address } }: Props) { 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /app/address/[address]/stake-history/page.tsx: -------------------------------------------------------------------------------- 1 | import StakeHistoryPageClient from './page-client'; 2 | 3 | type Props = Readonly<{ 4 | params: { 5 | address: string; 6 | }; 7 | }>; 8 | 9 | export const metadata = { 10 | description: `Stake history for each epoch on Solana`, 11 | title: `Stake History | Solana`, 12 | }; 13 | 14 | export default function StakeHistoryPage(props: Props) { 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /app/address/[address]/styles.css: -------------------------------------------------------------------------------- 1 | @import '../../styles.css'; 2 | 3 | :root { 4 | /* 5 | * Fill default vars for transformations as without them `rotate-N` does not work 6 | */ 7 | --tw-translate-x: 0; 8 | --tw-skew-x: 0; 9 | --tw-skew-y: 0; 10 | --tw-scale-x: 1; 11 | --tw-scale-y: 1; 12 | } 13 | -------------------------------------------------------------------------------- /app/address/[address]/token-extensions/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import TokenExtensionsEntriesPageClient, { Props } from './page-client'; 5 | 6 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 7 | return { 8 | description: `Token extensions information for address ${props.params.address} on Solana`, 9 | title: `Token Extensions | ${await getReadableTitleFromAddress(props)} | Solana`, 10 | }; 11 | } 12 | 13 | export default function TokenExtensionsPage(props: Props) { 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /app/address/[address]/tokens/page.tsx: -------------------------------------------------------------------------------- 1 | import { OwnedTokensCard } from '@components/account/OwnedTokensCard'; 2 | import { TokenHistoryCard } from '@components/account/TokenHistoryCard'; 3 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 4 | import { Metadata } from 'next/types'; 5 | 6 | import { TransactionsProvider } from '@/app/providers/transactions'; 7 | 8 | type Props = Readonly<{ 9 | params: { 10 | address: string; 11 | }; 12 | }>; 13 | 14 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 15 | return { 16 | description: `All tokens owned by the address ${props.params.address} on Solana`, 17 | title: `Tokens | ${await getReadableTitleFromAddress(props)} | Solana`, 18 | }; 19 | } 20 | 21 | export default function OwnedTokensPage({ params: { address } }: Props) { 22 | return ( 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/address/[address]/transfers/page.tsx: -------------------------------------------------------------------------------- 1 | import { TokenTransfersCard } from '@components/account/history/TokenTransfersCard'; 2 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 3 | import { Metadata } from 'next/types'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | address: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 12 | return { 13 | description: `History of all token transfers involving the address ${props.params.address} on Solana`, 14 | title: `Transfers | ${await getReadableTitleFromAddress(props)} | Solana`, 15 | }; 16 | } 17 | 18 | export default function TokenTransfersPage({ params: { address } }: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/address/[address]/verified-build/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 4 | import React from 'react'; 5 | import { ErrorBoundary } from 'react-error-boundary'; 6 | 7 | import { VerifiedBuildCard } from '@/app/components/account/VerifiedBuildCard'; 8 | import { ErrorCard } from '@/app/components/common/ErrorCard'; 9 | 10 | type Props = Readonly<{ 11 | params: { 12 | address: string; 13 | }; 14 | }>; 15 | 16 | function VerifiedBuildCardRenderer({ 17 | account, 18 | onNotFound, 19 | }: React.ComponentProps['renderComponent']>) { 20 | const parsedData = account?.data?.parsed; 21 | if (!parsedData || parsedData?.program !== 'bpf-upgradeable-loader') { 22 | return onNotFound(); 23 | } 24 | return ( 25 | }> 26 | 27 | 28 | ); 29 | } 30 | 31 | export default function VerifiedBuildPageClient({ params: { address } }: Props) { 32 | return ; 33 | } 34 | -------------------------------------------------------------------------------- /app/address/[address]/verified-build/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import VerifiedBuildClient from './page-client'; 5 | 6 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 7 | return { 8 | description: `Contents of the verified build info for the program with address ${props.params.address} on Solana`, 9 | title: `Verified Build | ${await getReadableTitleFromAddress(props)} | Solana`, 10 | }; 11 | } 12 | 13 | type Props = Readonly<{ 14 | params: { 15 | address: string; 16 | }; 17 | }>; 18 | 19 | export default function VerifiedBuildPage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/address/[address]/vote-history/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer'; 4 | import { VotesCard } from '@components/account/VotesCard'; 5 | import React from 'react'; 6 | 7 | type Props = Readonly<{ 8 | params: { 9 | address: string; 10 | }; 11 | }>; 12 | 13 | function VotesCardRenderer({ 14 | account, 15 | onNotFound, 16 | }: React.ComponentProps['renderComponent']>) { 17 | const parsedData = account?.data?.parsed; 18 | if (!parsedData || parsedData?.program !== 'vote') { 19 | return onNotFound(); 20 | } 21 | return ; 22 | } 23 | 24 | export default function VoteHistoryPageClient({ params: { address } }: Props) { 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /app/address/[address]/vote-history/page.tsx: -------------------------------------------------------------------------------- 1 | import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address'; 2 | import { Metadata } from 'next/types'; 3 | 4 | import VoteHistoryPageClient from './page-client'; 5 | 6 | type Props = Readonly<{ 7 | params: { 8 | address: string; 9 | }; 10 | }>; 11 | 12 | export async function generateMetadata(props: AddressPageMetadataProps): Promise { 13 | return { 14 | description: `Vote history of the address ${props.params.address} by slot on Solana`, 15 | title: `Vote History | ${await getReadableTitleFromAddress(props)} | Solana`, 16 | }; 17 | } 18 | 19 | export default function VoteHistoryPage(props: Props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /app/api/codama/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | import { getCodamaIdl } from '@/app/components/instruction/codama/getCodamaIdl'; 4 | import { Cluster, serverClusterUrl } from '@/app/utils/cluster'; 5 | 6 | const CACHE_DURATION = 30 * 60; // 30 minutes 7 | 8 | const CACHE_HEADERS = { 9 | 'Cache-Control': `public, s-maxage=${CACHE_DURATION}, stale-while-revalidate=60`, 10 | }; 11 | 12 | export async function GET(request: Request) { 13 | const { searchParams } = new URL(request.url); 14 | const clusterProp = searchParams.get('cluster'); 15 | const programAddress = searchParams.get('programAddress'); 16 | 17 | if (!programAddress || !clusterProp) { 18 | return NextResponse.json({ error: 'Invalid query params' }, { status: 400 }); 19 | } 20 | 21 | const url = Number(clusterProp) in Cluster && serverClusterUrl(Number(clusterProp) as Cluster, ''); 22 | 23 | if (!url) { 24 | return NextResponse.json({ error: 'Invalid cluster' }, { status: 400 }); 25 | } 26 | 27 | try { 28 | const codamaIdl = await getCodamaIdl(programAddress, url); 29 | return NextResponse.json( 30 | { codamaIdl }, 31 | { 32 | headers: CACHE_HEADERS, 33 | status: 200, 34 | } 35 | ); 36 | } catch (error) { 37 | return NextResponse.json( 38 | { details: error, error: error instanceof Error ? error.message : 'Unknown error' }, 39 | { 40 | headers: CACHE_HEADERS, 41 | status: 200, 42 | } 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/api/domain-info/[domain]/route.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from '@solana/web3.js'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | import { MAINNET_BETA_URL } from '@/app/utils/cluster'; 5 | import { getANSDomainInfo, getDomainInfo } from '@/app/utils/domain-info'; 6 | 7 | type Params = { 8 | params: { 9 | domain: string; 10 | }; 11 | }; 12 | 13 | export type FetchedDomainInfo = Awaited>; 14 | 15 | export async function GET(_request: Request, { params: { domain } }: Params) { 16 | // Intentionally using legacy web3js for compatibility with bonfida library 17 | // This is an API route so won't affect client bundle 18 | // We only fetch domains on mainnet 19 | const connection = new Connection(MAINNET_BETA_URL); 20 | const domainInfo = await (domain.substring(domain.length - 4) === '.sol' 21 | ? getDomainInfo(domain, connection) 22 | : getANSDomainInfo(domain, connection)); 23 | 24 | return NextResponse.json(domainInfo, { 25 | headers: { 26 | // 24 hours 27 | 'Cache-Control': 'max-age=86400', 28 | }, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /app/api/metadata/proxy/feature/errors.ts: -------------------------------------------------------------------------------- 1 | export class StatusError extends Error { 2 | status: number; 3 | constructor(message: string, options: ErrorOptions & { cause: number }) { 4 | super(message); 5 | this.status = options.cause; 6 | } 7 | } 8 | 9 | export const invalidRequestError = new StatusError('Invalid Request', { cause: 400 }); 10 | 11 | export const accessDeniedError = new StatusError('Access Denied', { cause: 403 }); 12 | 13 | export const resourceNotFoundError = new StatusError('Resource Not Found', { cause: 404 }); 14 | 15 | export const maxSizeError = new StatusError('Max Content Size Exceeded', { cause: 413 }); 16 | 17 | export const unsupportedMediaError = new StatusError('Unsupported Media Type', { cause: 415 }); 18 | 19 | export const generalError = new StatusError('General Error', { cause: 500 }); 20 | 21 | export const gatewayTimeoutError = new StatusError('Gateway Timeout', { cause: 504 }); 22 | 23 | export const errors = { 24 | 400: invalidRequestError, 25 | 403: accessDeniedError, 26 | 404: resourceNotFoundError, 27 | 413: maxSizeError, 28 | 415: unsupportedMediaError, 29 | 500: generalError, 30 | 504: gatewayTimeoutError, 31 | }; 32 | 33 | export function matchAbortError(error: unknown): error is Error { 34 | return Boolean(error instanceof Error && error.name === 'AbortError'); 35 | } 36 | 37 | export function matchMaxSizeError(error: unknown): error is Error { 38 | return Boolean(error instanceof Error && error.message.match(/over limit:/)); 39 | } 40 | 41 | export function matchTimeoutError(error: unknown): error is Error { 42 | return Boolean(error instanceof Error && error.name === 'TimeoutError'); 43 | } 44 | -------------------------------------------------------------------------------- /app/api/metadata/proxy/feature/processors.ts: -------------------------------------------------------------------------------- 1 | import { default as fetch } from 'node-fetch'; 2 | 3 | import Logger from '@/app/utils/logger'; 4 | 5 | import { errors, matchMaxSizeError } from './errors'; 6 | 7 | /** 8 | * process binary data and catch any specific errors 9 | */ 10 | export async function processBinary(data: fetch.Response) { 11 | const headers = data.headers; 12 | 13 | try { 14 | // request binary data to check for max-size excess 15 | const buffer = await data.arrayBuffer(); 16 | 17 | return { data: buffer, headers }; 18 | } catch (error) { 19 | if (matchMaxSizeError(error)) { 20 | throw errors[413]; 21 | } else { 22 | Logger.debug('Debug:', error); 23 | throw errors[500]; 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * process text data as json and handle specific errors 30 | */ 31 | export async function processJson(data: fetch.Response) { 32 | const headers = data.headers; 33 | 34 | try { 35 | const json = await data.json(); 36 | 37 | return { data: json, headers }; 38 | } catch (error) { 39 | if (matchMaxSizeError(error)) { 40 | throw errors[413]; 41 | } else if (error instanceof SyntaxError) { 42 | // Handle JSON syntax errors specifically 43 | throw errors[415]; 44 | } else { 45 | Logger.debug('Debug:', error); 46 | throw errors[500]; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/api/ping/[network]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | type Params = { 4 | params: { 5 | network: 'mainnet'; 6 | }; 7 | }; 8 | 9 | export type ValidatorsAppPingStats = { 10 | interval: number; 11 | max: number; 12 | median: number; 13 | min: number; 14 | network: string; 15 | num_of_records: number; 16 | time_from: string; 17 | average_slot_latency: number; 18 | tps: number; 19 | }; 20 | 21 | const PING_INTERVALS: number[] = [1, 3, 12]; 22 | 23 | export async function GET(_request: Request, { params: { network } }: Params) { 24 | const responses = await Promise.all( 25 | PING_INTERVALS.map(interval => 26 | fetch(`https://www.validators.app/api/v1/ping-thing-stats/${network}.json?interval=${interval}`, { 27 | headers: { 28 | Token: process.env.PING_API_KEY || '', 29 | }, 30 | next: { 31 | revalidate: 60, 32 | }, 33 | }) 34 | ) 35 | ); 36 | const data: { [interval: number]: ValidatorsAppPingStats[] } = {}; 37 | await Promise.all( 38 | responses.map(async (response, index) => { 39 | const interval = PING_INTERVALS[index]; 40 | data[interval] = await response.json(); 41 | }) 42 | ); 43 | 44 | return NextResponse.json(data, { 45 | headers: { 46 | 'Cache-Control': 'no-store, max-age=0', 47 | }, 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /app/block/[slot]/accounts/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { BlockAccountsCard } from '@components/block/BlockAccountsCard'; 4 | import { useBlock, useFetchBlock } from '@providers/block'; 5 | import { useCluster } from '@providers/cluster'; 6 | import { ClusterStatus } from '@utils/cluster'; 7 | import { notFound } from 'next/navigation'; 8 | import React from 'react'; 9 | 10 | type Props = Readonly<{ params: { slot: string } }>; 11 | 12 | export default function BlockAccountsTab({ params: { slot } }: Props) { 13 | const slotNumber = Number(slot); 14 | if (isNaN(slotNumber) || slotNumber >= Number.MAX_SAFE_INTEGER || slotNumber % 1 !== 0) { 15 | notFound(); 16 | } 17 | const confirmedBlock = useBlock(slotNumber); 18 | const fetchBlock = useFetchBlock(); 19 | const { status } = useCluster(); 20 | // Fetch block on load 21 | React.useEffect(() => { 22 | if (!confirmedBlock && status === ClusterStatus.Connected) { 23 | fetchBlock(slotNumber); 24 | } 25 | }, [slotNumber, status]); // eslint-disable-line react-hooks/exhaustive-deps 26 | if (confirmedBlock?.data?.block) { 27 | return ; 28 | } 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /app/block/[slot]/accounts/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next/types'; 2 | 3 | import BlockAccountsTabClient from './page-client'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | slot: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata({ params: { slot } }: Props): Promise { 12 | return { 13 | description: `Statistics pertaining to accounts which were accessed or written to during block ${slot} on Solana`, 14 | title: `Accounts Active In Block | ${slot} | Solana`, 15 | }; 16 | } 17 | 18 | export default function BlockAccountsTab(props: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/block/[slot]/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { BlockHistoryCard } from '@components/block/BlockHistoryCard'; 4 | import { useBlock, useFetchBlock } from '@providers/block'; 5 | import { useCluster } from '@providers/cluster'; 6 | import { ClusterStatus } from '@utils/cluster'; 7 | import { notFound } from 'next/navigation'; 8 | import React from 'react'; 9 | 10 | type Props = Readonly<{ params: { slot: string } }>; 11 | 12 | export default function BlockTransactionsTabClient({ params: { slot } }: Props) { 13 | const slotNumber = Number(slot); 14 | if (isNaN(slotNumber) || slotNumber >= Number.MAX_SAFE_INTEGER || slotNumber % 1 !== 0) { 15 | notFound(); 16 | } 17 | const confirmedBlock = useBlock(slotNumber); 18 | const fetchBlock = useFetchBlock(); 19 | const { status } = useCluster(); 20 | // Fetch block on load 21 | React.useEffect(() => { 22 | if (!confirmedBlock && status === ClusterStatus.Connected) { 23 | fetchBlock(slotNumber); 24 | } 25 | }, [slotNumber, status]); // eslint-disable-line react-hooks/exhaustive-deps 26 | if (confirmedBlock?.data?.block) { 27 | return ; 28 | } 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /app/block/[slot]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next/types'; 2 | 3 | import BlockTransactionsTabClient from './page-client'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | slot: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata({ params: { slot } }: Props): Promise { 12 | return { 13 | description: `History of all transactions during block ${slot} on Solana`, 14 | title: `Block | ${slot} | Solana`, 15 | }; 16 | } 17 | 18 | export default function BlockTransactionsTab(props: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/block/[slot]/programs/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { BlockProgramsCard } from '@components/block/BlockProgramsCard'; 4 | import { useBlock, useFetchBlock } from '@providers/block'; 5 | import { useCluster } from '@providers/cluster'; 6 | import { ClusterStatus } from '@utils/cluster'; 7 | import { notFound } from 'next/navigation'; 8 | import React from 'react'; 9 | 10 | type Props = Readonly<{ params: { slot: string } }>; 11 | 12 | export default function BlockProgramsTab({ params: { slot } }: Props) { 13 | const slotNumber = Number(slot); 14 | if (isNaN(slotNumber) || slotNumber >= Number.MAX_SAFE_INTEGER || slotNumber % 1 !== 0) { 15 | notFound(); 16 | } 17 | const confirmedBlock = useBlock(slotNumber); 18 | const fetchBlock = useFetchBlock(); 19 | const { status } = useCluster(); 20 | // Fetch block on load 21 | React.useEffect(() => { 22 | if (!confirmedBlock && status === ClusterStatus.Connected) { 23 | fetchBlock(slotNumber); 24 | } 25 | }, [slotNumber, status]); // eslint-disable-line react-hooks/exhaustive-deps 26 | if (confirmedBlock?.data?.block) { 27 | return ; 28 | } 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /app/block/[slot]/programs/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next/types'; 2 | 3 | import BlockProgramsTabClient from './page-client'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | slot: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata({ params: { slot } }: Props): Promise { 12 | return { 13 | description: `Statistics pertaining to programs which were active during block ${slot} on Solana`, 14 | title: `Programs Active In Block | ${slot} | Solana`, 15 | }; 16 | } 17 | 18 | export default function BlockProgramsTab(props: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/block/[slot]/rewards/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { BlockRewardsCard } from '@components/block/BlockRewardsCard'; 4 | import { useBlock, useFetchBlock } from '@providers/block'; 5 | import { useCluster } from '@providers/cluster'; 6 | import { ClusterStatus } from '@utils/cluster'; 7 | import { notFound } from 'next/navigation'; 8 | import React from 'react'; 9 | 10 | type Props = Readonly<{ params: { slot: string } }>; 11 | 12 | export default function BlockRewardsTabClient({ params: { slot } }: Props) { 13 | const slotNumber = Number(slot); 14 | if (isNaN(slotNumber) || slotNumber >= Number.MAX_SAFE_INTEGER || slotNumber % 1 !== 0) { 15 | notFound(); 16 | } 17 | const confirmedBlock = useBlock(slotNumber); 18 | const fetchBlock = useFetchBlock(); 19 | const { status } = useCluster(); 20 | // Fetch block on load 21 | React.useEffect(() => { 22 | if (!confirmedBlock && status === ClusterStatus.Connected) { 23 | fetchBlock(slotNumber); 24 | } 25 | }, [slotNumber, status]); // eslint-disable-line react-hooks/exhaustive-deps 26 | if (confirmedBlock?.data?.block) { 27 | return ; 28 | } 29 | return null; 30 | } 31 | -------------------------------------------------------------------------------- /app/block/[slot]/rewards/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next/types'; 2 | 3 | import BlockRewardsTabClient from './page-client'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | slot: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata({ params: { slot } }: Props): Promise { 12 | return { 13 | description: `List of addresses to which rewards were disbursed during block ${slot} on Solana`, 14 | title: `Block Rewards | ${slot} | Solana`, 15 | }; 16 | } 17 | 18 | export default function BlockRewardsTab(props: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { AccountHeader } from '@components/account/AccountHeader'; 2 | import { TokenMarketData } from '@components/common/TokenMarketData'; 3 | import { ComponentProps } from 'react'; 4 | 5 | import { useCoinGecko } from '@/app/utils/coingecko'; 6 | 7 | type HeaderProps = ComponentProps; 8 | 9 | export function Header({ address, account, tokenInfo, isTokenInfoLoading }: HeaderProps) { 10 | const coinInfo = useCoinGecko(tokenInfo?.extensions?.coingeckoId); 11 | 12 | return ( 13 |
14 |
15 | 21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/components/StatsNotReady.tsx: -------------------------------------------------------------------------------- 1 | import { useCluster } from '@providers/cluster'; 2 | import { useStatsProvider } from '@providers/stats/solanaClusterStats'; 3 | import React from 'react'; 4 | import { RefreshCw } from 'react-feather'; 5 | 6 | const CLUSTER_STATS_TIMEOUT = 5000; 7 | 8 | export function StatsNotReady({ error }: { error: boolean }) { 9 | const { setTimedOut, retry, active } = useStatsProvider(); 10 | const { cluster } = useCluster(); 11 | 12 | React.useEffect(() => { 13 | let timedOut: NodeJS.Timeout; 14 | if (!error) { 15 | timedOut = setTimeout(setTimedOut, CLUSTER_STATS_TIMEOUT); 16 | } 17 | return () => { 18 | if (timedOut) { 19 | clearTimeout(timedOut); 20 | } 21 | }; 22 | }, [setTimedOut, cluster, error]); 23 | 24 | if (error || !active) { 25 | return ( 26 |
27 | There was a problem loading cluster stats.{' '} 28 | 37 |
38 | ); 39 | } 40 | 41 | return ( 42 |
43 | 44 | Loading 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/components/account/FeatureGateCard.tsx: -------------------------------------------------------------------------------- 1 | export function FeatureGateCard({ children }: { children: React.ReactNode }) { 2 | return ( 3 |
4 |
5 |

Feature Information

6 |
7 |
8 |
{children}
9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/components/account/address-lookup-table/types.ts: -------------------------------------------------------------------------------- 1 | import { Address } from 'web3js-experimental'; 2 | 3 | const LOOKUP_TABLE_ACCOUNT_TYPE = 1; 4 | const PROGRAM_ID = 5 | 'AddressLookupTab1e1111111111111111111111111' as Address<'AddressLookupTab1e1111111111111111111111111'>; 6 | 7 | export function isAddressLookupTableAccount(accountOwner: Address, accountData: Uint8Array): boolean { 8 | if (accountOwner !== PROGRAM_ID) return false; 9 | if (!accountData || accountData.length === 0) return false; 10 | return accountData[0] === LOOKUP_TABLE_ACCOUNT_TYPE; 11 | } 12 | -------------------------------------------------------------------------------- /app/components/account/history/common.tsx: -------------------------------------------------------------------------------- 1 | import { ParsedTransactionWithMeta } from '@solana/web3.js'; 2 | 3 | export type MintDetails = { 4 | decimals: number; 5 | mint: string; 6 | }; 7 | 8 | export function extractMintDetails(transactionWithMeta: ParsedTransactionWithMeta, mintMap: Map) { 9 | if (transactionWithMeta.meta?.preTokenBalances) { 10 | transactionWithMeta.meta.preTokenBalances.forEach(balance => { 11 | const account = transactionWithMeta.transaction.message.accountKeys[balance.accountIndex]; 12 | mintMap.set(account.pubkey.toBase58(), { 13 | decimals: balance.uiTokenAmount.decimals, 14 | mint: balance.mint, 15 | }); 16 | }); 17 | } 18 | 19 | if (transactionWithMeta.meta?.postTokenBalances) { 20 | transactionWithMeta.meta.postTokenBalances.forEach(balance => { 21 | const account = transactionWithMeta.transaction.message.accountKeys[balance.accountIndex]; 22 | mintMap.set(account.pubkey.toBase58(), { 23 | decimals: balance.uiTokenAmount.decimals, 24 | mint: balance.mint, 25 | }); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/components/account/nftoken/README.md: -------------------------------------------------------------------------------- 1 | # NFToken 2 | 3 | NFToken is a cheap, simple, secure NFT standard on Solana. 4 | 5 | You can find more information and support here: 6 | 7 | - [Website](https://nftoken.so) 8 | - [Twitter](https://twitter.com/nftoken_so) 9 | - [Lead Maintainer](https://twitter.com/VictorPontis) 10 | -------------------------------------------------------------------------------- /app/components/account/types.ts: -------------------------------------------------------------------------------- 1 | import { StatusType } from '@/app/components/shared/StatusBadge'; 2 | import { TokenExtension } from '@/app/validators/accounts/token-extension'; 3 | 4 | export type ParsedTokenExtension = Pick & { 5 | name: string; 6 | tooltip?: string; 7 | description?: string; 8 | status: StatusType; 9 | externalLinks: { label: string; url: string }[]; 10 | parsed?: TokenExtension['state']; 11 | }; 12 | -------------------------------------------------------------------------------- /app/components/common/Account.tsx: -------------------------------------------------------------------------------- 1 | import { SolBalance } from '@components/common/SolBalance'; 2 | import { Account } from '@providers/accounts'; 3 | import React from 'react'; 4 | import { RefreshCw } from 'react-feather'; 5 | 6 | import { Address } from './Address'; 7 | 8 | type AccountHeaderProps = { 9 | title: string; 10 | refresh: () => void; 11 | }; 12 | 13 | type AccountProps = { 14 | account: Account; 15 | }; 16 | 17 | export function AccountHeader({ title, refresh }: AccountHeaderProps) { 18 | return ( 19 |
20 |

{title}

21 | 25 |
26 | ); 27 | } 28 | 29 | export function AccountAddressRow({ account }: AccountProps) { 30 | return ( 31 | 32 | Address 33 | 34 |
35 | 36 | 37 | ); 38 | } 39 | 40 | export function AccountBalanceRow({ account }: AccountProps) { 41 | const { lamports } = account; 42 | return ( 43 | 44 | Balance (SOL) 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /app/components/common/BalanceDelta.tsx: -------------------------------------------------------------------------------- 1 | import { SolBalance } from '@components/common/SolBalance'; 2 | import { BigNumber } from 'bignumber.js'; 3 | import React from 'react'; 4 | 5 | export function BalanceDelta({ delta, isSol = false }: { delta: BigNumber; isSol?: boolean }) { 6 | let sols; 7 | 8 | if (isSol) { 9 | sols = ; 10 | } 11 | 12 | if (delta.gt(0)) { 13 | return +{isSol ? sols : delta.toString()}; 14 | } else if (delta.lt(0)) { 15 | return {isSol ? <>-{sols} : delta.toString()}; 16 | } 17 | 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /app/components/common/BaseRawDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import { TransactionInstruction } from '@solana/web3.js'; 3 | import React from 'react'; 4 | 5 | import { HexData } from './HexData'; 6 | 7 | /** 8 | * Component that displays accounts from any Instruction. 9 | * 10 | * VersionedMessage is optional as it will be present at inspector page only. 11 | */ 12 | export function BaseRawDetails({ ix }: { ix: TransactionInstruction }) { 13 | return ; 14 | } 15 | 16 | function BaseTransactionInstructionRawDetails({ ix }: { ix: TransactionInstruction }) { 17 | return ( 18 | <> 19 | {ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => ( 20 | 21 | 22 |
Account #{keyIndex + 1}
23 | {isWritable && Writable} 24 | {isSigner && Signer} 25 | 26 | 27 |
28 | 29 | 30 | ))} 31 | 32 | 33 | 34 | Instruction Data (Hex) 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/components/common/BaseRawParsedDetails.tsx: -------------------------------------------------------------------------------- 1 | import { ParsedInstruction } from '@solana/web3.js'; 2 | import React from 'react'; 3 | 4 | export function BaseRawParsedDetails({ ix, children }: { ix: ParsedInstruction; children?: React.ReactNode }) { 5 | return ( 6 | <> 7 | {children} 8 | 9 | 10 | 11 | Instruction Data (JSON) 12 | 13 | 14 |
{JSON.stringify(ix.parsed, null, 2)}
15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/components/common/Epoch.tsx: -------------------------------------------------------------------------------- 1 | import { useClusterPath } from '@utils/url'; 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | 5 | import { Copyable } from './Copyable'; 6 | 7 | type Props = { 8 | epoch: number | bigint; 9 | link?: boolean; 10 | }; 11 | export function Epoch({ epoch, link }: Props) { 12 | const epochPath = useClusterPath({ pathname: `/epoch/${epoch}` }); 13 | return ( 14 | 15 | {link ? ( 16 | 17 | {epoch.toLocaleString('en-US')} 18 | 19 | ) : ( 20 | epoch.toLocaleString('en-US') 21 | )} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/components/common/ErrorCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function ErrorCard({ 4 | retry, 5 | retryText, 6 | text, 7 | subtext, 8 | }: { 9 | retry?: () => void; 10 | retryText?: string; 11 | text: string; 12 | subtext?: string; 13 | }) { 14 | const buttonText = retryText || 'Try Again'; 15 | return ( 16 |
17 |
18 | {text} 19 | {retry && ( 20 | <> 21 | 22 | {buttonText} 23 | 24 |
25 | 26 | {buttonText} 27 | 28 |
29 | {subtext && ( 30 |
31 |
32 | {subtext} 33 |
34 | )} 35 | 36 | )} 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/components/common/IDLBadge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { IdlSpec } from '@/app/utils/convertLegacyIdl'; 4 | 5 | interface IDLBadgeProps { 6 | spec: IdlSpec; 7 | } 8 | 9 | export function IDLBadge({ spec }: IDLBadgeProps) { 10 | const badgeClass = spec === 'legacy' ? 'bg-warning' : 'bg-success'; 11 | const badgeText = spec === 'legacy' ? 'Legacy' : '0.30.1'; 12 | 13 | return {badgeText} Anchor IDL; 14 | } 15 | -------------------------------------------------------------------------------- /app/components/common/Identicon.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | import Jazzicon from '@metamask/jazzicon'; 6 | import { PublicKey } from '@solana/web3.js'; 7 | import bs58 from 'bs58'; 8 | import React, { useEffect, useRef } from 'react'; 9 | 10 | export function Identicon(props: { address?: string | PublicKey; style?: React.CSSProperties; className?: string }) { 11 | const { style, className } = props; 12 | const address = typeof props.address === 'string' ? props.address : props.address?.toBase58(); 13 | const ref = useRef(null); 14 | 15 | useEffect(() => { 16 | if (address && ref.current) { 17 | ref.current.innerHTML = ''; 18 | ref.current.className = className || ''; 19 | ref.current.appendChild( 20 | Jazzicon(style?.width || 16, parseInt(bs58.decode(address).toString('hex').slice(5, 15), 16)) 21 | ); 22 | } 23 | }, [address, style, className]); 24 | 25 | return
; 26 | } 27 | -------------------------------------------------------------------------------- /app/components/common/InfoTooltip.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useState } from 'react'; 2 | import { HelpCircle } from 'react-feather'; 3 | 4 | type Props = { 5 | text: string; 6 | children?: ReactNode; 7 | bottom?: boolean; 8 | right?: boolean; 9 | }; 10 | 11 | type State = 'hide' | 'show'; 12 | 13 | function Popover({ state, bottom, right, text }: { state: State; bottom?: boolean; right?: boolean; text: string }) { 14 | if (state === 'hide') return null; 15 | return ( 16 |
17 |
18 |
{text}
19 |
20 | ); 21 | } 22 | 23 | export function InfoTooltip({ bottom, right, text, children }: Props) { 24 | const [state, setState] = useState('hide'); 25 | 26 | const justify = right ? 'end' : 'start'; 27 | return ( 28 |
setState('show')} 31 | onMouseOut={() => setState('hide')} 32 | > 33 |
34 | {children} 35 |
36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/components/common/LoadingArtPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import ContentLoader from 'react-content-loader'; 2 | 3 | export function LoadingArtPlaceholder() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/common/LoadingCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function LoadingCard({ message }: { message?: string }) { 4 | return ( 5 |
6 |
7 | 8 | {message || 'Loading'} 9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/components/common/Overlay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type OverlayProps = { 4 | show: boolean; 5 | }; 6 | 7 | export function Overlay({ show }: OverlayProps) { 8 | return
; 9 | } 10 | -------------------------------------------------------------------------------- /app/components/common/SecurityTXTBadge.tsx: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { fromProgramData } from '@utils/security-txt'; 3 | import { useClusterPath } from '@utils/url'; 4 | import { ProgramDataAccountInfo } from '@validators/accounts/upgradeable-program'; 5 | import Link from 'next/link'; 6 | 7 | export function SecurityTXTBadge({ programData, pubkey }: { programData: ProgramDataAccountInfo; pubkey: PublicKey }) { 8 | const { securityTXT, error } = fromProgramData(programData); 9 | const securityTabPath = useClusterPath({ pathname: `/address/${pubkey.toBase58()}/security` }); 10 | if (securityTXT) { 11 | return ( 12 |

13 | 14 | Included 15 | 16 |

17 | ); 18 | } else { 19 | return ( 20 |

21 | {error} 22 |

23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/components/common/Signature.tsx: -------------------------------------------------------------------------------- 1 | import { TransactionSignature } from '@solana/web3.js'; 2 | import { useClusterPath } from '@utils/url'; 3 | import Link from 'next/link'; 4 | import React from 'react'; 5 | 6 | import { Copyable } from './Copyable'; 7 | 8 | type Props = { 9 | signature: TransactionSignature; 10 | alignRight?: boolean; 11 | link?: boolean; 12 | truncate?: boolean; 13 | truncateChars?: number; 14 | }; 15 | 16 | export function Signature({ signature, alignRight, link, truncate, truncateChars }: Props) { 17 | let signatureLabel = signature; 18 | 19 | if (truncateChars) { 20 | signatureLabel = signature.slice(0, truncateChars) + '…'; 21 | } 22 | const transactionPath = useClusterPath({ pathname: `/tx/${signature}` }); 23 | return ( 24 |
25 | 26 | 27 | {link ? ( 28 | 29 | {signatureLabel} 30 | 31 | ) : ( 32 | signatureLabel 33 | )} 34 | 35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/components/common/Slot.tsx: -------------------------------------------------------------------------------- 1 | import { useClusterPath } from '@utils/url'; 2 | import Link from 'next/link'; 3 | import React from 'react'; 4 | 5 | import { Copyable } from './Copyable'; 6 | 7 | type Props = { 8 | slot: number | bigint; 9 | link?: boolean; 10 | }; 11 | export function Slot({ slot, link }: Props) { 12 | const slotPath = useClusterPath({ pathname: `/block/${slot}` }); 13 | return ( 14 | 15 | {link ? ( 16 | 17 | {slot.toLocaleString('en-US')} 18 | 19 | ) : ( 20 | slot.toLocaleString('en-US') 21 | )} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/components/common/SolBalance.tsx: -------------------------------------------------------------------------------- 1 | import { lamportsToSolString } from '@utils/index'; 2 | import React from 'react'; 3 | 4 | export function SolBalance({ 5 | lamports, 6 | maximumFractionDigits = 9, 7 | }: { 8 | lamports: number | bigint; 9 | maximumFractionDigits?: number; 10 | }) { 11 | return ( 12 | 13 | ◎{lamportsToSolString(lamports, maximumFractionDigits)} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/components/common/TableCardBody.tsx: -------------------------------------------------------------------------------- 1 | import { cva, VariantProps } from 'class-variance-authority'; 2 | import React from 'react'; 3 | 4 | const tableVariants = cva(['table table-sm card-table'], { 5 | defaultVariants: { 6 | layout: 'compact', 7 | }, 8 | variants: { 9 | layout: { 10 | compact: ['table-nowrap'], 11 | expanded: [], 12 | }, 13 | }, 14 | }); 15 | 16 | export interface TableCardBodyProps extends VariantProps, React.PropsWithChildren {} 17 | 18 | export function TableCardBody({ children, ...props }: TableCardBodyProps) { 19 | return ( 20 |
21 | 22 | {children} 23 |
24 |
25 | ); 26 | } 27 | 28 | export interface TableCardBodyProps extends VariantProps, React.PropsWithChildren { 29 | headerComponent?: React.ReactNode; 30 | } 31 | 32 | export function TableCardBodyHeaded({ children, headerComponent, ...props }: TableCardBodyProps) { 33 | return ( 34 |
35 | 36 | {headerComponent ? {headerComponent} : null} 37 | {children} 38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/components/common/TimestampToggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { displayTimestamp, displayTimestampUtc } from '@utils/date'; 4 | import { useState } from 'react'; 5 | 6 | type State = 'hide' | 'show'; 7 | 8 | function Tooltip({ state }: { state: State }) { 9 | const tooltip = { 10 | maxWidth: '20rem', 11 | }; 12 | 13 | if (state === 'hide') return null; 14 | return ( 15 |
16 |
17 |
(Click to toggle between local and UTC)
18 |
19 | ); 20 | } 21 | 22 | export function TimestampToggle({ unixTimestamp, shorter }: { unixTimestamp: number; shorter?: boolean }) { 23 | const [isTimestampTypeUtc, toggleTimestampType] = useState(true); 24 | const [showTooltip, toggleTooltip] = useState('hide'); 25 | 26 | const toggle = () => { 27 | toggleTimestampType(!isTimestampTypeUtc); 28 | }; 29 | 30 | const tooltipContainer = { 31 | cursor: 'pointer', 32 | }; 33 | 34 | return ( 35 |
36 | toggleTooltip('show')} onMouseOut={() => toggleTooltip('hide')}> 37 | {isTimestampTypeUtc === true 38 | ? displayTimestampUtc(unixTimestamp, shorter) 39 | : displayTimestamp(unixTimestamp, shorter)} 40 | 41 | 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/components/common/TokenExtensionBadge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import { useCallback } from 'react'; 3 | 4 | import { ParsedTokenExtension } from '@/app/components/account/types'; 5 | import { StatusBadge } from '@/app/components/shared/StatusBadge'; 6 | import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/shared/ui/tooltip'; 7 | 8 | const badgeVariants = cva('', { 9 | defaultVariants: { 10 | size: 'sm', 11 | }, 12 | variants: { 13 | size: { 14 | sm: 'e-text-14', 15 | }, 16 | }, 17 | }); 18 | 19 | export function TokenExtensionBadge({ 20 | extension, 21 | label, 22 | onClick, 23 | size, 24 | }: { 25 | extension: ParsedTokenExtension; 26 | label?: string; 27 | onClick?: ({ extensionName }: { extensionName: ParsedTokenExtension['extension'] }) => void; 28 | } & VariantProps) { 29 | const { extension: extensionName, status, tooltip } = extension; 30 | 31 | const handleClick = useCallback(() => { 32 | onClick?.({ extensionName }); 33 | }, [extensionName, onClick]); 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | {tooltip && ( 41 | 42 |
{tooltip}
43 |
44 | )} 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/components/common/TokenExtensionBadges.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | 3 | import { ParsedTokenExtension } from '@/app/components/account/types'; 4 | import { cn } from '@/app/components/shared/utils'; 5 | 6 | import { TokenExtensionBadge } from './TokenExtensionBadge'; 7 | 8 | export function TokenExtensionBadges({ 9 | className, 10 | extensions, 11 | onClick, 12 | }: { 13 | className?: string; 14 | extensions: ParsedTokenExtension[]; 15 | onClick?: ComponentProps['onClick']; 16 | }) { 17 | return ( 18 |
19 | {extensions.map((extension, i) => ( 20 | 26 | ))} 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/components/common/VerifiedBadge.tsx: -------------------------------------------------------------------------------- 1 | import { VerifiableBuild } from '@utils/program-verification'; 2 | 3 | export function VerifiedBadge({ 4 | verifiableBuild, 5 | deploySlot, 6 | }: { 7 | verifiableBuild: VerifiableBuild; 8 | deploySlot: number; 9 | }) { 10 | if (verifiableBuild && verifiableBuild.verified_slot === deploySlot) { 11 | return ( 12 |

13 | 19 | {verifiableBuild.label}: Verified 20 | 21 |

22 | ); 23 | } else { 24 | return ( 25 |

26 | {verifiableBuild.label}: Unverified 27 |

28 | ); 29 | } 30 | } 31 | 32 | export function CheckingBadge() { 33 | return ( 34 |

35 | Checking 36 |

37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/components/common/inspector/AddressTableLookupAddress.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-foundation/explorer/09098b354e6d82a53e0f557936e9a7753b60f719/app/components/common/inspector/AddressTableLookupAddress.tsx -------------------------------------------------------------------------------- /app/components/common/inspector/UnknownDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { InspectorInstructionCard } from '@components/common/InspectorInstructionCard'; 2 | import { useCluster } from '@providers/cluster'; 3 | import { ParsedInstruction, SignatureResult, TransactionInstruction, VersionedMessage } from '@solana/web3.js'; 4 | import { getProgramName } from '@utils/tx'; 5 | import React from 'react'; 6 | 7 | export function UnknownDetailsCard({ 8 | ix, 9 | index, 10 | message, 11 | raw, 12 | result, 13 | innerCards, 14 | childIndex, 15 | InstructionCardComponent = InspectorInstructionCard, 16 | }: { 17 | childIndex?: number; 18 | index: number; 19 | innerCards?: JSX.Element[]; 20 | ix: TransactionInstruction | ParsedInstruction; 21 | message: VersionedMessage; 22 | raw: TransactionInstruction; 23 | result: SignatureResult; 24 | InstructionCardComponent?: React.FC[0]>; 25 | }) { 26 | const { cluster } = useCluster(); 27 | const programName = getProgramName(ix.programId.toBase58(), cluster); 28 | return ( 29 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/components/common/stories/TokenExtensionBadge.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { expect, fn, userEvent, within } from '@storybook/test'; 3 | 4 | import * as mockExtensions from '@/app/__tests__/mock-parsed-extensions-stubs'; 5 | import { populatePartialParsedTokenExtension } from '@/app/utils/token-extension'; 6 | 7 | import { TokenExtensionBadge } from '../TokenExtensionBadge'; 8 | 9 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 10 | const meta = { 11 | args: { 12 | onClick: fn(), 13 | }, 14 | component: TokenExtensionBadge, 15 | tags: ['autodocs'], 16 | title: 'Components/Common/TokenExtensionBadge', 17 | } satisfies Meta; 18 | 19 | export default meta; 20 | type Story = StoryObj; 21 | 22 | const extension = { 23 | extension: mockExtensions.transferFeeConfig0.extension, 24 | parsed: mockExtensions.transferFeeConfig0, 25 | ...populatePartialParsedTokenExtension(mockExtensions.transferFeeConfig0.extension), 26 | }; 27 | 28 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args 29 | export const Primary: Story = { 30 | args: { 31 | extension, 32 | }, 33 | async play({ canvasElement }) { 34 | const canvas = within(canvasElement); 35 | const tooltipButton = canvas.getByRole('button'); 36 | expect(tooltipButton).toHaveAttribute('data-slot', 'tooltip-trigger'); 37 | await userEvent.hover(tooltipButton); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /app/components/common/stories/TokenExtensionBadges.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { expect, within } from '@storybook/test'; 3 | 4 | import * as mockExtensions from '@/app/__tests__/mock-parsed-extensions-stubs'; 5 | import { populatePartialParsedTokenExtension } from '@/app/utils/token-extension'; 6 | 7 | import { TokenExtensionBadges } from '../TokenExtensionBadges'; 8 | 9 | const meta = { 10 | component: TokenExtensionBadges, 11 | tags: ['autodocs'], 12 | title: 'Components/Common/TokenExtensionBadges', 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | const extension = { 19 | extension: mockExtensions.transferFeeConfig0.extension, 20 | parsed: mockExtensions.transferFeeConfig0, 21 | ...populatePartialParsedTokenExtension(mockExtensions.transferFeeConfig0.extension), 22 | }; 23 | 24 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args 25 | export const Primary: Story = { 26 | args: { 27 | extensions: new Array(5).fill(null).map(() => extension), 28 | }, 29 | async play({ canvasElement }) { 30 | const canvas = within(canvasElement); 31 | const tooltipButton = canvas.getAllByRole('button'); 32 | expect(tooltipButton).toHaveLength(5); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /app/components/inspector/associated-token/AssociatedTokenDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component is the resemblace of instruction/associated-token/AssociatedTokenDetailsCard 3 | * 4 | * The main difference is that component accepts MessageCompiledInstruction to allow use accountKeyIndexes to resolve proper address from address-lookup-table 5 | */ 6 | import { UnknownDetailsCard } from '@components/common/inspector/UnknownDetailsCard'; 7 | import { ParsedInfo } from '@validators/index'; 8 | import React from 'react'; 9 | import { create } from 'superstruct'; 10 | 11 | import { CreateDetailsCard } from './CreateDetailsCard'; 12 | import { CreateIdempotentDetailsCard } from './CreateIdempotentDetailsCard'; 13 | import { RecoverNestedDetailsCard } from './RecoverNestedDetailsCard'; 14 | 15 | type DetailsProps = 16 | | Parameters[0] 17 | | Parameters[0] 18 | | Parameters[0]; 19 | 20 | export function AssociatedTokenDetailsCard(props: React.PropsWithChildren) { 21 | try { 22 | const parsed = create(props.ix.parsed, ParsedInfo); 23 | switch (parsed.type) { 24 | case 'create': { 25 | return ; 26 | } 27 | case 'createIdempotent': { 28 | return ; 29 | } 30 | case 'recoverNested': { 31 | return ; 32 | } 33 | default: 34 | return ; 35 | } 36 | } catch (_error) { 37 | return ; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/components/instruction/MemoDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import { ParsedInstruction, SignatureResult } from '@solana/web3.js'; 3 | import { wrap } from '@utils/index'; 4 | import React from 'react'; 5 | 6 | import { InstructionCard } from './InstructionCard'; 7 | 8 | export function MemoDetailsCard({ 9 | ix, 10 | index, 11 | result, 12 | innerCards, 13 | childIndex, 14 | }: { 15 | ix: ParsedInstruction; 16 | index: number; 17 | result: SignatureResult; 18 | innerCards?: JSX.Element[]; 19 | childIndex?: number; 20 | }) { 21 | const data = wrap(ix.parsed, 50); 22 | return ( 23 | 31 | 32 | Program 33 | 34 |
35 | 36 | 37 | 38 | 39 | Data (UTF-8) 40 | 41 |
{data}
42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/components/instruction/SignatureContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const SignatureContext = React.createContext(''); 4 | -------------------------------------------------------------------------------- /app/components/instruction/TokenLendingDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { useCluster } from '@providers/cluster'; 2 | import { SignatureResult, TransactionInstruction } from '@solana/web3.js'; 3 | import React from 'react'; 4 | 5 | import { InstructionCard } from './InstructionCard'; 6 | import { parseTokenLendingInstructionTitle } from './token-lending/types'; 7 | 8 | export function TokenLendingDetailsCard({ 9 | ix, 10 | index, 11 | result, 12 | signature, 13 | innerCards, 14 | childIndex, 15 | }: { 16 | ix: TransactionInstruction; 17 | index: number; 18 | result: SignatureResult; 19 | signature: string; 20 | innerCards?: JSX.Element[]; 21 | childIndex?: number; 22 | }) { 23 | const { url } = useCluster(); 24 | 25 | let title; 26 | try { 27 | title = parseTokenLendingInstructionTitle(ix); 28 | } catch (error) { 29 | console.error(error, { 30 | signature: signature, 31 | url: url, 32 | }); 33 | } 34 | 35 | return ( 36 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/components/instruction/TokenSwapDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { useCluster } from '@providers/cluster'; 2 | import { SignatureResult, TransactionInstruction } from '@solana/web3.js'; 3 | import React from 'react'; 4 | 5 | import { InstructionCard } from './InstructionCard'; 6 | import { parseTokenSwapInstructionTitle } from './token-swap/types'; 7 | 8 | export function TokenSwapDetailsCard({ 9 | ix, 10 | index, 11 | result, 12 | signature, 13 | innerCards, 14 | childIndex, 15 | }: { 16 | ix: TransactionInstruction; 17 | index: number; 18 | result: SignatureResult; 19 | signature: string; 20 | innerCards?: JSX.Element[]; 21 | childIndex?: number; 22 | }) { 23 | const { url } = useCluster(); 24 | 25 | let title; 26 | try { 27 | title = parseTokenSwapInstructionTitle(ix); 28 | } catch (error) { 29 | console.error(error, { 30 | signature: signature, 31 | url: url, 32 | }); 33 | } 34 | 35 | return ( 36 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/components/instruction/UnknownDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { useCluster } from '@providers/cluster'; 2 | import { ParsedInstruction, SignatureResult, TransactionInstruction } from '@solana/web3.js'; 3 | import { getProgramName } from '@utils/tx'; 4 | import React from 'react'; 5 | 6 | import { InstructionCard } from './InstructionCard'; 7 | 8 | export function UnknownDetailsCard({ 9 | ix, 10 | index, 11 | result, 12 | innerCards, 13 | childIndex, 14 | InstructionCardComponent = InstructionCard, 15 | }: { 16 | ix: TransactionInstruction | ParsedInstruction; 17 | index: number; 18 | result: SignatureResult; 19 | innerCards?: JSX.Element[]; 20 | childIndex?: number; 21 | InstructionCardComponent?: React.FC[0]>; 22 | }) { 23 | const { cluster } = useCluster(); 24 | const programName = getProgramName(ix.programId.toBase58(), cluster); 25 | return ( 26 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/components/instruction/WormholeDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { useCluster } from '@providers/cluster'; 2 | import { SignatureResult, TransactionInstruction } from '@solana/web3.js'; 3 | import React from 'react'; 4 | 5 | import { InstructionCard } from './InstructionCard'; 6 | import { parsWormholeInstructionTitle } from './wormhole/types'; 7 | 8 | export function WormholeDetailsCard({ 9 | ix, 10 | index, 11 | result, 12 | signature, 13 | innerCards, 14 | childIndex, 15 | }: { 16 | ix: TransactionInstruction; 17 | index: number; 18 | result: SignatureResult; 19 | signature: string; 20 | innerCards?: JSX.Element[]; 21 | childIndex?: number; 22 | }) { 23 | const { url } = useCluster(); 24 | 25 | let title; 26 | try { 27 | title = parsWormholeInstructionTitle(ix); 28 | } catch (error) { 29 | console.error(error, { 30 | signature: signature, 31 | url: url, 32 | }); 33 | } 34 | 35 | return ( 36 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/components/instruction/address-lookup-table/CloseLookupTableDetails.tsx: -------------------------------------------------------------------------------- 1 | import { AddressLookupTableProgram } from '@solana/web3.js'; 2 | 3 | import { Address } from '@/app/components/common/Address'; 4 | import { InstructionCard } from '@/app/components/instruction/InstructionCard'; 5 | import { InstructionDetailsProps } from '@/app/components/transaction/InstructionsSection'; 6 | 7 | import { CloseLookupTableInfo } from './types'; 8 | 9 | export function CloseLookupTableDetailsCard(props: InstructionDetailsProps & { info: CloseLookupTableInfo }) { 10 | const { ix, index, result, innerCards, childIndex, info } = props; 11 | return ( 12 | 20 | 21 | Program 22 | 23 |
24 | 25 | 26 | 27 | Lookup Table 28 | 29 |
30 | 31 | 32 | 33 | Lookup Table Authority 34 | 35 |
36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/components/instruction/address-lookup-table/DeactivateLookupTableDetails.tsx: -------------------------------------------------------------------------------- 1 | import { AddressLookupTableProgram } from '@solana/web3.js'; 2 | 3 | import { Address } from '@/app/components/common/Address'; 4 | import { InstructionCard } from '@/app/components/instruction/InstructionCard'; 5 | import { InstructionDetailsProps } from '@/app/components/transaction/InstructionsSection'; 6 | 7 | import { DeactivateLookupTableInfo } from './types'; 8 | 9 | export function DeactivateLookupTableDetailsCard(props: InstructionDetailsProps & { info: DeactivateLookupTableInfo }) { 10 | const { ix, index, result, innerCards, childIndex, info } = props; 11 | return ( 12 | 20 | 21 | Program 22 | 23 |
24 | 25 | 26 | 27 | Lookup Table 28 | 29 |
30 | 31 | 32 | 33 | Lookup Table Authority 34 | 35 |
36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/components/instruction/associated-token/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-redeclare */ 2 | 3 | import { PublicKeyFromString } from '@validators/pubkey'; 4 | import { enums, Infer, type } from 'superstruct'; 5 | 6 | export type CreateIdempotentInfo = Infer; 7 | export const CreateIdempotentInfo = type({ 8 | account: PublicKeyFromString, 9 | mint: PublicKeyFromString, 10 | source: PublicKeyFromString, 11 | systemProgram: PublicKeyFromString, 12 | tokenProgram: PublicKeyFromString, 13 | wallet: PublicKeyFromString, 14 | }); 15 | 16 | export type RecoverNestedInfo = Infer; 17 | export const RecoverNestedInfo = type({ 18 | destination: PublicKeyFromString, 19 | nestedMint: PublicKeyFromString, 20 | nestedOwner: PublicKeyFromString, 21 | nestedSource: PublicKeyFromString, 22 | ownerMint: PublicKeyFromString, 23 | tokenProgram: PublicKeyFromString, 24 | wallet: PublicKeyFromString, 25 | }); 26 | 27 | export type SystemInstructionType = Infer; 28 | export const SystemInstructionType = enums(['create', 'createIdempotent', 'recoverNested']); 29 | -------------------------------------------------------------------------------- /app/components/instruction/bpf-loader/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-redeclare */ 2 | 3 | import { PublicKeyFromString } from '@validators/pubkey'; 4 | import { enums, Infer, number, string, type } from 'superstruct'; 5 | 6 | export type WriteInfo = Infer; 7 | export const WriteInfo = type({ 8 | account: PublicKeyFromString, 9 | bytes: string(), 10 | offset: number(), 11 | }); 12 | 13 | export type FinalizeInfo = Infer; 14 | export const FinalizeInfo = type({ 15 | account: PublicKeyFromString, 16 | }); 17 | 18 | export type BpfLoaderInstructionType = Infer; 19 | export const BpfLoaderInstructionType = enums(['write', 'finalize']); 20 | -------------------------------------------------------------------------------- /app/components/instruction/codama/getCodamaIdl.ts: -------------------------------------------------------------------------------- 1 | import { fetchMetadataFromSeeds, unpackAndFetchData } from '@solana-program/program-metadata'; 2 | import { address, createSolanaRpc, mainnet } from 'web3js-experimental'; 3 | 4 | export async function getCodamaIdl(programAddress: string, url: string) { 5 | const rpc = createSolanaRpc(mainnet(url)); 6 | let metadata; 7 | 8 | try { 9 | // @ts-expect-error RPC types mismatch 10 | metadata = await fetchMetadataFromSeeds(rpc, { 11 | authority: null, 12 | program: address(programAddress), 13 | seed: 'idl', 14 | }); 15 | } catch (error) { 16 | console.error('Metadata fetch failed', error); 17 | throw new Error('Metadata fetch failed'); 18 | } 19 | try { 20 | // @ts-expect-error RPC types mismatch 21 | const content = await unpackAndFetchData({ rpc, ...metadata.data }); 22 | const parsed = JSON.parse(content); 23 | return parsed; 24 | } catch (error) { 25 | throw new Error('JSON parse failed'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/components/instruction/ed25519/types.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, TransactionInstruction } from '@solana/web3.js'; 2 | import { PublicKeyFromString } from '@validators/pubkey'; 3 | import { Infer, type } from 'superstruct'; 4 | 5 | const PROGRAM_ADDRESS = 'Ed25519SigVerify111111111111111111111111111'; 6 | export const PROGRAM_ID = new PublicKey(PROGRAM_ADDRESS); 7 | 8 | export type Ed25519Info = Infer; 9 | export const Ed25519Info = type({ 10 | account: PublicKeyFromString, 11 | }); 12 | 13 | export function isEd25519Instruction(instruction: TransactionInstruction): boolean { 14 | return PROGRAM_ADDRESS === instruction.programId.toBase58(); 15 | } 16 | -------------------------------------------------------------------------------- /app/components/instruction/lighthouse/types.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInstruction } from '@solana/web3.js'; 2 | 3 | export const LIGHTHOUSE_ADDRESS = 'L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95'; 4 | 5 | export function isLighthouseInstruction(instruction: TransactionInstruction): boolean { 6 | return instruction.programId.toBase58() === LIGHTHOUSE_ADDRESS; 7 | } 8 | -------------------------------------------------------------------------------- /app/components/instruction/mango/AddOracleDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { SignatureResult, TransactionInstruction } from '@solana/web3.js'; 2 | 3 | import { InstructionCard } from '../InstructionCard'; 4 | 5 | export function AddOracleDetailsCard(props: { 6 | ix: TransactionInstruction; 7 | index: number; 8 | result: SignatureResult; 9 | innerCards?: JSX.Element[]; 10 | childIndex?: number; 11 | }) { 12 | const { ix, index, result, innerCards, childIndex } = props; 13 | 14 | return ( 15 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/components/instruction/mango/ConsumeEventsDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import { SignatureResult, TransactionInstruction } from '@solana/web3.js'; 3 | 4 | import { InstructionCard } from '../InstructionCard'; 5 | import { getPerpMarketFromInstruction } from './types'; 6 | 7 | export function ConsumeEventsDetailsCard(props: { 8 | ix: TransactionInstruction; 9 | index: number; 10 | result: SignatureResult; 11 | innerCards?: JSX.Element[]; 12 | childIndex?: number; 13 | }) { 14 | const { ix, index, result, innerCards, childIndex } = props; 15 | 16 | const perpMarketAccountMeta = ix.keys[2]; 17 | const mangoPerpMarketConfig = getPerpMarketFromInstruction(ix, perpMarketAccountMeta); 18 | 19 | return ( 20 | 28 | {mangoPerpMarketConfig !== undefined && ( 29 | 30 | Perp market 31 | {mangoPerpMarketConfig.name} 32 | 33 | )} 34 | 35 | 36 | Perp market address 37 | 38 |
39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /app/components/instruction/mango/GenericMngoAccountDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import { SignatureResult, TransactionInstruction } from '@solana/web3.js'; 3 | 4 | import { InstructionCard } from '../InstructionCard'; 5 | 6 | export function GenericMngoAccountDetailsCard(props: { 7 | ix: TransactionInstruction; 8 | index: number; 9 | result: SignatureResult; 10 | mangoAccountKeyLocation: number; 11 | title: string; 12 | innerCards?: JSX.Element[]; 13 | childIndex?: number; 14 | }) { 15 | const { ix, index, result, mangoAccountKeyLocation, title, innerCards, childIndex } = props; 16 | const mangoAccount = ix.keys[mangoAccountKeyLocation]; 17 | 18 | return ( 19 | 27 | 28 | Mango account 29 | 30 |
31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /app/components/instruction/pyth/types.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInstruction } from '@solana/web3.js'; 2 | 3 | export const PROGRAM_IDS: string[] = [ 4 | 'gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s', // devnet 5 | '8tfDNiaEyrV6Q1U4DEXrEigs9DoDtkugzFbybENEbCDz', // testnet 6 | 'FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH', // mainnet 7 | ]; 8 | 9 | export function isPythInstruction(instruction: TransactionInstruction): boolean { 10 | return PROGRAM_IDS.includes(instruction.programId.toBase58()); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/instruction/serum/DisableMarketDetails.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import React from 'react'; 3 | 4 | import { InstructionCard } from '../InstructionCard'; 5 | import { DisableMarket, SerumIxDetailsProps } from './types'; 6 | 7 | export function DisableMarketDetailsCard(props: SerumIxDetailsProps) { 8 | const { ix, index, result, programName, info, innerCards, childIndex } = props; 9 | 10 | return ( 11 | 19 | 20 | Program 21 | 22 |
23 | 24 | 25 | 26 | 27 | Market 28 | 29 |
30 | 31 | 32 | 33 | 34 | Disable Authority 35 | 36 |
37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/components/instruction/stake/DeactivateDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import { ParsedInstruction, SignatureResult, StakeProgram } from '@solana/web3.js'; 3 | import React from 'react'; 4 | 5 | import { InstructionCard } from '../InstructionCard'; 6 | import { DeactivateInfo } from './types'; 7 | 8 | export function DeactivateDetailsCard(props: { 9 | ix: ParsedInstruction; 10 | index: number; 11 | result: SignatureResult; 12 | info: DeactivateInfo; 13 | innerCards?: JSX.Element[]; 14 | childIndex?: number; 15 | }) { 16 | const { ix, index, result, info, innerCards, childIndex } = props; 17 | 18 | return ( 19 | 27 | 28 | Program 29 | 30 |
31 | 32 | 33 | 34 | 35 | Stake Address 36 | 37 |
38 | 39 | 40 | 41 | 42 | Authority Address 43 | 44 |
45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/components/instruction/system/AllocateDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import { ParsedInstruction, SignatureResult, SystemProgram } from '@solana/web3.js'; 3 | import React from 'react'; 4 | 5 | import { InstructionCard } from '../InstructionCard'; 6 | import { AllocateInfo } from './types'; 7 | 8 | export function AllocateDetailsCard(props: { 9 | ix: ParsedInstruction; 10 | index: number; 11 | result: SignatureResult; 12 | info: AllocateInfo; 13 | innerCards?: JSX.Element[]; 14 | childIndex?: number; 15 | }) { 16 | const { ix, index, result, info, innerCards, childIndex } = props; 17 | 18 | return ( 19 | 27 | 28 | Program 29 | 30 |
31 | 32 | 33 | 34 | 35 | Account Address 36 | 37 |
38 | 39 | 40 | 41 | 42 | Allocated Data Size 43 | {info.space} byte(s) 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /app/components/instruction/system/AssignDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import { ParsedInstruction, SignatureResult, SystemProgram } from '@solana/web3.js'; 3 | import React from 'react'; 4 | 5 | import { InstructionCard } from '../InstructionCard'; 6 | import { AssignInfo } from './types'; 7 | 8 | export function AssignDetailsCard(props: { 9 | ix: ParsedInstruction; 10 | index: number; 11 | result: SignatureResult; 12 | info: AssignInfo; 13 | innerCards?: JSX.Element[]; 14 | childIndex?: number; 15 | }) { 16 | const { ix, index, result, info, innerCards, childIndex } = props; 17 | 18 | return ( 19 | 27 | 28 | Program 29 | 30 |
31 | 32 | 33 | 34 | 35 | Account Address 36 | 37 |
38 | 39 | 40 | 41 | 42 | Assigned Program Id 43 | 44 |
45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/components/instruction/system/NonceAdvanceDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import { ParsedInstruction, SignatureResult, SystemProgram } from '@solana/web3.js'; 3 | import React from 'react'; 4 | 5 | import { InstructionCard } from '../InstructionCard'; 6 | import { AdvanceNonceInfo } from './types'; 7 | 8 | export function NonceAdvanceDetailsCard(props: { 9 | ix: ParsedInstruction; 10 | index: number; 11 | result: SignatureResult; 12 | info: AdvanceNonceInfo; 13 | innerCards?: JSX.Element[]; 14 | childIndex?: number; 15 | }) { 16 | const { ix, index, result, info, innerCards, childIndex } = props; 17 | 18 | return ( 19 | 27 | 28 | Program 29 | 30 |
31 | 32 | 33 | 34 | 35 | Nonce Address 36 | 37 |
38 | 39 | 40 | 41 | 42 | Authority Address 43 | 44 |
45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/components/instruction/system/UpgradeNonceDetailsCard.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@components/common/Address'; 2 | import { ParsedInstruction, SignatureResult, SystemProgram } from '@solana/web3.js'; 3 | import React from 'react'; 4 | 5 | import { InstructionCard } from '../InstructionCard'; 6 | import { UpgradeNonceInfo } from './types'; 7 | 8 | export function UpgradeNonceDetailsCard(props: { 9 | ix: ParsedInstruction; 10 | index: number; 11 | result: SignatureResult; 12 | info: UpgradeNonceInfo; 13 | innerCards?: JSX.Element[]; 14 | childIndex?: number; 15 | }) { 16 | const { ix, index, result, info, innerCards, childIndex } = props; 17 | 18 | return ( 19 | 27 | 28 | Program 29 | 30 |
31 | 32 | 33 | 34 | 35 | Nonce Address 36 | 37 |
38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/components/instruction/token-lending/types.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInstruction } from '@solana/web3.js'; 2 | 3 | export const PROGRAM_IDS: string[] = [ 4 | 'LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi', // mainnet / testnet / devnet 5 | ]; 6 | 7 | const INSTRUCTION_LOOKUP: { [key: number]: string } = { 8 | 0: 'Initialize Lending Market', 9 | 1: 'Initialize Reserve', 10 | 2: 'Initialize Obligation', 11 | 3: 'Reserve Deposit', 12 | 4: 'Reserve Withdraw', 13 | 5: 'Borrow', 14 | 6: 'Repay Loan', 15 | 7: 'Liquidate Loan', 16 | 8: 'Accrue Interest', 17 | }; 18 | 19 | export function isTokenLendingInstruction(instruction: TransactionInstruction): boolean { 20 | return PROGRAM_IDS.includes(instruction.programId.toBase58()); 21 | } 22 | 23 | export function parseTokenLendingInstructionTitle(instruction: TransactionInstruction): string { 24 | const code = instruction.data[0]; 25 | 26 | if (!(code in INSTRUCTION_LOOKUP)) { 27 | throw new Error(`Unrecognized Token Lending instruction code: ${code}`); 28 | } 29 | 30 | return INSTRUCTION_LOOKUP[code]; 31 | } 32 | -------------------------------------------------------------------------------- /app/components/instruction/token-swap/types.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInstruction } from '@solana/web3.js'; 2 | 3 | export const PROGRAM_IDS: string[] = [ 4 | 'SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8', // mainnet / testnet / devnet 5 | '9qvG1zUp8xF1Bi4m6UdRNby1BAAuaDrUxSpv4CmRRMjL', // mainnet - legacy 6 | '2n2dsFSgmPcZ8jkmBZLGUM2nzuFqcBGQ3JEEj6RJJcEg', // testnet - legacy 7 | '9tdctNJuFsYZ6VrKfKEuwwbPp4SFdFw3jYBZU8QUtzeX', // testnet - legacy 8 | 'CrRvVBS4Hmj47TPU3cMukurpmCUYUrdHYxTQBxncBGqw', // testnet - legacy 9 | 'BSfTAcBdqmvX5iE2PW88WFNNp2DHhLUaBKk5WrnxVkcJ', // devnet - legacy 10 | 'H1E1G7eD5Rrcy43xvDxXCsjkRggz7MWNMLGJ8YNzJ8PM', // devnet - legacy 11 | 'CMoteLxSPVPoc7Drcggf3QPg3ue8WPpxYyZTg77UGqHo', // devnet - legacy 12 | 'EEuPz4iZA5reBUeZj6x1VzoiHfYeHMppSCnHZasRFhYo', // devnet - legacy 13 | '5rdpyt5iGfr68qt28hkefcFyF4WtyhTwqKDmHSBG8GZx', // localnet 14 | ]; 15 | 16 | const INSTRUCTION_LOOKUP: { [key: number]: string } = { 17 | 0: 'Initialize Swap', 18 | 1: 'Swap', 19 | 2: 'Deposit All Token Types', 20 | 3: 'Withdraw All Token Types', 21 | 4: 'Deposit Single Token Type Exact Amount In', 22 | 5: 'Withdraw Single Token Type Exact Amount Out', 23 | }; 24 | 25 | export function isTokenSwapInstruction(instruction: TransactionInstruction): boolean { 26 | return PROGRAM_IDS.includes(instruction.programId.toBase58()); 27 | } 28 | 29 | export function parseTokenSwapInstructionTitle(instruction: TransactionInstruction): string { 30 | const code = instruction.data[0]; 31 | 32 | if (!(code in INSTRUCTION_LOOKUP)) { 33 | throw new Error(`Unrecognized Token Swap instruction code: ${code}`); 34 | } 35 | 36 | return INSTRUCTION_LOOKUP[code]; 37 | } 38 | -------------------------------------------------------------------------------- /app/components/instruction/wormhole/types.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInstruction } from '@solana/web3.js'; 2 | 3 | export const PROGRAM_IDS: string[] = [ 4 | 'WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC', // mainnet / testnet / devnet 5 | ]; 6 | 7 | const INSTRUCTION_LOOKUP: { [key: number]: string } = { 8 | 0: 'Initialize Bridge', 9 | 1: 'Transfer Assets Out', 10 | 2: 'Post VAA', 11 | 3: 'Evict Transfer Proposal', 12 | 4: 'Evict Claimed VAA', 13 | 5: 'Poke Proposal', 14 | 6: 'Verify Signatures', 15 | 7: 'Create Wrapped Asset', 16 | }; 17 | 18 | export function isWormholeInstruction(instruction: TransactionInstruction): boolean { 19 | return PROGRAM_IDS.includes(instruction.programId.toBase58()); 20 | } 21 | 22 | export function parsWormholeInstructionTitle(instruction: TransactionInstruction): string { 23 | const code = instruction.data[0]; 24 | 25 | if (!(code in INSTRUCTION_LOOKUP)) { 26 | throw new Error(`Unrecognized Wormhole instruction code: ${code}`); 27 | } 28 | 29 | return INSTRUCTION_LOOKUP[code]; 30 | } 31 | -------------------------------------------------------------------------------- /app/components/shared/ErrorCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { cn } from './utils'; 4 | 5 | export function ErrorCard({ className, message }: React.HTMLAttributes & { message?: string }) { 6 | return ( 7 |
8 |
{message || 'Error'}
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/components/shared/LoadingCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { cn } from './utils'; 4 | 5 | export function LoadingCard({ className, message }: React.HTMLAttributes & { message?: string }) { 6 | return ( 7 |
8 |
9 | 10 | {message || 'Loading'} 11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/components/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /app/epoch/[epoch]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { EpochProvider } from '@providers/epoch'; 2 | import { PropsWithChildren } from 'react'; 3 | 4 | export default function EpochLayout({ children }: PropsWithChildren>) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /app/epoch/[epoch]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next/types'; 2 | 3 | import EpochDetailsPageClient from './page-client'; 4 | 5 | type Props = Readonly<{ 6 | params: { 7 | epoch: string; 8 | }; 9 | }>; 10 | 11 | export async function generateMetadata({ params: { epoch } }: Props): Promise { 12 | return { 13 | description: `Summary of ${epoch} on Solana`, 14 | title: `Epoch | ${epoch} | Solana`, 15 | }; 16 | } 17 | 18 | export default function EpochDetailsPage(props: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/feature-gates/page.tsx: -------------------------------------------------------------------------------- 1 | import FeatureGatesPageClient from './page-client'; 2 | 3 | export const metadata = { 4 | description: `Overview of the feature gates on Solana`, 5 | title: `Feature Gates | Solana`, 6 | }; 7 | 8 | export default function FeatureGatesPage() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /app/features/metadata/__tests__/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | import { getProxiedUri } from '../utils'; 4 | 5 | describe('getProxiedUri', () => { 6 | const originalEnv = process.env; 7 | 8 | beforeEach(() => { 9 | vi.resetModules(); 10 | process.env = { ...originalEnv }; 11 | }); 12 | 13 | afterAll(() => { 14 | process.env = originalEnv; 15 | }); 16 | 17 | it('returns the original URI when proxy is not enabled', () => { 18 | process.env.NEXT_PUBLIC_METADATA_ENABLED = 'false'; 19 | const uri = 'http://example.com'; 20 | expect(getProxiedUri(uri)).toBe(uri); 21 | }); 22 | 23 | it('returns the original URI for non-http/https protocols', () => { 24 | process.env.NEXT_PUBLIC_METADATA_ENABLED = 'true'; 25 | const uri = 'ftp://example.com'; 26 | expect(getProxiedUri(uri)).toBe(uri); 27 | }); 28 | 29 | it('returns proxied URI when proxy is enabled and protocol is http', () => { 30 | process.env.NEXT_PUBLIC_METADATA_ENABLED = 'true'; 31 | const uri = 'http://example.com'; 32 | expect(getProxiedUri(uri)).toBe('/api/metadata/proxy?uri=http%3A%2F%2Fexample.com'); 33 | }); 34 | 35 | it('returns proxied URI when proxy is enabled and protocol is https', () => { 36 | process.env.NEXT_PUBLIC_METADATA_ENABLED = 'true'; 37 | const uri = 'https://example.com'; 38 | expect(getProxiedUri(uri)).toBe('/api/metadata/proxy?uri=https%3A%2F%2Fexample.com'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /app/features/metadata/utils.ts: -------------------------------------------------------------------------------- 1 | export const getProxiedUri = (uri: string): string => { 2 | const isProxyEnabled = process.env.NEXT_PUBLIC_METADATA_ENABLED === 'true'; 3 | 4 | if (!isProxyEnabled) return uri; 5 | 6 | const url = new URL(uri); 7 | 8 | if (!['http:', 'https:'].includes(url.protocol)) return uri; 9 | 10 | return `/api/metadata/proxy?uri=${encodeURIComponent(uri)}`; 11 | }; 12 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-foundation/explorer/09098b354e6d82a53e0f557936e9a7753b60f719/app/globals.css -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorCard } from '@components/common/ErrorCard'; 2 | 3 | export default function NotFoundPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-foundation/explorer/09098b354e6d82a53e0f557936e9a7753b60f719/app/opengraph-image.png -------------------------------------------------------------------------------- /app/providers/accounts/utils/getEditionInfo.ts: -------------------------------------------------------------------------------- 1 | import { programs } from '@metaplex/js'; 2 | import { Connection } from '@solana/web3.js'; 3 | 4 | const { 5 | metadata: { Metadata, MasterEdition, MetadataKey }, 6 | } = programs; 7 | 8 | type MasterEditionData = programs.metadata.MasterEditionV1Data | programs.metadata.MasterEditionV2Data; 9 | type EditionData = programs.metadata.EditionData; 10 | 11 | export type EditionInfo = { 12 | masterEdition?: MasterEditionData; 13 | edition?: EditionData; 14 | }; 15 | 16 | export default async function getEditionInfo( 17 | metadata: programs.metadata.Metadata, 18 | connection: Connection 19 | ): Promise { 20 | try { 21 | const edition = (await Metadata.getEdition(connection, metadata.data.mint)).data; 22 | 23 | if (edition) { 24 | if (edition.key === MetadataKey.MasterEditionV1 || edition.key === MetadataKey.MasterEditionV2) { 25 | return { 26 | edition: undefined, 27 | masterEdition: edition as MasterEditionData, 28 | }; 29 | } 30 | 31 | // This is an Edition NFT. Pull the Parent (MasterEdition) 32 | const masterEdition = (await MasterEdition.load(connection, (edition as EditionData).parent)).data; 33 | if (masterEdition) { 34 | return { 35 | edition: edition as EditionData, 36 | masterEdition, 37 | }; 38 | } 39 | } 40 | } catch { 41 | /* ignore */ 42 | } 43 | 44 | return { 45 | edition: undefined, 46 | masterEdition: undefined, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /app/providers/accounts/utils/isMetaplexNFT.ts: -------------------------------------------------------------------------------- 1 | import { MintAccountInfo } from '@validators/accounts/token'; 2 | 3 | import { isTokenProgramData, ParsedData, TokenProgramData } from '..'; 4 | 5 | export default function isMetaplexNFT( 6 | parsedData?: ParsedData, 7 | mintInfo?: MintAccountInfo 8 | ): parsedData is TokenProgramData { 9 | return !!( 10 | parsedData && 11 | isTokenProgramData(parsedData) && 12 | parsedData.parsed.type === 'mint' && 13 | parsedData.nftData && 14 | mintInfo?.decimals === 0 && 15 | (parseInt(mintInfo.supply) === 1 || parsedData.nftData.metadata.tokenStandard === 1) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/providers/accounts/vote-accounts.tsx: -------------------------------------------------------------------------------- 1 | import { useCluster } from '@providers/cluster'; 2 | import { Cluster } from '@utils/cluster'; 3 | import React from 'react'; 4 | import { createSolanaRpc } from 'web3js-experimental'; 5 | 6 | type VoteAccountInfo = Readonly<{ 7 | activatedStake: bigint; 8 | }>; 9 | 10 | type VoteAccounts = Readonly<{ 11 | current: VoteAccountInfo[]; 12 | delinquent: VoteAccountInfo[]; 13 | }>; 14 | 15 | async function fetchVoteAccounts( 16 | cluster: Cluster, 17 | url: string, 18 | setVoteAccounts: React.Dispatch> 19 | ) { 20 | try { 21 | const rpc = createSolanaRpc(url); 22 | 23 | const voteAccountsResponse = await rpc.getVoteAccounts({ commitment: 'confirmed' }).send(); 24 | const voteAccounts: VoteAccounts = { 25 | current: voteAccountsResponse.current.map(c => ({ activatedStake: c.activatedStake })), 26 | delinquent: voteAccountsResponse.delinquent.map(d => ({ activatedStake: d.activatedStake })), 27 | }; 28 | 29 | setVoteAccounts(voteAccounts); 30 | } catch (error) { 31 | if (cluster !== Cluster.Custom) { 32 | console.error(error, { url }); 33 | } 34 | } 35 | } 36 | 37 | export function useVoteAccounts() { 38 | const [voteAccounts, setVoteAccounts] = React.useState(); 39 | const { cluster, url } = useCluster(); 40 | 41 | return { 42 | fetchVoteAccounts: () => fetchVoteAccounts(cluster, url, setVoteAccounts), 43 | voteAccounts, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /app/providers/stats/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SolanaClusterStatsProvider } from './solanaClusterStats'; 4 | 5 | type Props = { children: React.ReactNode }; 6 | export function StatsProvider({ children }: Props) { 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /app/providers/useCodamaIdl.tsx: -------------------------------------------------------------------------------- 1 | import { fetch } from 'cross-fetch'; 2 | import useSWRImmutable from 'swr/immutable'; 3 | 4 | import { getCodamaIdl } from '../components/instruction/codama/getCodamaIdl'; 5 | import { Cluster } from '../utils/cluster'; 6 | 7 | const PMP_IDL_ENABLED = process.env.NEXT_PUBLIC_PMP_IDL_ENABLED === 'true'; 8 | 9 | export function useCodamaIdl(programAddress: string, url: string, cluster: Cluster) { 10 | const { data } = useSWRImmutable(`codama-idl-${programAddress}-${url}`, async () => { 11 | if (!PMP_IDL_ENABLED) { 12 | return null; 13 | } 14 | 15 | try { 16 | const response = await fetch(`/api/codama?programAddress=${programAddress}&cluster=${cluster}`); 17 | if (response.ok) { 18 | return response.json().then(data => data.codamaIdl); 19 | } 20 | // Only attempt to fetch client side if the url is localhost or 127.0.0.1 21 | if (new URL(url).hostname === 'localhost' || new URL(url).hostname === '127.0.0.1') { 22 | return getCodamaIdl(programAddress, url); 23 | } 24 | return null; 25 | } catch (error) { 26 | console.error('Error fetching codama idl', error); 27 | return null; 28 | } 29 | }); 30 | return { codamaIdl: data }; 31 | } 32 | -------------------------------------------------------------------------------- /app/scss/_solana-dark-overrides.scss: -------------------------------------------------------------------------------- 1 | // 2 | // solana.scss 3 | // Use this to write your custom SCSS 4 | // 5 | 6 | code, 7 | pre { 8 | background-color: $black-dark; 9 | color: $white; 10 | } 11 | 12 | ul.log-messages { 13 | background-color: $black-dark; 14 | color: $white; 15 | } 16 | 17 | .form-control { 18 | border-color: $input-border-color; 19 | } 20 | 21 | .input-group .input-group-text { 22 | border-color: $input-border-color; 23 | } 24 | 25 | .navbar { 26 | border-bottom: 1px solid $card-outline-color; 27 | } 28 | 29 | .search-bar__placeholder { 30 | color: $input-placeholder-color !important; 31 | } 32 | 33 | .search-bar__control { 34 | background-color: $gray-800-dark !important; 35 | border-color: $card-outline-color !important; 36 | box-shadow: $card-box-shadow !important; 37 | 38 | .search-bar__input { 39 | color: $white !important; 40 | } 41 | } 42 | 43 | .search-bar__menu { 44 | background-color: $gray-800-dark !important; 45 | border: 1px solid $card-outline-color !important; 46 | box-shadow: $card-box-shadow !important; 47 | 48 | .search-bar__option { 49 | cursor: pointer; 50 | 51 | &.search-bar__option--is-focused, 52 | &:hover { 53 | background-color: $gray-700-dark !important; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/scss/_solana-variables-dark.scss: -------------------------------------------------------------------------------- 1 | // 2 | // solana-variables.scss 3 | // Use this to overwrite Bootstrap and Dashkit variables 4 | // 5 | 6 | // Example of a variable override to change Dashkit's background color 7 | // Remove the "//" to comment it in and see it in action! 8 | $body-bg: #161b19 !default; 9 | 10 | $black: #232323; 11 | $white: #fff !default; 12 | $gray-600: #86b8b6; 13 | $gray-600-dark: #343a37 !default; 14 | $gray-700-dark: #282d2b !default; 15 | $gray-800-dark: #1e2423 !default; 16 | $black-dark: #141816 !default; 17 | $input-placeholder-color: #ccc !default; 18 | 19 | $primary: #1dd79b !default; 20 | $success: #26e97e !default; 21 | $warning: #fa62fc !default; 22 | $card-border-color: $gray-700-dark !default; 23 | $card-outline-color: #111 !default; 24 | 25 | $input-bg: transparent !default; 26 | $input-border-color: $gray-600-dark !default; 27 | $input-group-addon-color: $white !default; 28 | 29 | $theme-colors: ( 30 | 'black': $black, 31 | 'gray': $gray-600, 32 | 'gray-dark': $gray-800-dark, 33 | ); 34 | -------------------------------------------------------------------------------- /app/scss/dashkit/_alert.scss: -------------------------------------------------------------------------------- 1 | // 2 | // alerts 3 | // Extended from Bootstrap 4 | // 5 | 6 | // 7 | // Bootstrap Overrides ===================================== 8 | // 9 | 10 | // Allow for a text-decoration since links are the same color as the alert text. 11 | 12 | .alert-link { 13 | text-decoration: $alert-link-text-decoration; 14 | } 15 | 16 | // Color variants 17 | // 18 | // Using Bootstrap's core alert-variant mixin to generate solid background color + yiq colorized text (and making close/links match those colors) 19 | 20 | @each $color, $value in $theme-colors { 21 | .alert-#{$color} { 22 | @include alert-variant( 23 | shift-color($value, $alert-bg-scale), 24 | shift-color($value, $alert-border-scale), 25 | color-contrast(shift-color($value, $alert-bg-scale)) 26 | ); 27 | 28 | // Divider 29 | hr { 30 | background-color: darken(shift-color($value, $alert-border-scale), 5%); 31 | } 32 | 33 | // Close 34 | .btn-close { 35 | padding: calc(#{$alert-padding-y} + #{$btn-close-padding-y}) $alert-padding-x; 36 | background-image: escape-svg( 37 | url("data:image/svg+xml,") 38 | ); 39 | } 40 | 41 | // Link 42 | .alert-link { 43 | color: color-contrast(shift-color($value, $alert-bg-scale)); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/scss/dashkit/_badge.scss: -------------------------------------------------------------------------------- 1 | // 2 | // badge.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | // 7 | // Bootstrap Overrides ===================================== 8 | // 9 | 10 | .badge { 11 | vertical-align: middle; 12 | } 13 | 14 | // Quick fix for badges in buttons 15 | .btn .badge { 16 | top: -2px; 17 | } 18 | 19 | // Pills 20 | 21 | .badge.rounded-pill { 22 | padding-right: $border-radius-pill-padding-x; 23 | padding-left: $border-radius-pill-padding-x; 24 | } 25 | 26 | // Text color 27 | // 28 | // Replacing the default white text color 29 | 30 | @each $color, $value in $theme-colors { 31 | .badge.bg-#{$color} { 32 | color: color-contrast($value); 33 | } 34 | } 35 | 36 | // 37 | // Theme ===================================== 38 | // 39 | 40 | // Creates the "soft" badge variant 41 | @each $color, $value in $theme-colors { 42 | .badge.bg-#{$color}-soft { 43 | @include badge-variant-soft(shift-color($value, $bg-soft-scale), $value); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/scss/dashkit/_breadcrumb.scss: -------------------------------------------------------------------------------- 1 | // 2 | // breadcrumb.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | // 7 | // Bootstrap Overrides ===================================== 8 | // 9 | 10 | .breadcrumb-item { 11 | + .breadcrumb-item::before { 12 | content: '\e930'; 13 | align-self: center; 14 | font-size: 0.8rem; 15 | font-family: 'Feather'; 16 | color: $breadcrumb-divider-color; 17 | } 18 | } 19 | 20 | // 21 | // Theme ===================================== 22 | // 23 | 24 | // Small 25 | // 26 | // Reduces font size 27 | 28 | .breadcrumb-sm { 29 | font-size: $breadcrumb-font-size-sm; 30 | } 31 | 32 | // Overflow 33 | // 34 | // Allows the breadcrumb to be overflown horizontally 35 | 36 | .breadcrumb-overflow { 37 | display: flex; 38 | flex-direction: row; 39 | flex-wrap: nowrap; 40 | overflow-x: auto; 41 | 42 | &::-webkit-scrollbar { 43 | display: none; 44 | } 45 | } 46 | 47 | .breadcrumb-overflow .breadcrumb-item { 48 | white-space: nowrap; 49 | } 50 | -------------------------------------------------------------------------------- /app/scss/dashkit/_chart.scss: -------------------------------------------------------------------------------- 1 | // 2 | // chart.scss 3 | // Dashkit component 4 | // 5 | 6 | // Chart 7 | // 8 | // General styles 9 | 10 | .chart { 11 | position: relative; 12 | height: $chart-height; 13 | } 14 | 15 | .chart.chart-appended { 16 | height: calc(#{$chart-height} - #{$chart-legend-height}); 17 | } 18 | 19 | .chart-sm { 20 | height: $chart-height-sm; 21 | } 22 | 23 | .chart-sm.chart-appended { 24 | height: calc(#{$chart-height-sm} - #{$chart-legend-height}); 25 | } 26 | 27 | // Sparkline 28 | 29 | .chart-sparkline { 30 | width: $chart-sparkline-width; 31 | height: $chart-sparkline-height; 32 | } 33 | 34 | // Legend 35 | // 36 | // Custom legend 37 | 38 | .chart-legend { 39 | display: flex; 40 | justify-content: center; 41 | margin-top: $chart-legend-margin-top; 42 | font-size: $chart-legend-font-size; 43 | text-align: center; 44 | color: $chart-legend-color; 45 | } 46 | 47 | .chart-legend-item { 48 | display: inline-flex; 49 | align-items: center; 50 | 51 | + .chart-legend-item { 52 | margin-left: 1rem; 53 | } 54 | } 55 | 56 | .chart-legend-indicator { 57 | display: inline-block; 58 | width: 0.5rem; 59 | height: 0.5rem; 60 | margin-right: 0.375rem; 61 | border-radius: 50%; 62 | } 63 | 64 | // Tooltip 65 | // 66 | // Custom tooltip 67 | 68 | #chart-tooltip { 69 | z-index: 0; 70 | } 71 | 72 | #chart-tooltip .popover-arrow { 73 | top: 100%; 74 | left: 50%; 75 | transform: translateX(-50%) translateX(-0.5rem); 76 | } 77 | -------------------------------------------------------------------------------- /app/scss/dashkit/_checklist.scss: -------------------------------------------------------------------------------- 1 | // 2 | // checklist.scss 3 | // Dashkit component 4 | // 5 | 6 | .checklist { 7 | outline: none; 8 | } 9 | 10 | .checklist .form-check { 11 | outline: none; 12 | user-select: none; 13 | } 14 | 15 | .checklist .form-check + .form-check { 16 | margin-top: $checklist-control-spacer; 17 | } 18 | 19 | .checklist .form-check:first-child[style*='display: none'] + .form-check { 20 | margin-top: 0; 21 | } 22 | 23 | .checklist .form-check.draggable-mirror { 24 | z-index: $zindex-fixed; 25 | } 26 | 27 | .checklist .form-check.draggable-source--is-dragging { 28 | opacity: 0.2; 29 | } 30 | 31 | .checklist .form-check .form-check-input:checked + .form-check-label { 32 | text-decoration: line-through; 33 | color: $checklist-control-checked-color; 34 | } 35 | -------------------------------------------------------------------------------- /app/scss/dashkit/_close.scss: -------------------------------------------------------------------------------- 1 | // 2 | // close.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | .btn-close { 7 | float: right; 8 | } 9 | -------------------------------------------------------------------------------- /app/scss/dashkit/_comment.scss: -------------------------------------------------------------------------------- 1 | // 2 | // comment.scss 3 | // Dashkit component 4 | // 5 | 6 | // Comment 7 | // 8 | // General styles 9 | 10 | .comment { 11 | margin-bottom: $comment-margin-bottom; 12 | } 13 | 14 | .comment-body { 15 | display: inline-block; 16 | padding: $comment-body-padding-y $comment-body-padding-x; 17 | background-color: $comment-body-bg; 18 | border-radius: $comment-body-border-radius; 19 | } 20 | 21 | .comment-time { 22 | display: block; 23 | margin-bottom: $comment-time-margin-bottom; 24 | font-size: $comment-time-font-size; 25 | color: $comment-time-color; 26 | } 27 | 28 | .comment-text { 29 | font-size: $comment-body-font-size; 30 | } 31 | 32 | .comment-text:last-child { 33 | margin-bottom: 0; 34 | } 35 | -------------------------------------------------------------------------------- /app/scss/dashkit/_forms.scss: -------------------------------------------------------------------------------- 1 | @import 'forms/form-text'; 2 | @import 'forms/form-control'; 3 | @import 'forms/form-check'; 4 | @import 'forms/input-group'; 5 | @import 'forms/validation'; 6 | @import 'forms/form-group'; 7 | -------------------------------------------------------------------------------- /app/scss/dashkit/_header.scss: -------------------------------------------------------------------------------- 1 | // 2 | // header.scss 3 | // Dashkit component 4 | // 5 | 6 | // Header 7 | // 8 | // General styles 9 | 10 | .header { 11 | margin-bottom: $header-margin-bottom; 12 | } 13 | 14 | .header-img-top { 15 | width: 100%; 16 | height: auto; 17 | } 18 | 19 | .header-body { 20 | padding-top: $header-spacing-y; 21 | padding-bottom: $header-spacing-y; 22 | border-bottom: $header-body-border-width solid $header-body-border-color; 23 | } 24 | 25 | .header.bg-dark .header-body { 26 | border-bottom-color: $header-body-border-color-dark; 27 | } 28 | 29 | .header-footer { 30 | padding-top: $header-spacing-y; 31 | padding-bottom: $header-spacing-y; 32 | } 33 | 34 | .header-pretitle { 35 | text-transform: uppercase; 36 | letter-spacing: 0.08em; 37 | color: $text-muted; 38 | } 39 | 40 | .header-title { 41 | margin-bottom: 0; 42 | } 43 | 44 | .header-subtitle { 45 | margin-top: map-get($spacers, 2); 46 | margin-bottom: 0; 47 | color: $text-muted; 48 | } 49 | 50 | .header-tabs { 51 | margin-bottom: -$header-spacing-y; 52 | border-bottom-width: 0; 53 | 54 | .nav-link { 55 | padding-top: $header-spacing-y; 56 | padding-bottom: $header-spacing-y; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/scss/dashkit/_icon.scss: -------------------------------------------------------------------------------- 1 | // 2 | // icon.scss 3 | // Dashkit component 4 | // 5 | 6 | // Icon 7 | // 8 | // General styles 9 | 10 | .icon { 11 | display: inline-block; 12 | 13 | // Feather icon 14 | 15 | > .fe { 16 | display: block; 17 | min-width: 1em * $line-height-base; 18 | min-height: 1em * $line-height-base; 19 | text-align: center; 20 | font-size: $font-size-lg; 21 | } 22 | 23 | // Active state 24 | 25 | &.active { 26 | position: relative; 27 | 28 | // Feather icon 29 | 30 | > .fe { 31 | mask-image: url(#{$path-to-img}/masks/icon-status.svg); 32 | mask-size: 100% 100%; 33 | } 34 | 35 | // Indicator 36 | 37 | &::after { 38 | content: ''; 39 | position: absolute; 40 | top: 10%; 41 | right: 20%; 42 | width: 20%; 43 | height: 20%; 44 | border-radius: 50%; 45 | background-color: $primary; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/scss/dashkit/_kanban.scss: -------------------------------------------------------------------------------- 1 | // 2 | // kanban.scss 3 | // Dashkit component 4 | // 5 | 6 | // Container 7 | 8 | .container-fluid.kanban-container { 9 | min-height: calc(100vh - 129px); 10 | } 11 | 12 | .container.kanban-container { 13 | min-height: calc(100vh - 129px - 69px); 14 | } 15 | 16 | .kanban-container { 17 | overflow-x: scroll; 18 | -webkit-overflow-scrolling: touch; 19 | } 20 | 21 | .kanban-container > .row { 22 | flex-wrap: nowrap; 23 | } 24 | 25 | .kanban-container > .row > [class*='col'] { 26 | max-width: $kanban-col-width; 27 | } 28 | 29 | // Category 30 | 31 | .kanban-category { 32 | min-height: 1rem; 33 | } 34 | 35 | // Item 36 | 37 | .kanban-item { 38 | outline: none; 39 | user-select: none; 40 | } 41 | 42 | .kanban-item.draggable-source--is-dragging { 43 | opacity: 0.2; 44 | } 45 | 46 | .kanban-item.draggable-mirror { 47 | z-index: $zindex-fixed; 48 | } 49 | 50 | .card-body .kanban-item.draggable-mirror > .card { 51 | transform: rotateZ(-3deg); 52 | } 53 | 54 | // Card 55 | 56 | .kanban-item > .card[data-bs-toggle='modal'] { 57 | cursor: pointer; 58 | } 59 | 60 | // Add form 61 | 62 | .kanban-add-form .form-control[data-flatpickr] { 63 | width: 12ch; // there is no CSS way to set input's width to auto so hardcoding this value 64 | } 65 | -------------------------------------------------------------------------------- /app/scss/dashkit/_list-group.scss: -------------------------------------------------------------------------------- 1 | // 2 | // list-group.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | @use 'sass:math'; 7 | 8 | // 9 | // Bootstrap Overrides ===================================== 10 | // 11 | 12 | // Contextual variants 13 | // 14 | // Changing the Bootstrap color modifier classes to be full opacity background with yiq calculated font color 15 | 16 | @each $color, $value in $theme-colors { 17 | @include list-group-item-variant($color, $value, color-yiq($value)); 18 | } 19 | 20 | // List group sizing 21 | 22 | .list-group-lg .list-group-item { 23 | padding-top: $list-group-item-padding-y-lg; 24 | padding-bottom: $list-group-item-padding-y-lg; 25 | } 26 | 27 | // List group flush 28 | 29 | .list-group-flush > .list-group-item { 30 | padding-left: 0; 31 | padding-right: 0; 32 | } 33 | 34 | .list-group-flush:not(:last-child) > .list-group-item:last-child { 35 | border-bottom-width: $list-group-border-width; 36 | } 37 | 38 | // List group focus 39 | 40 | .list-group-focus .list-group-item:focus .text-focus { 41 | color: $link-color !important; 42 | } 43 | 44 | // 45 | // Theme =================================== 46 | // 47 | 48 | // Activity 49 | 50 | .list-group-activity .list-group-item { 51 | border: 0; 52 | } 53 | 54 | .list-group-activity .list-group-item:not(:last-child)::before { 55 | content: ''; 56 | position: absolute; 57 | top: $list-group-item-padding-y; 58 | left: math.div($avatar-size-sm, 2); 59 | height: 100%; 60 | border-left: $border-width solid $border-color; 61 | } 62 | -------------------------------------------------------------------------------- /app/scss/dashkit/_mixins.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Mixins 3 | // 4 | 5 | // Utilities 6 | @import 'mixins/breakpoints'; 7 | @import 'mixins/badge'; 8 | -------------------------------------------------------------------------------- /app/scss/dashkit/_modal.scss: -------------------------------------------------------------------------------- 1 | // 2 | // modal.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | // 7 | // Bootstrap Overrides ======================= 8 | // 9 | 10 | .modal-dialog { 11 | // When fading in the modal, animate it to slide down 12 | .modal.fade & { 13 | transform: translate(0, -150px); 14 | } 15 | 16 | .modal.show & { 17 | transform: translate(0, 0); 18 | } 19 | } 20 | 21 | .modal-header .btn-close { 22 | margin: -1.5rem -1.5rem -1.5rem auto; 23 | } 24 | 25 | // 26 | // Theme =================================== 27 | // 28 | 29 | // Modal card 30 | 31 | .modal-card { 32 | margin-bottom: 0; 33 | 34 | .card-body { 35 | max-height: $modal-card-body-max-height; 36 | overflow-y: auto; 37 | } 38 | } 39 | 40 | // Modal tabs 41 | 42 | .modal-header-tabs { 43 | margin-top: -$modal-header-padding-y; 44 | margin-bottom: calc(-#{$modal-header-padding-y} - #{$border-width}); 45 | } 46 | -------------------------------------------------------------------------------- /app/scss/dashkit/_offcanvas.scss: -------------------------------------------------------------------------------- 1 | // 2 | // offcanvas.scss 3 | // 4 | 5 | // Header 6 | 7 | .offcanvas-header { 8 | padding: $offcanvas-header-padding-y $offcanvas-header-padding-x; 9 | border-bottom: $border-width solid $border-color; 10 | } 11 | -------------------------------------------------------------------------------- /app/scss/dashkit/_progress.scss: -------------------------------------------------------------------------------- 1 | // 2 | // progress.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | // 7 | // Bootstrap Overrides ===================================== 8 | // 9 | 10 | // Rounds the progress bar, even for "multiple bar" progress bars 11 | .progress-bar:first-child { 12 | border-top-left-radius: $progress-border-radius; 13 | border-bottom-left-radius: $progress-border-radius; 14 | } 15 | .progress-bar:last-child { 16 | border-top-right-radius: $progress-border-radius; 17 | border-bottom-right-radius: $progress-border-radius; 18 | } 19 | 20 | // 21 | // Theme =================================== 22 | // 23 | 24 | .progress-sm { 25 | height: $progress-height-sm; 26 | } 27 | -------------------------------------------------------------------------------- /app/scss/dashkit/_reboot.scss: -------------------------------------------------------------------------------- 1 | // 2 | // reboot.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | html { 7 | height: 100%; 8 | } 9 | 10 | body { 11 | min-height: 100%; 12 | } 13 | 14 | // Lists 15 | 16 | ul, 17 | ol { 18 | padding-left: 2.5rem; 19 | } 20 | 21 | // 22 | // Remove the cancel buttons in Chrome and Safari on macOS. 23 | // 24 | 25 | [type='search']::-webkit-search-cancel-button { 26 | -webkit-appearance: none; 27 | } 28 | -------------------------------------------------------------------------------- /app/scss/dashkit/_root.scss: -------------------------------------------------------------------------------- 1 | // 2 | // root.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | :root { 7 | // Chart variables 8 | @each $color, $value in $chart-colors { 9 | --bs-chart-#{$color}: #{$value}; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/scss/dashkit/_theme.scss: -------------------------------------------------------------------------------- 1 | // Configuration 2 | @import 'mixins'; 3 | @import 'utilities'; 4 | 5 | // Layout & components 6 | @import 'root'; 7 | @import 'reboot'; 8 | @import 'type'; 9 | @import 'tables'; 10 | @import 'forms'; 11 | @import 'buttons'; 12 | @import 'dropdown'; 13 | @import 'nav'; 14 | @import 'navbar'; 15 | @import 'card'; 16 | @import 'breadcrumb'; 17 | @import 'pagination'; 18 | @import 'badge'; 19 | @import 'alert'; 20 | @import 'progress'; 21 | @import 'list-group'; 22 | @import 'close'; 23 | @import 'modal'; 24 | @import 'popover'; 25 | @import 'offcanvas'; 26 | 27 | // Dashkit 28 | @import 'avatar'; 29 | @import 'chart'; 30 | @import 'comment'; 31 | @import 'checklist'; 32 | @import 'header'; 33 | @import 'icon'; 34 | @import 'kanban'; 35 | @import 'main-content'; 36 | @import 'vendor'; 37 | -------------------------------------------------------------------------------- /app/scss/dashkit/_utilities.scss: -------------------------------------------------------------------------------- 1 | // 2 | // utilities.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | @import 'utilities/background'; 7 | @import 'utilities/lift'; 8 | -------------------------------------------------------------------------------- /app/scss/dashkit/_vendor.scss: -------------------------------------------------------------------------------- 1 | @import 'vendor/choices'; 2 | @import 'vendor/dropzone'; 3 | @import 'vendor/flatpickr'; 4 | @import 'vendor/highlight'; 5 | @import 'vendor/quill'; 6 | @import 'vendor/list'; 7 | -------------------------------------------------------------------------------- /app/scss/dashkit/dark/_overrides-dark.scss: -------------------------------------------------------------------------------- 1 | // 2 | // overrides.scss 3 | // Dark mode overrides 4 | // 5 | 6 | // 7 | // Table of contents 8 | // 9 | // 1. Buttons 10 | // 2. Dropzone 11 | // 3. Quill 12 | // 13 | 14 | // Buttons 15 | 16 | .btn-white, 17 | .btn-light { 18 | @include button-variant($gray-800-dark, $gray-600-dark); 19 | 20 | &:not(:disabled):not(.disabled):hover, 21 | &:not(:disabled):not(.disabled):focus, 22 | &:not(:disabled):not(.disabled):active, 23 | &:not(:disabled):not(.disabled).active, 24 | &:not(:disabled):not(.disabled):active:focus, 25 | &:not(:disabled):not(.disabled).active:focus, 26 | .show > &.dropdown-toggle { 27 | background-color: $black-dark; 28 | border-color: $gray-700-dark; 29 | color: $white; 30 | } 31 | } 32 | 33 | .btn-black.active { 34 | box-shadow: 0 0 0 .15rem #33a382 !important; 35 | } 36 | 37 | // Dropzone 38 | 39 | .dz-message { 40 | border-color: $black-dark; 41 | } 42 | 43 | // Quill 44 | 45 | .ql-toolbar { 46 | border-bottom-color: $black-dark; 47 | } 48 | 49 | .ql-editor { 50 | border-top-color: $black-dark; 51 | } 52 | -------------------------------------------------------------------------------- /app/scss/dashkit/forms/_form-group.scss: -------------------------------------------------------------------------------- 1 | // 2 | // form-group.scss 3 | // Dashkit component 4 | // 5 | 6 | .form-group { 7 | margin-bottom: $form-group-margin-bottom; 8 | } 9 | -------------------------------------------------------------------------------- /app/scss/dashkit/forms/_form-text.scss: -------------------------------------------------------------------------------- 1 | // 2 | // form-text.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | .form-text { 7 | display: block; 8 | margin-bottom: $form-text-margin-bottom; 9 | } 10 | -------------------------------------------------------------------------------- /app/scss/dashkit/forms/_validation.scss: -------------------------------------------------------------------------------- 1 | // 2 | // validation.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | // 7 | // Bootstrap Overrides ===================================== 8 | // 9 | 10 | .form-control.is-valid:focus, 11 | .form-control.is-invalid:focus { 12 | box-shadow: none; 13 | 14 | @if ($enable-shadows) { 15 | box-shadow: $input-focus-box-shadow; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/scss/dashkit/mixins/_badge.scss: -------------------------------------------------------------------------------- 1 | // Badge Mixins 2 | // 3 | // This is a custom mixin for badge-#{color}-soft variant of Bootstrap's .badge class 4 | 5 | @mixin badge-variant-soft($bg, $color) { 6 | color: $color; 7 | 8 | &[href]:hover, 9 | &[href]:focus { 10 | background-color: darken($bg, 5%) !important; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/scss/dashkit/mixins/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // 2 | // breakpoint.scss 3 | // Extended from Bootstrap 4 | // 5 | 6 | @function breakpoint-prev($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) { 7 | $n: index($breakpoint-names, $name); 8 | @return if($n != null and $n != 1, nth($breakpoint-names, $n - 1), null); 9 | } 10 | -------------------------------------------------------------------------------- /app/scss/dashkit/utilities/_background.scss: -------------------------------------------------------------------------------- 1 | // 2 | // background.scss 3 | // Theme utilities 4 | // 5 | 6 | // Fixed at the bottom 7 | 8 | .bg-fixed-bottom { 9 | background-repeat: no-repeat; 10 | background-position: right bottom; 11 | background-size: 100% auto; 12 | background-attachment: fixed; 13 | } 14 | 15 | // Calculate the width of the main container because 16 | // the background-attachment property will use 100vw instead 17 | 18 | .navbar-vertical ~ .main-content.bg-fixed-bottom { 19 | background-size: 100%; 20 | 21 | @include media-breakpoint-up(md) { 22 | background-size: calc(100% - #{$navbar-vertical-width}); 23 | } 24 | } 25 | 26 | // Cover 27 | 28 | .bg-cover { 29 | background-repeat: no-repeat; 30 | background-position: center center; 31 | background-size: cover; 32 | } 33 | 34 | // Ellipses 35 | 36 | @each $color, $value in $theme-colors { 37 | .bg-ellipses.bg-#{$color} { 38 | background-color: transparent !important; 39 | background-repeat: no-repeat; 40 | background-image: radial-gradient(#{$value}, #{$value} 70%, transparent 70.1%); 41 | background-size: 200% 150%; 42 | background-position: center bottom; 43 | } 44 | } 45 | 46 | // Soft colors 47 | 48 | @each $color, $value in $theme-colors { 49 | .bg-#{$color}-soft { 50 | background-color: shift-color($value, $bg-soft-scale) !important; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/scss/dashkit/utilities/_lift.scss: -------------------------------------------------------------------------------- 1 | // 2 | // lift.scss 3 | // Theme utility 4 | // 5 | 6 | .lift { 7 | transition: box-shadow 0.25s ease, transform 0.25s ease; 8 | } 9 | 10 | .lift:hover, 11 | .lift:focus { 12 | box-shadow: $box-shadow-lift !important; 13 | transform: translate3d(0, -3px, 0); 14 | } 15 | 16 | .lift-lg:hover, 17 | .lift-lg:focus { 18 | box-shadow: $box-shadow-lift-lg !important; 19 | transform: translate3d(0, -5px, 0); 20 | } 21 | -------------------------------------------------------------------------------- /app/scss/dashkit/vendor/_flatpickr.scss: -------------------------------------------------------------------------------- 1 | // 2 | // flatpickr.scss 3 | // Flatpickr plugin overrides 4 | // 5 | 6 | .flatpickr-calendar { 7 | background-color: $input-bg; 8 | border: $input-border-width solid $input-border-color; 9 | color: $input-color; 10 | box-shadow: none; 11 | 12 | * { 13 | color: inherit !important; 14 | fill: currentColor !important; 15 | } 16 | 17 | &.arrowTop:before { 18 | border-bottom-color: $input-border-color; 19 | } 20 | 21 | &.arrowTop:after { 22 | border-bottom-color: $input-bg; 23 | } 24 | 25 | .flatpickr-months { 26 | padding-top: 0.625rem; 27 | padding-bottom: 0.625rem; 28 | } 29 | 30 | .flatpickr-prev-month, 31 | .flatpickr-next-month { 32 | top: 0.625rem; 33 | } 34 | 35 | .flatpickr-current-month { 36 | font-size: 115%; 37 | } 38 | 39 | .flatpickr-day { 40 | border-radius: $border-radius; 41 | 42 | &:hover { 43 | background-color: $light; 44 | border-color: $input-border-color; 45 | } 46 | } 47 | 48 | .flatpickr-day.prevMonthDay { 49 | color: $text-muted !important; 50 | } 51 | 52 | .flatpickr-day.today { 53 | border-color: $border-color; 54 | } 55 | 56 | .flatpickr-day.selected { 57 | background-color: $primary; 58 | border-color: $primary; 59 | color: $white !important; 60 | } 61 | 62 | .flatpickr-day.inRange { 63 | background-color: $light; 64 | border: none; 65 | border-radius: 0; 66 | box-shadow: -5px 0 0 $light, 5px 0 0 $light; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/scss/dashkit/vendor/_highlight.scss: -------------------------------------------------------------------------------- 1 | // 2 | // highlight.scss 3 | // Highlight.js plugin overrides 4 | // 5 | 6 | .hljs { 7 | padding: 0; 8 | } 9 | -------------------------------------------------------------------------------- /app/scss/dashkit/vendor/_list.scss: -------------------------------------------------------------------------------- 1 | // 2 | // list.scss 3 | // List.js plugin overrides 4 | // 5 | 6 | @use 'sass:math'; 7 | 8 | // Pagination 9 | 10 | .page { 11 | @extend .page-link; 12 | } 13 | 14 | .list-pagination > li + li { 15 | margin-left: -$pagination-border-width; 16 | } 17 | 18 | // Alert 19 | 20 | .list-alert { 21 | position: fixed; 22 | bottom: $spacer; 23 | left: 50%; 24 | z-index: $zindex-fixed; 25 | min-width: $list-alert-min-width; 26 | margin-bottom: 0; 27 | transform: translateX(-50%); 28 | } 29 | 30 | .list-alert:not(.show) { 31 | pointer-events: none; 32 | } 33 | 34 | @include media-breakpoint-up($navbar-vertical-expand-breakpoint) { 35 | .navbar-vertical:not(.navbar-vertical-sm):not([style*='display: none']) ~ .main-content .list-alert { 36 | left: calc(50% + #{math.div($navbar-vertical-width, 2)}); 37 | } 38 | } 39 | 40 | .list-alert .btn-close { 41 | top: 50%; 42 | transform: translateY(-50%); 43 | } 44 | -------------------------------------------------------------------------------- /app/scss/theme-dark.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Dashkit 3 | * 4 | * Custom variables followed by Dashkit variables followed by Bootstrap variables 5 | * to ensure cascade of styles. 6 | */ 7 | 8 | $path-to-img: '/img/dashkit' !default; 9 | 10 | // Bootstrap functions 11 | @import '~bootstrap/scss/functions'; 12 | 13 | // Custom variables 14 | @import 'solana-variables-dark'; 15 | 16 | // Custom variables 17 | @import 'solana-variables'; 18 | 19 | // Dark mode variables 20 | @import 'dashkit/dark/variables-dark'; 21 | 22 | // Dashkit variables 23 | @import 'dashkit/variables'; 24 | 25 | // Bootstrap core 26 | @import '~bootstrap/scss/bootstrap'; 27 | 28 | // Dashkit core 29 | @import 'dashkit/theme'; 30 | 31 | // Dark mode overrides 32 | @import 'dashkit/dark/overrides-dark'; 33 | 34 | // Custom core 35 | @import 'solana'; 36 | 37 | // Dark mode overrides 38 | @import 'solana-dark-overrides'; 39 | -------------------------------------------------------------------------------- /app/scss/theme.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Dashkit 3 | * 4 | * Custom variables followed by Dashkit variables followed by Bootstrap variables 5 | * to ensure cascade of styles. 6 | */ 7 | 8 | // Icon font 9 | @import '../fonts/feather/feather'; 10 | 11 | // Bootstrap functions 12 | @import '~bootstrap/scss/functions.scss'; 13 | 14 | // Custom variables 15 | @import 'solana-variables'; 16 | 17 | // Dashkit variables 18 | @import 'dashkit/variables'; 19 | 20 | // Bootstrap core 21 | @import '~bootstrap/scss/bootstrap.scss'; 22 | 23 | // Dashkit core 24 | @import 'dashkit/dashkit'; 25 | 26 | // Custom core 27 | @import 'solana'; 28 | -------------------------------------------------------------------------------- /app/supply/layout.tsx: -------------------------------------------------------------------------------- 1 | import { RichListProvider } from '@providers/richList'; 2 | import { SupplyProvider } from '@providers/supply'; 3 | import { PropsWithChildren } from 'react'; 4 | 5 | export default function SupplyLayout({ children }: PropsWithChildren>) { 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/supply/page-client.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SupplyCard } from '@components/SupplyCard'; 4 | import { TopAccountsCard } from '@components/TopAccountsCard'; 5 | import { useCluster } from '@providers/cluster'; 6 | import { Cluster } from '@utils/cluster'; 7 | import React from 'react'; 8 | 9 | export default function SupplyPageClient() { 10 | const cluster = useCluster(); 11 | return ( 12 |
13 | 14 | {cluster.cluster === Cluster.Custom ? : null} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/supply/page.tsx: -------------------------------------------------------------------------------- 1 | import SupplyPageClient from './page-client'; 2 | 3 | export const metadata = { 4 | description: `Overview of the native token supply on Solana`, 5 | title: `Supply Overview | Solana`, 6 | }; 7 | 8 | export default function SupplyPage() { 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /app/tx/(inspector)/[signature]/inspect/page.tsx: -------------------------------------------------------------------------------- 1 | import { TransactionInspectorPage } from '@components/inspector/InspectorPage'; 2 | import { Metadata } from 'next/types'; 3 | 4 | type Props = Readonly<{ 5 | params: Readonly<{ 6 | signature: string; 7 | }>; 8 | }>; 9 | 10 | export async function generateMetadata({ params: { signature } }: Props): Promise { 11 | return { 12 | description: `Interactively inspect the transaction with signature ${signature} on Solana`, 13 | title: `Transaction Inspector | ${signature} | Solana`, 14 | }; 15 | } 16 | 17 | export default function TransactionInspectionPage({ params: { signature } }: Props) { 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /app/tx/(inspector)/inspector/page.tsx: -------------------------------------------------------------------------------- 1 | import { TransactionInspectorPage } from '@components/inspector/InspectorPage'; 2 | 3 | type Props = Readonly<{ 4 | params: Readonly<{ 5 | signature: string; 6 | }>; 7 | }>; 8 | 9 | export default function Page({ params: { signature } }: Props) { 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /app/tx/(inspector)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next/types'; 2 | import React from 'react'; 3 | 4 | type Props = Readonly<{ 5 | children: React.ReactNode; 6 | params: Readonly<{ 7 | signature: string; 8 | }>; 9 | }>; 10 | 11 | export async function generateMetadata({ params: { signature } }: Props): Promise { 12 | if (signature) { 13 | return { 14 | description: `Interactively inspect the Solana transaction with signature ${signature}`, 15 | title: `Transaction Inspector | ${signature} | Solana`, 16 | }; 17 | } else { 18 | return { 19 | description: `Interactively inspect Solana transactions`, 20 | title: `Transaction Inspector | Solana`, 21 | }; 22 | } 23 | } 24 | 25 | export default function TransactionInspectorLayout({ children }: Props) { 26 | return children; 27 | } 28 | -------------------------------------------------------------------------------- /app/tx/[signature]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignatureProps } from '@utils/index'; 2 | import { Metadata } from 'next/types'; 3 | import React from 'react'; 4 | 5 | import TransactionDetailsPageClient from './page-client'; 6 | 7 | type Props = Readonly<{ 8 | params: SignatureProps; 9 | }>; 10 | 11 | export async function generateMetadata({ params: { signature } }: Props): Promise { 12 | return { 13 | description: `Details of the Solana transaction with signature ${signature}`, 14 | title: `Transaction | ${signature} | Solana`, 15 | }; 16 | } 17 | 18 | export default function TransactionDetailsPage(props: Props) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /app/tx/layout.tsx: -------------------------------------------------------------------------------- 1 | import { TransactionsProvider } from '@providers/transactions'; 2 | import { PropsWithChildren } from 'react'; 3 | 4 | import { AccountsProvider } from '../providers/accounts'; 5 | 6 | export default function TxLayout({ children }: PropsWithChildren>) { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/types/react-json-view.d.ts: -------------------------------------------------------------------------------- 1 | import 'react-json-view'; 2 | 3 | declare module 'react-json-view' { 4 | interface ReactJsonViewProps { 5 | displayArrayKey?: boolean; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/utils/__tests__/lamportsToSol-test.ts: -------------------------------------------------------------------------------- 1 | import { LAMPORTS_PER_SOL, lamportsToSol } from '@utils/index'; 2 | 3 | describe('lamportsToSol', () => { 4 | it('0 lamports', () => { 5 | expect(lamportsToSol(0)).toBe(0.0); 6 | expect(lamportsToSol(BigInt(0))).toBe(0.0); 7 | }); 8 | 9 | it('1 lamport', () => { 10 | expect(lamportsToSol(1)).toBe(0.000000001); 11 | expect(lamportsToSol(BigInt(1))).toBe(0.000000001); 12 | expect(lamportsToSol(-1)).toBe(-0.000000001); 13 | expect(lamportsToSol(BigInt(-1))).toBe(-0.000000001); 14 | }); 15 | 16 | it('1 SOL', () => { 17 | expect(lamportsToSol(LAMPORTS_PER_SOL)).toBe(1.0); 18 | expect(lamportsToSol(BigInt(LAMPORTS_PER_SOL))).toBe(1.0); 19 | expect(lamportsToSol(-LAMPORTS_PER_SOL)).toBe(-1.0); 20 | expect(lamportsToSol(BigInt(-LAMPORTS_PER_SOL))).toBe(-1.0); 21 | }); 22 | 23 | it('u64::MAX lamports', () => { 24 | expect(lamportsToSol(2n ** 64n)).toBe(18446744073.709553); 25 | expect(lamportsToSol(-(2n ** 64n))).toBe(-18446744073.709553); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /app/utils/__tests__/math-test.ts: -------------------------------------------------------------------------------- 1 | import { percentage } from '@utils/math'; 2 | 3 | describe('percentage', () => { 4 | it('returns a number with the right decimals', () => { 5 | expect(percentage(BigInt(1), BigInt(3), 0)).toEqual(33); 6 | expect(percentage(BigInt(1), BigInt(3), 1)).toEqual(33.3); 7 | expect(percentage(BigInt(1), BigInt(3), 2)).toEqual(33.33); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /app/utils/__tests__/parseFeatureAccount-test.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { FEATURE_PROGRAM_ID, parseFeatureAccount } from '@utils/parseFeatureAccount'; 3 | 4 | describe('parseFeatureAccount', () => { 5 | it('parses an activated feature', () => { 6 | const buffer = new Uint8Array([0x01, 0x80, 0xc2, 0x2b, 0x0a, 0x00, 0x00, 0x00, 0x00]); 7 | const feature = parseFeatureAccount({ 8 | data: { raw: buffer as Buffer }, 9 | executable: false, 10 | lamports: 1, 11 | owner: new PublicKey(FEATURE_PROGRAM_ID), 12 | pubkey: new PublicKey('7txXZZD6Um59YoLMF7XUNimbMjsqsWhc7g2EniiTrmp1'), 13 | space: buffer.length, 14 | }); 15 | expect(feature?.activatedAt).toBe(170640000); 16 | }); 17 | it('parses a feature that is scheduled for activation', () => { 18 | const buffer = new Uint8Array([0x00]); 19 | const feature = parseFeatureAccount({ 20 | data: { raw: buffer as Buffer }, 21 | executable: false, 22 | lamports: 1, 23 | owner: new PublicKey(FEATURE_PROGRAM_ID), 24 | pubkey: new PublicKey('7txXZZD6Um59YoLMF7XUNimbMjsqsWhc7g2EniiTrmp1'), 25 | space: buffer.length, 26 | }); 27 | expect(feature?.activatedAt).toBeNull(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /app/utils/__tests__/use-tab-visibility.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | import { expect, test } from 'vitest'; 4 | 5 | import useTabVisibility from '../use-tab-visibility'; 6 | 7 | const isVisibleTestId = 'isVisible'; 8 | 9 | const App: React.FC = () => { 10 | const { visible } = useTabVisibility(); 11 | 12 | return
; 13 | }; 14 | 15 | test('detects visibility', async () => { 16 | render(); 17 | 18 | const el = screen.getByTestId(isVisibleTestId); 19 | 20 | expect(el.dataset.visible).toBe('true'); 21 | }); 22 | -------------------------------------------------------------------------------- /app/utils/feature-gate/types.ts: -------------------------------------------------------------------------------- 1 | export type FeatureInfoType = { 2 | key: string; 3 | title: string; 4 | simd_link: string[]; 5 | simds: string[]; 6 | owners: string[]; 7 | min_agave_versions: string[]; 8 | min_fd_versions: string[]; 9 | min_jito_versions: string[]; 10 | planned_testnet_order: number | string | null; 11 | testnet_activation_epoch: number | string | null; 12 | devnet_activation_epoch: number | string | null; 13 | comms_required: string | null; 14 | mainnet_activation_epoch: number | string | null; 15 | description: string | null; 16 | }; 17 | -------------------------------------------------------------------------------- /app/utils/feature-gate/utils.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | import FEATURES from './featureGates.json'; 4 | import { FeatureInfoType } from './types'; 5 | 6 | export function getFeatureInfo(address: string): FeatureInfoType | undefined { 7 | const index = FEATURES.findIndex(feature => feature.key === address); 8 | 9 | if (index === -1) return undefined; 10 | 11 | return FEATURES[index] as FeatureInfoType; 12 | } 13 | 14 | export function useFeatureInfo({ address }: { address: string }) { 15 | return useMemo(() => getFeatureInfo(address), [address]); 16 | } 17 | -------------------------------------------------------------------------------- /app/utils/get-instruction-card-scroll-anchor-id.ts: -------------------------------------------------------------------------------- 1 | export default function getInstructionCardScrollAnchorId( 2 | // An array of instruction sequence numbers, starting with the 3 | // top level instruction number. Instruction numbers start from 1. 4 | instructionNumberPath: number[] 5 | ): string { 6 | return `ix-${instructionNumberPath.join('-')}`; 7 | } 8 | -------------------------------------------------------------------------------- /app/utils/get-readable-title-from-address.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { Cluster, clusterUrl } from '@utils/cluster'; 3 | 4 | import { getTokenInfo } from './token-info'; 5 | 6 | export type AddressPageMetadataProps = Readonly<{ 7 | params: { 8 | address: string; 9 | }; 10 | searchParams: { 11 | cluster: string; 12 | customUrl?: string; 13 | }; 14 | }>; 15 | 16 | export default async function getReadableTitleFromAddress(props: AddressPageMetadataProps): Promise { 17 | const { 18 | params: { address }, 19 | searchParams: { cluster: clusterParam, customUrl }, 20 | } = props; 21 | 22 | let cluster: Cluster; 23 | switch (clusterParam) { 24 | case 'custom': 25 | cluster = Cluster.Custom; 26 | break; 27 | case 'devnet': 28 | cluster = Cluster.Devnet; 29 | break; 30 | case 'testnet': 31 | cluster = Cluster.Testnet; 32 | break; 33 | default: 34 | cluster = Cluster.MainnetBeta; 35 | } 36 | 37 | try { 38 | const url = clusterUrl(cluster, customUrl ? decodeURI(customUrl) : ''); 39 | const tokenInfo = await getTokenInfo(new PublicKey(address), cluster, url); 40 | const tokenName = tokenInfo?.name; 41 | if (tokenName == null) { 42 | return address; 43 | } 44 | const tokenDisplayAddress = address.slice(0, 2) + '\u2026' + address.slice(-2); 45 | return `Token | ${tokenName} (${tokenDisplayAddress})`; 46 | } catch { 47 | return address; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/utils/local-storage.ts: -------------------------------------------------------------------------------- 1 | let localStorageIsAvailableDecision: boolean | undefined; 2 | export function localStorageIsAvailable() { 3 | if (localStorageIsAvailableDecision === undefined) { 4 | const test = 'test'; 5 | try { 6 | localStorage.setItem(test, test); 7 | localStorage.removeItem(test); 8 | localStorageIsAvailableDecision = true; 9 | } catch (e) { 10 | localStorageIsAvailableDecision = false; 11 | } 12 | } 13 | return localStorageIsAvailableDecision; 14 | } 15 | -------------------------------------------------------------------------------- /app/utils/logger.ts: -------------------------------------------------------------------------------- 1 | enum LOG_LEVEL { 2 | ERROR, 3 | WARN, 4 | INFO, 5 | DEBUG, 6 | } 7 | 8 | function isLoggable(expectedLevel: LOG_LEVEL) { 9 | const currentLevel = process.env.NEXT_LOG_LEVEL ? parseInt(process.env.NEXT_LOG_LEVEL) : undefined; 10 | 11 | function isNullish(value: any): value is null | undefined { 12 | return value === null || value === undefined; 13 | } 14 | 15 | // do not log if expected level is greater than current one 16 | return !isNullish(currentLevel) && Number.isFinite(currentLevel) && expectedLevel <= currentLevel; 17 | } 18 | 19 | export default class StraightforwardLogger { 20 | static error(maybeError: any, ...other: any[]) { 21 | let error; 22 | if (maybeError instanceof Error) { 23 | error = maybeError; 24 | } else { 25 | error = new Error('Unrecognized error'); 26 | isLoggable(3) && console.debug(maybeError); 27 | } 28 | isLoggable(0) && console.error(error, ...other); 29 | } 30 | static debug(message: any, ...other: any[]) { 31 | isLoggable(3) && console.debug(message, ...other); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/utils/math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculate a percentage using bigints, as numerator/denominator * 100 3 | * @returns the percentage, with the requested number of decimal places 4 | */ 5 | export function percentage(numerator: bigint, denominator: bigint, decimals: number): number { 6 | // since bigint is integer, we need to multiply first to get decimals 7 | // see https://stackoverflow.com/a/63095380/1375972 8 | const pow = 10 ** decimals; 9 | return Number((numerator * BigInt(100 * pow)) / denominator) / pow; 10 | } 11 | -------------------------------------------------------------------------------- /app/utils/parseFeatureAccount.ts: -------------------------------------------------------------------------------- 1 | import { Account } from '@providers/accounts'; 2 | import * as BufferLayout from '@solana/buffer-layout'; 3 | 4 | export const FEATURE_PROGRAM_ID = 'Feature111111111111111111111111111111111111'; 5 | 6 | type FeatureAccount = { 7 | address: string; 8 | activatedAt: number | null; 9 | }; 10 | 11 | function isFeatureAccount(account: Account): boolean { 12 | return account.owner.toBase58() === FEATURE_PROGRAM_ID && account.data.raw != null; 13 | } 14 | 15 | export const useFeatureAccount = (account: Account) => { 16 | const isFeature = isFeatureAccount(account); 17 | 18 | // allow to retrieve sign of a Feature Account 19 | return { isFeature }; 20 | }; 21 | 22 | export const parseFeatureAccount = (account: Account): FeatureAccount => { 23 | if (!isFeatureAccount(account) || account.data.raw == null) { 24 | throw new Error(`Failed to parse ${account} as a feature account`); 25 | } 26 | const address = account.pubkey.toBase58(); 27 | const parsed = BufferLayout.struct([ 28 | ((): BufferLayout.Union => { 29 | const union = BufferLayout.union(BufferLayout.u8('isActivated'), null, 'activatedAt'); 30 | union.addVariant(0, BufferLayout.constant(null), 'value'); 31 | union.addVariant(1, BufferLayout.nu64(), 'value'); 32 | return union; 33 | })(), 34 | ]).decode(account.data.raw); 35 | return { 36 | activatedAt: parsed.activatedAt.value, 37 | address, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /app/utils/types/elfy.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'elfy' { 2 | const elfy: any; 3 | export = elfy; 4 | } 5 | -------------------------------------------------------------------------------- /app/utils/url.ts: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from 'next/navigation'; 2 | import { useMemo } from 'react'; 3 | 4 | type Config = Readonly<{ 5 | additionalParams?: { get(key: string): string | null; toString(): string }; 6 | pathname: string; 7 | }>; 8 | 9 | export function useClusterPath({ additionalParams, pathname }: Config) { 10 | const currentSearchParams = useSearchParams(); 11 | const [pathnameWithoutHash, hash] = pathname.split('#'); 12 | return useMemo( 13 | () => 14 | pickClusterParams(pathnameWithoutHash, currentSearchParams ?? undefined, additionalParams) + 15 | (hash ? `#${hash}` : ''), 16 | [additionalParams, currentSearchParams, hash, pathnameWithoutHash] 17 | ); 18 | } 19 | 20 | export function pickClusterParams( 21 | pathname: string, 22 | currentSearchParams?: { toString(): string; get(key: string): string | null }, 23 | additionalParams?: { get(key: string): string | null } 24 | ): string { 25 | let nextSearchParams = additionalParams ? new URLSearchParams(additionalParams.toString()) : undefined; 26 | if (currentSearchParams && !!currentSearchParams.toString()) { 27 | // Pick the params we care about 28 | ['cluster', 'customUrl'].forEach(paramName => { 29 | const existingValue = currentSearchParams.get(paramName); 30 | if (existingValue) { 31 | nextSearchParams ||= new URLSearchParams(); 32 | nextSearchParams.set(paramName, existingValue); 33 | } 34 | }); 35 | } 36 | const queryString = nextSearchParams?.toString(); 37 | return `${pathname}${queryString ? `?${queryString}` : ''}`; 38 | } 39 | -------------------------------------------------------------------------------- /app/utils/use-debounce-async.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export function useDebouncedAsync( 4 | fn: (...args: TArgs) => Promise, 5 | delay: number 6 | ): (...args: TArgs) => Promise { 7 | const timeout = useRef | null>(null); 8 | const pending = useRef<{ 9 | args: TArgs; 10 | resolve: (val: TResult) => void; 11 | reject: (err: any) => void; 12 | } | null>(null); 13 | 14 | return (...args: TArgs) => { 15 | return new Promise((resolve, reject) => { 16 | if (timeout.current) clearTimeout(timeout.current); 17 | 18 | pending.current = { args, reject, resolve }; 19 | 20 | timeout.current = setTimeout(async () => { 21 | if (pending.current) { 22 | const { args, resolve, reject } = pending.current; 23 | pending.current = null; 24 | try { 25 | const result = await fn(...args); 26 | resolve(result); 27 | } catch (e) { 28 | reject(e); 29 | } 30 | } 31 | }, delay); 32 | }); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /app/utils/use-tab-visibility.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useState } from 'react'; 2 | 3 | const getEventListenerName = (): string => { 4 | return 'visibilitychange'; 5 | }; 6 | 7 | const getIsVisible = (): boolean => { 8 | // assumes ssr generated page is always true 9 | if (typeof document === 'undefined') { 10 | return true; 11 | } 12 | 13 | return !document.hidden; 14 | }; 15 | 16 | const useTabVisibility = () => { 17 | const [visible, setVisible] = useState(getIsVisible()); 18 | const handleVisibility = useCallback(() => setVisible(getIsVisible()), [setVisible]); 19 | 20 | useEffect(() => { 21 | const evListenerName = getEventListenerName(); 22 | window?.document.addEventListener(evListenerName, handleVisibility, false); 23 | 24 | return () => window?.document.removeEventListener(evListenerName, handleVisibility); 25 | }, [handleVisibility]); 26 | 27 | return useMemo(() => ({ visible }), [visible]); 28 | }; 29 | 30 | export default useTabVisibility; 31 | -------------------------------------------------------------------------------- /app/validators/accounts/address-lookup-table.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-redeclare */ 2 | 3 | import { BigIntFromString, NumberFromString } from '@validators/number'; 4 | import { PublicKeyFromString } from '@validators/pubkey'; 5 | import { array, enums, Infer, number, optional, type } from 'superstruct'; 6 | 7 | export type AddressLookupTableAccountType = Infer; 8 | export const AddressLookupTableAccountType = enums(['uninitialized', 'lookupTable']); 9 | 10 | export type AddressLookupTableAccountInfo = Infer; 11 | export const AddressLookupTableAccountInfo = type({ 12 | addresses: array(PublicKeyFromString), 13 | authority: optional(PublicKeyFromString), 14 | deactivationSlot: BigIntFromString, 15 | lastExtendedSlot: NumberFromString, 16 | lastExtendedSlotStartIndex: number(), 17 | }); 18 | 19 | export type ParsedAddressLookupTableAccount = Infer; 20 | export const ParsedAddressLookupTableAccount = type({ 21 | info: AddressLookupTableAccountInfo, 22 | type: AddressLookupTableAccountType, 23 | }); 24 | -------------------------------------------------------------------------------- /app/validators/accounts/config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-redeclare */ 2 | 3 | import { array, boolean, Infer, literal, number, record, string, type, union } from 'superstruct'; 4 | 5 | export type StakeConfigInfo = Infer; 6 | export const StakeConfigInfo = type({ 7 | slashPenalty: number(), 8 | warmupCooldownRate: number(), 9 | }); 10 | 11 | export type ConfigKey = Infer; 12 | export const ConfigKey = type({ 13 | pubkey: string(), 14 | signer: boolean(), 15 | }); 16 | 17 | export type ValidatorInfoConfigData = Infer; 18 | export const ValidatorInfoConfigData = record(string(), string()); 19 | 20 | export type ValidatorInfoConfigInfo = Infer; 21 | export const ValidatorInfoConfigInfo = type({ 22 | configData: ValidatorInfoConfigData, 23 | keys: array(ConfigKey), 24 | }); 25 | 26 | export type ValidatorInfoAccount = Infer; 27 | export const ValidatorInfoAccount = type({ 28 | info: ValidatorInfoConfigInfo, 29 | type: literal('validatorInfo'), 30 | }); 31 | 32 | export type StakeConfigInfoAccount = Infer; 33 | export const StakeConfigInfoAccount = type({ 34 | info: StakeConfigInfo, 35 | type: literal('stakeConfig'), 36 | }); 37 | 38 | export type ConfigAccount = Infer; 39 | export const ConfigAccount = union([StakeConfigInfoAccount, ValidatorInfoAccount]); 40 | -------------------------------------------------------------------------------- /app/validators/accounts/nonce.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-redeclare */ 2 | 3 | import { PublicKeyFromString } from '@validators/pubkey'; 4 | import { enums, Infer, string, type } from 'superstruct'; 5 | 6 | export type NonceAccountType = Infer; 7 | export const NonceAccountType = enums(['uninitialized', 'initialized']); 8 | 9 | export type NonceAccountInfo = Infer; 10 | export const NonceAccountInfo = type({ 11 | authority: PublicKeyFromString, 12 | blockhash: string(), 13 | feeCalculator: type({ 14 | lamportsPerSignature: string(), 15 | }), 16 | }); 17 | 18 | export type NonceAccount = Infer; 19 | export const NonceAccount = type({ 20 | info: NonceAccountInfo, 21 | type: NonceAccountType, 22 | }); 23 | -------------------------------------------------------------------------------- /app/validators/accounts/stake.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-redeclare */ 2 | 3 | import { BigIntFromString } from '@validators/number'; 4 | import { PublicKeyFromString } from '@validators/pubkey'; 5 | import { enums, Infer, nullable, number, type } from 'superstruct'; 6 | 7 | export type StakeAccountType = Infer; 8 | export const StakeAccountType = enums(['uninitialized', 'initialized', 'delegated', 'rewardsPool']); 9 | 10 | export type StakeMeta = Infer; 11 | export const StakeMeta = type({ 12 | authorized: type({ 13 | staker: PublicKeyFromString, 14 | withdrawer: PublicKeyFromString, 15 | }), 16 | lockup: type({ 17 | custodian: PublicKeyFromString, 18 | epoch: number(), 19 | unixTimestamp: number(), 20 | }), 21 | rentExemptReserve: BigIntFromString, 22 | }); 23 | 24 | export type StakeAccountInfo = Infer; 25 | export const StakeAccountInfo = type({ 26 | meta: StakeMeta, 27 | stake: nullable( 28 | type({ 29 | creditsObserved: number(), 30 | delegation: type({ 31 | activationEpoch: BigIntFromString, 32 | deactivationEpoch: BigIntFromString, 33 | stake: BigIntFromString, 34 | voter: PublicKeyFromString, 35 | warmupCooldownRate: number(), 36 | }), 37 | }) 38 | ), 39 | }); 40 | 41 | export type StakeAccount = Infer; 42 | export const StakeAccount = type({ 43 | info: StakeAccountInfo, 44 | type: StakeAccountType, 45 | }); 46 | -------------------------------------------------------------------------------- /app/validators/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-redeclare */ 2 | 3 | import { any, Infer, string, type } from 'superstruct'; 4 | 5 | export type ParsedInfo = Infer; 6 | export const ParsedInfo = type({ 7 | info: any(), 8 | type: string(), 9 | }); 10 | -------------------------------------------------------------------------------- /app/validators/number.ts: -------------------------------------------------------------------------------- 1 | import { bigint, coerce, number, string } from 'superstruct'; 2 | 3 | export const BigIntFromString = coerce(bigint(), string(), (value): bigint => { 4 | if (typeof value === 'string') return BigInt(value); 5 | throw new Error('invalid bigint'); 6 | }); 7 | 8 | export const NumberFromString = coerce(number(), string(), (value): number => { 9 | if (typeof value === 'string') return Number(value); 10 | throw new Error('invalid number'); 11 | }); 12 | -------------------------------------------------------------------------------- /app/validators/pubkey.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | import { coerce, instance, string } from 'superstruct'; 3 | 4 | export const PublicKeyFromString = coerce(instance(PublicKey), string(), value => new PublicKey(value)); 5 | -------------------------------------------------------------------------------- /cache/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "telemetry": { 3 | "notifiedAt": "1683346064993", 4 | "enabled": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "tailwind": { 5 | "config": "tailwind.config.ts", 6 | "css": "app/styles.css", 7 | "baseColor": "neutral", 8 | "cssVariables": false, 9 | "prefix": "e-", 10 | "version": "v3" 11 | }, 12 | "rsc": false, 13 | "tsx": true, 14 | "aliases": { 15 | "utils": "@/app/components/shared/utils", 16 | "components": "@/app/components/shared" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | ### Creating new UI components 4 | 5 | For new components we use [shadcn/ui](https://ui.shadcn.com/docs). 6 | 7 | To generate a component, use this script: 8 | 9 | ```bash 10 | pnpm gen accordion 11 | ``` 12 | 13 | It translates needed component into `pnpx shadcn@version add accordion` and installs it. -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | autoprefixer: {}, 4 | 'postcss-import': {}, 5 | tailwindcss: {}, 6 | }, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-foundation/explorer/09098b354e6d82a53e0f557936e9a7753b60f719/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-foundation/explorer/09098b354e6d82a53e0f557936e9a7753b60f719/public/favicon.png -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-foundation/explorer/09098b354e6d82a53e0f557936e9a7753b60f719/public/icon-192.png -------------------------------------------------------------------------------- /public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-foundation/explorer/09098b354e6d82a53e0f557936e9a7753b60f719/public/icon-512.png -------------------------------------------------------------------------------- /public/img/dashkit/masks/avatar-group-hover-last.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/dashkit/masks/avatar-group-hover.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/dashkit/masks/avatar-group.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/dashkit/masks/avatar-status.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/dashkit/masks/icon-status.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Solana Explorer", 3 | "name": "Inspect transactions, accounts, blocks, and more on the Solana blockchain", 4 | "icons": [ 5 | { 6 | "src": "/icon-192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/icon-512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#1dd79b", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /test-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | if (!AbortSignal.timeout) { 4 | AbortSignal.timeout = ms => { 5 | const controller = new AbortController(); 6 | setTimeout(() => controller.abort(), ms); 7 | return controller.signal; 8 | }; 9 | } 10 | 11 | // Needed for @solana/web3.js to treat Uint8Arrays as Buffers 12 | // See https://github.com/anza-xyz/solana-pay/issues/106 13 | const originalHasInstance = Uint8Array[Symbol.hasInstance]; 14 | Object.defineProperty(Uint8Array, Symbol.hasInstance, { 15 | value(potentialInstance: any) { 16 | return originalHasInstance.call(this, potentialInstance) || Buffer.isBuffer(potentialInstance); 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"], 24 | "@components/*": ["./app/components/*"], 25 | "@img/*": ["./app/img/*"], 26 | "@providers/*": ["./app/providers/*"], 27 | "@utils/*": ["./app/utils/*"], 28 | "@validators/*": ["./app/validators/*"] 29 | } 30 | }, 31 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import path from 'path'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | const specWorkspace = (name = 'specs') => ({ 6 | environment: 'jsdom', 7 | globals: true, 8 | name, 9 | server: { 10 | deps: { 11 | inline: ['@noble', 'change-case', '@react-hook/previous'], 12 | }, 13 | }, 14 | setupFiles: ['./test-setup.ts'], 15 | testTimeout: 10000, 16 | }); 17 | 18 | export default defineConfig({ 19 | plugins: [react()], 20 | resolve: { 21 | alias: { 22 | '@/': path.resolve(__dirname, './'), 23 | 24 | '@/app': path.resolve(__dirname, './app'), 25 | '@/components': path.resolve(__dirname, './app/components'), 26 | '@/providers': path.resolve(__dirname, './app/providers'), 27 | '@/utils': path.resolve(__dirname, './app/utils'), 28 | '@/validators': path.resolve(__dirname, './app/validators'), 29 | 30 | // @ aliases 31 | '@app': path.resolve(__dirname, './app'), 32 | '@components': path.resolve(__dirname, './app/components'), 33 | '@providers': path.resolve(__dirname, './app/providers'), 34 | '@utils': path.resolve(__dirname, './app/utils'), 35 | '@validators': path.resolve(__dirname, './app/validators'), 36 | }, 37 | }, 38 | test: { 39 | coverage: { 40 | provider: 'v8', 41 | }, 42 | poolOptions: { 43 | threads: { 44 | useAtomics: true, 45 | }, 46 | }, 47 | ...specWorkspace(), 48 | }, 49 | }); 50 | --------------------------------------------------------------------------------