explorer: add domain names to account details (#20911)
This commit is contained in:
parent
6470560dd1
commit
fb36f0085b
|
@ -5,6 +5,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blockworks-foundation/mango-client": "^3.1.1",
|
"@blockworks-foundation/mango-client": "^3.1.1",
|
||||||
"@bonfida/bot": "^0.5.3",
|
"@bonfida/bot": "^0.5.3",
|
||||||
|
"@bonfida/spl-name-service": "^0.1.22",
|
||||||
"@cloudflare/stream-react": "^1.2.0",
|
"@cloudflare/stream-react": "^1.2.0",
|
||||||
"@metamask/jazzicon": "^2.0.0",
|
"@metamask/jazzicon": "^2.0.0",
|
||||||
"@metaplex/js": "2.0.1",
|
"@metaplex/js": "2.0.1",
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ import { TokenInstructionsCard } from "components/account/history/TokenInstructi
|
||||||
import { RewardsCard } from "components/account/RewardsCard";
|
import { RewardsCard } from "components/account/RewardsCard";
|
||||||
import { MetaplexMetadataCard } from "components/account/MetaplexMetadataCard";
|
import { MetaplexMetadataCard } from "components/account/MetaplexMetadataCard";
|
||||||
import { NFTHeader } from "components/account/MetaplexNFTHeader";
|
import { NFTHeader } from "components/account/MetaplexNFTHeader";
|
||||||
|
import { DomainsCard } from "components/account/DomainsCard";
|
||||||
|
|
||||||
const IDENTICON_WIDTH = 64;
|
const IDENTICON_WIDTH = 64;
|
||||||
|
|
||||||
|
@ -310,7 +311,8 @@ export type MoreTabs =
|
||||||
| "transfers"
|
| "transfers"
|
||||||
| "instructions"
|
| "instructions"
|
||||||
| "rewards"
|
| "rewards"
|
||||||
| "metadata";
|
| "metadata"
|
||||||
|
| "domains";
|
||||||
|
|
||||||
function MoreSection({
|
function MoreSection({
|
||||||
account,
|
account,
|
||||||
|
@ -379,6 +381,7 @@ function MoreSection({
|
||||||
nftData={(account.details?.data as TokenProgramData).nftData!}
|
nftData={(account.details?.data as TokenProgramData).nftData!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{tab === "domains" && <DomainsCard pubkey={pubkey} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -426,6 +429,11 @@ function getTabs(data?: ProgramData): Tab[] {
|
||||||
title: "Tokens",
|
title: "Tokens",
|
||||||
path: "/tokens",
|
path: "/tokens",
|
||||||
});
|
});
|
||||||
|
tabs.push({
|
||||||
|
slug: "domains",
|
||||||
|
title: "Domains",
|
||||||
|
path: "/domains",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return tabs;
|
return tabs;
|
||||||
|
|
|
@ -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];
|
||||||
|
};
|
Loading…
Reference in New Issue