look up domain owner on .sol search (explorer) (#24300)

* lookup domain owner on .sol search

* add detected domain names to search options

* lookup domain owner on .sol search

* add detected domain names to search options

* add loading state and only append domain search results if search state has not changed

* rm url and rename fn

* useRef to check if domain lookup is still valid
This commit is contained in:
Dennis Antela Martinez 2022-04-16 00:28:44 -07:00 committed by GitHub
parent 6e03e0e987
commit 5de8061bed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 143 additions and 8 deletions

View File

@ -13,14 +13,30 @@ import {
import { Cluster, useCluster } from "providers/cluster";
import { useTokenRegistry } from "providers/mints/token-registry";
import { TokenInfoMap } from "@solana/spl-token-registry";
import { Connection } from "@solana/web3.js";
import { getDomainInfo, hasDomainSyntax } from "utils/name-service";
interface SearchOptions {
label: string;
options: {
label: string;
value: string[];
pathname: string;
}[];
}
export function SearchBar() {
const [search, setSearch] = React.useState("");
const searchRef = React.useRef("");
const [searchOptions, setSearchOptions] = React.useState<SearchOptions[]>([]);
const [loadingSearch, setLoadingSearch] = React.useState<boolean>(false);
const [loadingSearchMessage, setLoadingSearchMessage] =
React.useState<string>("loading...");
const selectRef = React.useRef<StateManager<any> | null>(null);
const history = useHistory();
const location = useLocation();
const { tokenRegistry } = useTokenRegistry();
const { cluster, clusterInfo } = useCluster();
const { url, cluster, clusterInfo } = useCluster();
const onChange = (
{ pathname }: ValueType<any, false>,
@ -33,7 +49,54 @@ export function SearchBar() {
};
const onInputChange = (value: string, { action }: InputActionMeta) => {
if (action === "input-change") setSearch(value);
if (action === "input-change") {
setSearch(value);
}
};
React.useEffect(() => {
searchRef.current = search;
setLoadingSearchMessage("Loading...");
setLoadingSearch(true);
// builds and sets local search output
const options = buildOptions(
search,
cluster,
tokenRegistry,
clusterInfo?.epochInfo.epoch
);
setSearchOptions(options);
// checking for non local search output
if (hasDomainSyntax(search)) {
// if search input is a potential domain we continue the loading state
domainSearch(options);
} else {
// if search input is not a potential domain we can conclude the search has finished
setLoadingSearch(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search]);
// appends domain lookup results to the local search state
const domainSearch = async (options: SearchOptions[]) => {
setLoadingSearchMessage("Looking up domain...");
const connection = new Connection(url);
const searchTerm = search;
const updatedOptions = await buildDomainOptions(
connection,
search,
options
);
if (searchRef.current === searchTerm) {
setSearchOptions(updatedOptions);
// after attempting to fetch the domain name we can conclude the loading state
setLoadingSearch(false);
setLoadingSearchMessage("Loading...");
}
};
const resetValue = "" as any;
@ -44,13 +107,9 @@ export function SearchBar() {
<Select
autoFocus
ref={(ref) => (selectRef.current = ref)}
options={buildOptions(
search,
cluster,
tokenRegistry,
clusterInfo?.epochInfo.epoch
)}
options={searchOptions}
noOptionsMessage={() => "No Results"}
loadingMessage={() => loadingSearchMessage}
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
value={resetValue}
inputValue={search}
@ -65,6 +124,7 @@ export function SearchBar() {
onInputChange={onInputChange}
components={{ DropdownIndicator }}
classNamePrefix="search-bar"
isLoading={loadingSearch}
/>
</div>
</div>
@ -196,6 +256,39 @@ function buildTokenOptions(
}
}
async function buildDomainOptions(
connection: Connection,
search: string,
options: SearchOptions[]
) {
const domainInfo = await getDomainInfo(search, connection);
const updatedOptions: SearchOptions[] = [...options];
if (domainInfo && domainInfo.owner && domainInfo.address) {
updatedOptions.push({
label: "Domain Owner",
options: [
{
label: domainInfo.owner,
value: [search],
pathname: "/address/" + domainInfo.owner,
},
],
});
updatedOptions.push({
label: "Name Service Account",
options: [
{
label: search,
value: [search],
pathname: "/address/" + domainInfo.address,
},
],
});
}
return updatedOptions;
}
// builds local search options
function buildOptions(
rawSearch: string,
cluster: Cluster,
@ -287,6 +380,7 @@ function buildOptions(
});
}
} catch (err) {}
return options;
}

View File

@ -1,6 +1,9 @@
import { PublicKey, Connection } from "@solana/web3.js";
import {
getFilteredProgramAccounts,
getHashedName,
getNameAccountKey,
getNameOwner,
NAME_PROGRAM_ID,
performReverseLookup,
} from "@bonfida/spl-name-service";
@ -11,10 +14,48 @@ import { Cluster, useCluster } from "providers/cluster";
const SOL_TLD_AUTHORITY = new PublicKey(
"58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx"
);
export interface DomainInfo {
name: string;
address: PublicKey;
}
export const hasDomainSyntax = (value: string) => {
return value.length > 4 && value.substring(value.length - 4) === ".sol";
};
async function getDomainKey(
name: string,
nameClass?: PublicKey,
nameParent?: PublicKey
) {
const hashedDomainName = await getHashedName(name);
const nameKey = await getNameAccountKey(
hashedDomainName,
nameClass,
nameParent
);
return nameKey;
}
// returns non empty wallet string if a given .sol domain is owned by a wallet
export async function getDomainInfo(domain: string, connection: Connection) {
const domainKey = await getDomainKey(
domain.slice(0, -4), // remove .sol
undefined,
SOL_TLD_AUTHORITY
);
try {
const registry = await getNameOwner(connection, domainKey);
return registry && registry.registry.owner
? {
owner: registry.registry.owner.toString(),
address: domainKey.toString(),
}
: null;
} catch {
return null;
}
}
async function getUserDomainAddresses(
connection: Connection,