├── .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 |
16 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------