explorer: add domain names to account details (#20911)

This commit is contained in:
DR497 2021-10-26 10:14:24 +08:00 committed by GitHub
parent 6470560dd1
commit fb36f0085b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 192 additions and 1 deletions

View File

@ -5,6 +5,7 @@
"dependencies": {
"@blockworks-foundation/mango-client": "^3.1.1",
"@bonfida/bot": "^0.5.3",
"@bonfida/spl-name-service": "^0.1.22",
"@cloudflare/stream-react": "^1.2.0",
"@metamask/jazzicon": "^2.0.0",
"@metaplex/js": "2.0.1",

View File

@ -0,0 +1,61 @@
import React from "react";
import { PublicKey } from "@solana/web3.js";
import { useUserDomains, DomainInfo } from "../../utils/name-service";
import { LoadingCard } from "components/common/LoadingCard";
import { ErrorCard } from "components/common/ErrorCard";
import { Address } from "components/common/Address";
export function DomainsCard({ pubkey }: { pubkey: PublicKey }) {
const [domains, domainsLoading] = useUserDomains(pubkey);
if (domainsLoading && (!domains || domains.length === 0)) {
return <LoadingCard message="Loading domains" />;
} else if (!domains) {
return <ErrorCard text="Failed to fetch domains" />;
}
if (domains.length === 0) {
return <ErrorCard text="No domain name found" />;
}
return (
<div className="card">
<div className="card-header align-items-center">
<h3 className="card-header-title">Domain Names Owned</h3>
</div>
<div className="table-responsive mb-0">
<table className="table table-sm table-nowrap card-table">
<thead>
<tr>
<th className="text-muted">Domain name</th>
<th className="text-muted">Domain Address</th>
<th className="text-muted">Domain Class Address</th>
</tr>
</thead>
<tbody className="list">
{domains.map((domain) => (
<RenderDomainRow
key={domain.address.toBase58()}
domainInfo={domain}
/>
))}
</tbody>
</table>
</div>
</div>
);
}
function RenderDomainRow({ domainInfo }: { domainInfo: DomainInfo }) {
return (
<tr>
<td>{domainInfo.name}</td>
<td>
<Address pubkey={domainInfo.address} link />
</td>
<td>
<Address pubkey={domainInfo.class} link />
</td>
</tr>
);
}

View File

@ -37,6 +37,7 @@ import { TokenInstructionsCard } from "components/account/history/TokenInstructi
import { RewardsCard } from "components/account/RewardsCard";
import { MetaplexMetadataCard } from "components/account/MetaplexMetadataCard";
import { NFTHeader } from "components/account/MetaplexNFTHeader";
import { DomainsCard } from "components/account/DomainsCard";
const IDENTICON_WIDTH = 64;
@ -310,7 +311,8 @@ export type MoreTabs =
| "transfers"
| "instructions"
| "rewards"
| "metadata";
| "metadata"
| "domains";
function MoreSection({
account,
@ -379,6 +381,7 @@ function MoreSection({
nftData={(account.details?.data as TokenProgramData).nftData!}
/>
)}
{tab === "domains" && <DomainsCard pubkey={pubkey} />}
</>
);
}
@ -426,6 +429,11 @@ function getTabs(data?: ProgramData): Tab[] {
title: "Tokens",
path: "/tokens",
});
tabs.push({
slug: "domains",
title: "Domains",
path: "/domains",
});
}
return tabs;

View File

@ -0,0 +1,121 @@
import { PublicKey, Connection } from "@solana/web3.js";
import {
getHashedName,
getNameAccountKey,
NameRegistryState,
getFilteredProgramAccounts,
NAME_PROGRAM_ID,
} from "@bonfida/spl-name-service";
import BN from "bn.js";
import { useState, useEffect } from "react";
import { Cluster, useCluster } from "providers/cluster";
// Name auctionning Program ID
export const PROGRAM_ID = new PublicKey(
"jCebN34bUfdeUYJT13J1yG16XWQpt5PDx6Mse9GUqhR"
);
export interface DomainInfo {
name: string;
address: PublicKey;
class: PublicKey;
}
async function getDomainKey(
name: string,
nameClass?: PublicKey,
nameParent?: PublicKey
) {
const hashedDomainName = await getHashedName(name);
const nameKey = await getNameAccountKey(
hashedDomainName,
nameClass,
nameParent
);
return nameKey;
}
export async function findOwnedNameAccountsForUser(
connection: Connection,
userAccount: PublicKey
): Promise<PublicKey[]> {
const filters = [
{
memcmp: {
offset: 32,
bytes: userAccount.toBase58(),
},
},
];
const accounts = await getFilteredProgramAccounts(
connection,
NAME_PROGRAM_ID,
filters
);
return accounts.map((a) => a.publicKey);
}
export async function performReverseLookup(
connection: Connection,
nameAccounts: PublicKey[]
): Promise<DomainInfo[]> {
let [centralState] = await PublicKey.findProgramAddress(
[PROGRAM_ID.toBuffer()],
PROGRAM_ID
);
const reverseLookupAccounts = await Promise.all(
nameAccounts.map((name) => getDomainKey(name.toBase58(), centralState))
);
let names = await NameRegistryState.retrieveBatch(
connection,
reverseLookupAccounts
);
return names
.map((name) => {
if (!name?.data) {
return undefined;
}
const nameLength = new BN(name!.data.slice(0, 4), "le").toNumber();
return {
name: name.data.slice(4, 4 + nameLength).toString() + ".sol",
address: name.address,
class: name.class,
};
})
.filter((e) => !!e) as DomainInfo[];
}
export const useUserDomains = (
address: PublicKey
): [DomainInfo[] | null, boolean] => {
const { url, cluster } = useCluster();
const [result, setResult] = useState<DomainInfo[] | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const resolve = async () => {
// Allow only mainnet and custom
if (![Cluster.MainnetBeta, Cluster.Custom].includes(cluster)) return;
const connection = new Connection(url, "confirmed");
try {
setLoading(true);
const domains = await findOwnedNameAccountsForUser(connection, address);
let names = await performReverseLookup(connection, domains);
names.sort((a, b) => {
return a.name.localeCompare(b.name);
});
setResult(names);
} catch (err) {
console.log(`Error fetching user domains ${err}`);
} finally {
setLoading(false);
}
};
resolve();
}, [address, url]); // eslint-disable-line react-hooks/exhaustive-deps
return [result, loading];
};