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:
parent
6e03e0e987
commit
5de8061bed
|
@ -13,14 +13,30 @@ import {
|
||||||
import { Cluster, useCluster } from "providers/cluster";
|
import { Cluster, useCluster } from "providers/cluster";
|
||||||
import { useTokenRegistry } from "providers/mints/token-registry";
|
import { useTokenRegistry } from "providers/mints/token-registry";
|
||||||
import { TokenInfoMap } from "@solana/spl-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() {
|
export function SearchBar() {
|
||||||
const [search, setSearch] = React.useState("");
|
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 selectRef = React.useRef<StateManager<any> | null>(null);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { tokenRegistry } = useTokenRegistry();
|
const { tokenRegistry } = useTokenRegistry();
|
||||||
const { cluster, clusterInfo } = useCluster();
|
const { url, cluster, clusterInfo } = useCluster();
|
||||||
|
|
||||||
const onChange = (
|
const onChange = (
|
||||||
{ pathname }: ValueType<any, false>,
|
{ pathname }: ValueType<any, false>,
|
||||||
|
@ -33,7 +49,54 @@ export function SearchBar() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInputChange = (value: string, { action }: InputActionMeta) => {
|
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;
|
const resetValue = "" as any;
|
||||||
|
@ -44,13 +107,9 @@ export function SearchBar() {
|
||||||
<Select
|
<Select
|
||||||
autoFocus
|
autoFocus
|
||||||
ref={(ref) => (selectRef.current = ref)}
|
ref={(ref) => (selectRef.current = ref)}
|
||||||
options={buildOptions(
|
options={searchOptions}
|
||||||
search,
|
|
||||||
cluster,
|
|
||||||
tokenRegistry,
|
|
||||||
clusterInfo?.epochInfo.epoch
|
|
||||||
)}
|
|
||||||
noOptionsMessage={() => "No Results"}
|
noOptionsMessage={() => "No Results"}
|
||||||
|
loadingMessage={() => loadingSearchMessage}
|
||||||
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
|
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
|
||||||
value={resetValue}
|
value={resetValue}
|
||||||
inputValue={search}
|
inputValue={search}
|
||||||
|
@ -65,6 +124,7 @@ export function SearchBar() {
|
||||||
onInputChange={onInputChange}
|
onInputChange={onInputChange}
|
||||||
components={{ DropdownIndicator }}
|
components={{ DropdownIndicator }}
|
||||||
classNamePrefix="search-bar"
|
classNamePrefix="search-bar"
|
||||||
|
isLoading={loadingSearch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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(
|
function buildOptions(
|
||||||
rawSearch: string,
|
rawSearch: string,
|
||||||
cluster: Cluster,
|
cluster: Cluster,
|
||||||
|
@ -287,6 +380,7 @@ function buildOptions(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { PublicKey, Connection } from "@solana/web3.js";
|
import { PublicKey, Connection } from "@solana/web3.js";
|
||||||
import {
|
import {
|
||||||
getFilteredProgramAccounts,
|
getFilteredProgramAccounts,
|
||||||
|
getHashedName,
|
||||||
|
getNameAccountKey,
|
||||||
|
getNameOwner,
|
||||||
NAME_PROGRAM_ID,
|
NAME_PROGRAM_ID,
|
||||||
performReverseLookup,
|
performReverseLookup,
|
||||||
} from "@bonfida/spl-name-service";
|
} from "@bonfida/spl-name-service";
|
||||||
|
@ -11,10 +14,48 @@ import { Cluster, useCluster } from "providers/cluster";
|
||||||
const SOL_TLD_AUTHORITY = new PublicKey(
|
const SOL_TLD_AUTHORITY = new PublicKey(
|
||||||
"58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx"
|
"58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx"
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface DomainInfo {
|
export interface DomainInfo {
|
||||||
name: string;
|
name: string;
|
||||||
address: PublicKey;
|
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(
|
async function getUserDomainAddresses(
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
|
|
Loading…
Reference in New Issue