Skip to content

Commit

Permalink
Add squads program auth integration (#379)
Browse files Browse the repository at this point in the history
  • Loading branch information
ngundotra authored Oct 1, 2024
1 parent b46e0e4 commit 253496f
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 11 deletions.
55 changes: 52 additions & 3 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
AccountsProvider,
isTokenProgramData,
TokenProgramData,
UpgradeableLoaderAccountData,
useAccountInfo,
useFetchAccountInfo,
useMintAccountInfo,
Expand All @@ -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';

Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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: (
<React.Suspense key={programMultisigTab.slug} fallback={<></>}>
<ProgramMultisigLink
tab={programMultisigTab}
address={pubkey.toString()}
authority={(account.data.parsed as UpgradeableLoaderAccountData | undefined)?.programData?.authority}
/>
</React.Suspense>
),
tab: programMultisigTab,
});

const anchorProgramTab: Tab = {
path: 'anchor-program',
slug: 'anchor-program',
Expand Down Expand Up @@ -786,3 +807,31 @@ function CompressedNftLink({ tab, address, pubkey }: { tab: Tab; address: string
</li>
);
}

// 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 (
<li key={tab.slug} className="nav-item">
<Link className={`${isActive ? 'active ' : ''}nav-link`} href={tabPath}>
{tab.title}
</Link>
</li>
);
}
27 changes: 27 additions & 0 deletions app/address/[address]/program-multisig/page-client.tsx
Original file line number Diff line number Diff line change
@@ -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<React.ComponentProps<typeof ParsedAccountRenderer>['renderComponent']>) {
const parsedData = account?.data?.parsed;
if (!parsedData || parsedData?.program !== 'bpf-upgradeable-loader') {
return onNotFound();
}
return <ProgramMultisigCard data={parsedData} />;
}

export default function ProgramMultisigPageClient({ params: { address } }: Props) {
return <ParsedAccountRenderer address={address} renderComponent={ProgramMultisigCardRenderer} />;
}
21 changes: 21 additions & 0 deletions app/address/[address]/program-multisig/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
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 <ProgramMultisigPageClient {...props} />;
}
2 changes: 1 addition & 1 deletion app/address/[address]/verified-build/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ function VerifiedBuildCardRenderer({
return <VerifiedBuildCard data={parsedData} pubkey={account.pubkey} />;
}

export default function SecurityPageClient({ params: { address } }: Props) {
export default function VerifiedBuildPageClient({ params: { address } }: Props) {
return <ParsedAccountRenderer address={address} renderComponent={VerifiedBuildCardRenderer} />;
}
97 changes: 97 additions & 0 deletions app/components/account/ProgramMultisigCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Suspense fallback={<LoadingCard message="Loading multisig information" />}>
<ProgramMultisigCardInner programAuthority={data.programData?.authority} />
</Suspense>
);
}

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 (
<div className="card security-txt">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">
Upgrade Authority Multisig Information
</h3>
</div>
<TableCardBody>
<tr>
<td>Multisig Program</td>
<td className="text-lg-end">{squadMapInfo?.version === 'v4' ? 'Squads V4' : 'Squads V3'}</td>
</tr>
<tr>
<td>Multisig Program Id</td>
<td className="text-lg-end">
<Address
pubkey={
new PublicKey(squadMapInfo?.version === 'v4' ? SQUADS_V4_ADDRESS : SQUADS_V3_ADDRESS)
}
alignRight
link
/>
</td>
</tr>
<tr>
<td>Multisig Account</td>
<td className="text-lg-end">
{squadMapInfo?.isSquad ? (
<Address pubkey={new PublicKey(squadMapInfo.multisig)} alignRight link />
) : null}
</td>
</tr>
<tr>
<td>Multisig Approval Threshold</td>
<td className="text-lg-end">
{squadInfo?.multisig.threshold}
{' of '}
{squadInfo?.version === 'v4'
? squadInfo?.multisig.members.length
: squadInfo?.multisig.keys.length}
</td>
</tr>
{members.map((member, idx) => (
<tr key={idx}>
<td>Multisig Member {idx + 1}</td>
<td className="text-lg-end">
<Address pubkey={member} alignRight link />
</td>
</tr>
))}
</TableCardBody>
</div>
);
}
36 changes: 30 additions & 6 deletions app/components/account/UpgradeableLoaderAccountSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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({
Expand Down Expand Up @@ -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 (
<div className="card">
<div className="card-header">
Expand Down Expand Up @@ -134,12 +142,17 @@ export function UpgradeableProgramSection({
</td>
</tr>
{programData.authority !== null && (
<tr>
<td>Upgrade Authority</td>
<td className="text-lg-end">
<Address pubkey={programData.authority} alignRight link />
</td>
</tr>
<>
<tr>
<td>Upgrade Authority</td>
<td className="text-lg-end">
{cluster == Cluster.MainnetBeta && squadMapInfo?.isSquad ? (
<MultisigBadge pubkey={account.pubkey} />
) : null}
<Address pubkey={programData.authority} alignRight link />
</td>
</tr>
</>
)}
</>
)}
Expand All @@ -148,6 +161,17 @@ export function UpgradeableProgramSection({
);
}

function MultisigBadge({ pubkey }: { pubkey: PublicKey }) {
const programMultisigTabPath = useClusterPath({ pathname: `/address/${pubkey.toBase58()}/program-multisig` });
return (
<h3 className="mb-0">
<Link className="badge bg-success-soft rank" href={programMultisigTabPath}>
Program Multisig
</Link>
</h3>
);
}

function SecurityLabel() {
return (
<InfoTooltip text="Security.txt helps security researchers to contact developers if they find security bugs.">
Expand Down
69 changes: 69 additions & 0 deletions app/providers/squadsMultisig.tsx
Original file line number Diff line number Diff line change
@@ -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<PublicKey> } };

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<SquadsMultisigMapInfo | null>(
['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<MinimalMultisigInfo | null>(
['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;
}
}
);
}
Loading

0 comments on commit 253496f

Please sign in to comment.