diff --git a/app/address/[address]/layout.tsx b/app/address/[address]/layout.tsx
index bfb23cd3..13656727 100644
--- a/app/address/[address]/layout.tsx
+++ b/app/address/[address]/layout.tsx
@@ -24,6 +24,7 @@ import {
AccountsProvider,
isTokenProgramData,
TokenProgramData,
+ UpgradeableLoaderAccountData,
useAccountInfo,
useFetchAccountInfo,
useMintAccountInfo,
@@ -49,6 +50,7 @@ import { Address } from 'web3js-experimental';
import { CompressedNftAccountHeader, CompressedNftCard } from '@/app/components/account/CompressedNftCard';
import { useCompressedNft, useMetadataJsonLink } from '@/app/providers/compressed-nft';
+import { useSquadsMultisigLookup } from '@/app/providers/squadsMultisig';
import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info';
import { MintAccountInfo } from '@/app/validators/accounts/token';
@@ -460,7 +462,7 @@ function DetailsSections({
}
const account = info.data;
- const tabComponents = getTabs(pubkey, account).concat(getAnchorTabs(pubkey, account));
+ const tabComponents = getTabs(pubkey, account).concat(getCustomLinkedTabs(pubkey, account));
if (tab && tabComponents.filter(tabComponent => tabComponent.tab.slug === tab).length === 0) {
redirect(`/address/${address}`);
@@ -561,7 +563,8 @@ export type MoreTabs =
| 'entries'
| 'concurrent-merkle-tree'
| 'compression'
- | 'verified-build';
+ | 'verified-build'
+ | 'program-multisig';
function MoreSection({ children, tabs }: { children: React.ReactNode; tabs: (JSX.Element | null)[] }) {
return (
@@ -694,8 +697,26 @@ function Tab({ address, path, title }: { address: string; path: string; title: s
);
}
-function getAnchorTabs(pubkey: PublicKey, account: Account) {
+function getCustomLinkedTabs(pubkey: PublicKey, account: Account) {
const tabComponents = [];
+ const programMultisigTab: Tab = {
+ path: 'program-multisig',
+ slug: 'program-multisig',
+ title: 'Program Multisig',
+ };
+ tabComponents.push({
+ component: (
+ >}>
+
+
+ ),
+ tab: programMultisigTab,
+ });
+
const anchorProgramTab: Tab = {
path: 'anchor-program',
slug: 'anchor-program',
@@ -786,3 +807,31 @@ function CompressedNftLink({ tab, address, pubkey }: { tab: Tab; address: string
);
}
+
+// Checks that a program multisig exists at the given address and returns a link to the tab
+function ProgramMultisigLink({
+ tab,
+ address,
+ authority,
+}: {
+ tab: Tab;
+ address: string;
+ authority: PublicKey | null | undefined;
+}) {
+ const { cluster } = useCluster();
+ const { data: squadMapInfo, error } = useSquadsMultisigLookup(authority, cluster);
+ const tabPath = useClusterPath({ pathname: `/address/${address}/${tab.path}` });
+ const selectedLayoutSegment = useSelectedLayoutSegment();
+ const isActive = selectedLayoutSegment === tab.path;
+ if (!squadMapInfo || error || !squadMapInfo.isSquad) {
+ return null;
+ }
+
+ return (
+
+
+ {tab.title}
+
+
+ );
+}
diff --git a/app/address/[address]/program-multisig/page-client.tsx b/app/address/[address]/program-multisig/page-client.tsx
new file mode 100644
index 00000000..8e26f100
--- /dev/null
+++ b/app/address/[address]/program-multisig/page-client.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import { ParsedAccountRenderer } from '@components/account/ParsedAccountRenderer';
+import React from 'react';
+
+import { ProgramMultisigCard } from '@/app/components/account/ProgramMultisigCard';
+
+type Props = Readonly<{
+ params: {
+ address: string;
+ };
+}>;
+
+function ProgramMultisigCardRenderer({
+ account,
+ onNotFound,
+}: React.ComponentProps['renderComponent']>) {
+ const parsedData = account?.data?.parsed;
+ if (!parsedData || parsedData?.program !== 'bpf-upgradeable-loader') {
+ return onNotFound();
+ }
+ return ;
+}
+
+export default function ProgramMultisigPageClient({ params: { address } }: Props) {
+ return ;
+}
diff --git a/app/address/[address]/program-multisig/page.tsx b/app/address/[address]/program-multisig/page.tsx
new file mode 100644
index 00000000..a7af2418
--- /dev/null
+++ b/app/address/[address]/program-multisig/page.tsx
@@ -0,0 +1,21 @@
+import getReadableTitleFromAddress, { AddressPageMetadataProps } from '@utils/get-readable-title-from-address';
+import { Metadata } from 'next/types';
+
+import ProgramMultisigPageClient from './page-client';
+
+export async function generateMetadata(props: AddressPageMetadataProps): Promise {
+ return {
+ description: `Multisig information for the upgrade authority of the program with address ${props.params.address} on Solana`,
+ title: `Upgrade Authority Multisig | ${await getReadableTitleFromAddress(props)} | Solana`,
+ };
+}
+
+type Props = Readonly<{
+ params: {
+ address: string;
+ };
+}>;
+
+export default function ProgramMultisigPage(props: Props) {
+ return ;
+}
diff --git a/app/address/[address]/verified-build/page-client.tsx b/app/address/[address]/verified-build/page-client.tsx
index 89ab035d..cd09a5c0 100644
--- a/app/address/[address]/verified-build/page-client.tsx
+++ b/app/address/[address]/verified-build/page-client.tsx
@@ -22,6 +22,6 @@ function VerifiedBuildCardRenderer({
return ;
}
-export default function SecurityPageClient({ params: { address } }: Props) {
+export default function VerifiedBuildPageClient({ params: { address } }: Props) {
return ;
}
diff --git a/app/components/account/ProgramMultisigCard.tsx b/app/components/account/ProgramMultisigCard.tsx
new file mode 100644
index 00000000..bcf7c0b1
--- /dev/null
+++ b/app/components/account/ProgramMultisigCard.tsx
@@ -0,0 +1,97 @@
+import { PublicKey } from '@solana/web3.js';
+import { Suspense } from 'react';
+
+import { UpgradeableLoaderAccountData } from '@/app/providers/accounts';
+import { useAnchorProgram } from '@/app/providers/anchor';
+import { useCluster } from '@/app/providers/cluster';
+import {
+ SQUADS_V3_ADDRESS,
+ SQUADS_V4_ADDRESS,
+ useSquadsMultisig,
+ useSquadsMultisigLookup,
+} from '@/app/providers/squadsMultisig';
+
+import { Address } from '../common/Address';
+import { LoadingCard } from '../common/LoadingCard';
+import { TableCardBody } from '../common/TableCardBody';
+
+export function ProgramMultisigCard({ data }: { data: UpgradeableLoaderAccountData }) {
+ return (
+ }>
+
+
+ );
+}
+
+function ProgramMultisigCardInner({ programAuthority }: { programAuthority: PublicKey | null | undefined }) {
+ const { cluster, url } = useCluster();
+ const { data: squadMapInfo } = useSquadsMultisigLookup(programAuthority, cluster);
+ const anchorProgram = useAnchorProgram(squadMapInfo?.version === 'v3' ? SQUADS_V3_ADDRESS : SQUADS_V4_ADDRESS, url);
+ const { data: squadInfo } = useSquadsMultisig(
+ anchorProgram.program,
+ squadMapInfo?.multisig,
+ cluster,
+ squadMapInfo?.version
+ );
+
+ let members: PublicKey[];
+ if (squadInfo !== undefined && squadInfo?.version === 'v4') {
+ members = squadInfo.multisig.members.map(obj => obj.key) ?? [];
+ } else {
+ members = squadInfo?.multisig.keys ?? [];
+ }
+
+ return (
+
+
+
+ Upgrade Authority Multisig Information
+
+
+
+
+ Multisig Program |
+ {squadMapInfo?.version === 'v4' ? 'Squads V4' : 'Squads V3'} |
+
+
+ Multisig Program Id |
+
+
+ |
+
+
+ Multisig Account |
+
+ {squadMapInfo?.isSquad ? (
+
+ ) : null}
+ |
+
+
+ Multisig Approval Threshold |
+
+ {squadInfo?.multisig.threshold}
+ {' of '}
+ {squadInfo?.version === 'v4'
+ ? squadInfo?.multisig.members.length
+ : squadInfo?.multisig.keys.length}
+ |
+
+ {members.map((member, idx) => (
+
+ Multisig Member {idx + 1} |
+
+
+ |
+
+ ))}
+
+
+ );
+}
diff --git a/app/components/account/UpgradeableLoaderAccountSection.tsx b/app/components/account/UpgradeableLoaderAccountSection.tsx
index 03623d0f..5c4b2af5 100644
--- a/app/components/account/UpgradeableLoaderAccountSection.tsx
+++ b/app/components/account/UpgradeableLoaderAccountSection.tsx
@@ -8,6 +8,7 @@ import { SolBalance } from '@components/common/SolBalance';
import { TableCardBody } from '@components/common/TableCardBody';
import { Account, useFetchAccountInfo } from '@providers/accounts';
import { useCluster } from '@providers/cluster';
+import { PublicKey } from '@solana/web3.js';
import { addressLabel } from '@utils/tx';
import {
ProgramAccountInfo,
@@ -19,6 +20,10 @@ import Link from 'next/link';
import React from 'react';
import { ExternalLink, RefreshCw } from 'react-feather';
+import { useSquadsMultisigLookup } from '@/app/providers/squadsMultisig';
+import { Cluster } from '@/app/utils/cluster';
+import { useClusterPath } from '@/app/utils/url';
+
import { VerifiedProgramBadge } from '../common/VerifiedProgramBadge';
export function UpgradeableLoaderAccountSection({
@@ -63,7 +68,10 @@ export function UpgradeableProgramSection({
}) {
const refresh = useFetchAccountInfo();
const { cluster } = useCluster();
+ const { data: squadMapInfo } = useSquadsMultisigLookup(programData?.authority, cluster);
+
const label = addressLabel(account.pubkey.toBase58(), cluster);
+
return (
@@ -134,12 +142,17 @@ export function UpgradeableProgramSection({
{programData.authority !== null && (
-
- Upgrade Authority |
-
-
- |
-
+ <>
+
+ Upgrade Authority |
+
+ {cluster == Cluster.MainnetBeta && squadMapInfo?.isSquad ? (
+
+ ) : null}
+
+ |
+
+ >
)}
>
)}
@@ -148,6 +161,17 @@ export function UpgradeableProgramSection({
);
}
+function MultisigBadge({ pubkey }: { pubkey: PublicKey }) {
+ const programMultisigTabPath = useClusterPath({ pathname: `/address/${pubkey.toBase58()}/program-multisig` });
+ return (
+
+
+ Program Multisig
+
+
+ );
+}
+
function SecurityLabel() {
return (
diff --git a/app/providers/squadsMultisig.tsx b/app/providers/squadsMultisig.tsx
new file mode 100644
index 00000000..724f07cf
--- /dev/null
+++ b/app/providers/squadsMultisig.tsx
@@ -0,0 +1,69 @@
+import { Program } from '@coral-xyz/anchor';
+import { PublicKey } from '@solana/web3.js';
+import { Cluster } from '@utils/cluster';
+import useSWRImmutable from 'swr/immutable';
+
+export const SQUADS_V3_ADDRESS = 'SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu';
+export const SQUADS_V4_ADDRESS = 'SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf';
+
+export type SquadsMultisigVersion = 'v3' | 'v4';
+export type SquadsMultisigMapInfo = {
+ isSquad: boolean;
+ version: SquadsMultisigVersion;
+ multisig: string;
+};
+export type MinimalMultisigInfo =
+ | { version: 'v4'; multisig: { threshold: number; members: Array<{ key: PublicKey }> } }
+ | { version: 'v3'; multisig: { threshold: number; keys: Array } };
+
+const SQUADS_MAP_URL = 'https://4fnetmviidiqkjzenwxe66vgoa0soerr.lambda-url.us-east-1.on.aws/isSquadV2';
+
+// Squads Multisig reverse map info is only available on mainnet
+export function useSquadsMultisigLookup(programAuthority: PublicKey | null | undefined, cluster: Cluster) {
+ return useSWRImmutable(
+ ['squadsReverseMap', programAuthority?.toString(), cluster],
+ async ([_prefix, programIdString, cluster]: [string, string | undefined, Cluster]) => {
+ if (cluster !== Cluster.MainnetBeta || !programIdString) {
+ return null;
+ }
+ const response = await fetch(`${SQUADS_MAP_URL}/${programIdString}`);
+ const data = await response.json();
+ return 'error' in data ? null : (data as SquadsMultisigMapInfo);
+ },
+ { suspense: true }
+ );
+}
+
+export function useSquadsMultisig(
+ anchorProgram: Program | null | undefined,
+ multisig: string | undefined,
+ cluster: Cluster,
+ version: SquadsMultisigVersion | undefined
+) {
+ return useSWRImmutable(
+ ['squadsMultisig', multisig, cluster],
+ async ([_prefix, multisig, cluster]: [string, string | undefined, Cluster]) => {
+ if (cluster !== Cluster.MainnetBeta || !multisig || !version) {
+ return null;
+ }
+ if (version === 'v4') {
+ const multisigInfo = await (anchorProgram?.account as unknown as any).multisig.fetch(
+ multisig,
+ 'confirmed'
+ );
+ return {
+ multisig: multisigInfo,
+ version,
+ };
+ } else if (version === 'v3') {
+ const multisigInfo = await (anchorProgram?.account as unknown as any).ms.fetch(multisig, 'confirmed');
+ return {
+ multisig: multisigInfo,
+ version,
+ };
+ } else {
+ return null;
+ }
+ }
+ );
+}
diff --git a/package.json b/package.json
index 2b186482..a699544a 100644
--- a/package.json
+++ b/package.json
@@ -104,5 +104,6 @@
"node-fetch": "^2.6.9",
"uuid": "^9.0.0"
}
- }
+ },
+ "packageManager": "pnpm@9.10.0+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c"
}