Skip to content

Commit

Permalink
Refactor search to remove legacy web3js dependency (#275)
Browse files Browse the repository at this point in the history
This PR refactors the search bar to remove the client-side legacy web3js
dependency. This means that the core app layout, shared by every page,
no longer pulls in legacy web3js

- The client-side call to `getDomainInfo` is replaced with a GET request
to a new API `/api/domain-info/[domain]`
- This API just calls `getDomainInfo` and returns the response as JSON.
This function and everything else in `utils/name-service` that can be
run on the server are extracted to a new `utils/domain-info`. This is
necessary because `utils/domain-info` has `use client` (needed for its
client-side hooks)
- The API has a 24h cache-response header. I haven't done any manual
client-side caching, Next's fetch de-dupe should be sufficient.
- For now I haven't changed how we get the domain info. Longer term I'd
like to support multiple domains:
#271
- I've extracted everything used by the search bar from `utils/tx` to a
new `utils/programs`. I've removed the legacy web3js dependency, which
was just used to pull in program public keys

The result of this is that some pages load ~100kb less JS. I'm not
exactly sure how some of these numbers work though, eg the homepage is
already not including web3js despite including the search bar.

<img width="1366" alt="Screenshot 2023-07-18 at 15 23 19"
src="https://github.com/solana-labs/explorer/assets/1711350/a1cc43d8-1f76-44ca-a558-1f9f8a8d2f3d">

We can also inspect the bundle before and after this change, this is the
app/layout endpoint. On master we have the legacy web3js
<img width="2387" alt="Screenshot 2023-07-18 at 15 01 10"
src="https://github.com/solana-labs/explorer/assets/1711350/fd0d4a6e-4e47-400a-b45f-fed2719415ef">

After we've stripped many of these dependencies out:
<img width="2388" alt="Screenshot 2023-07-18 at 15 02 33"
src="https://github.com/solana-labs/explorer/assets/1711350/04294a1c-03d7-4b67-8fcd-549cbc5a6215">

Mostly it shows that we should stop including that token list though...
#201
  • Loading branch information
mcintyre94 committed Jul 18, 2023
1 parent 7e0e619 commit 47006f0
Show file tree
Hide file tree
Showing 7 changed files with 526 additions and 514 deletions.
31 changes: 31 additions & 0 deletions app/api/domain-info/[domain]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Connection } from "@solana/web3.js"
import { NextResponse } from "next/server"

import { MAINNET_BETA_URL } from "@/app/utils/cluster"
import { getDomainInfo } from "@/app/utils/domain-info"

type Params = {
params: {
domain: string
}
}

export type FetchedDomainInfo = Awaited<ReturnType<typeof getDomainInfo>>;

export async function GET(
_request: Request,
{ params: { domain } }: Params
) {
// Intentionally using legacy web3js for compatibility with bonfida library
// This is an API route so won't affect client bundle
// We only fetch domains on mainnet
const connection = new Connection(MAINNET_BETA_URL);
const domainInfo = await getDomainInfo(domain, connection);

return NextResponse.json(domainInfo, {
headers: {
// 24 hours
"Cache-Control": "max-age=86400"
}
});
}
22 changes: 13 additions & 9 deletions app/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
import { useCluster } from '@providers/cluster';
import { useTokenRegistry } from '@providers/token-registry';
import { TokenInfoMap } from '@solana/spl-token-registry';
import { Connection } from '@solana/web3.js';
import { Cluster } from '@utils/cluster';
import { getDomainInfo, hasDomainSyntax } from '@utils/name-service';
import { LOADER_IDS, LoaderName, PROGRAM_INFO_BY_ID, SPECIAL_IDS, SYSVAR_IDS } from '@utils/tx';
import bs58 from 'bs58';
import { useRouter, useSearchParams } from 'next/navigation';
import React, { useId } from 'react';
import { Search } from 'react-feather';
import Select, { ActionMeta, InputActionMeta, ValueType } from 'react-select';

import { FetchedDomainInfo } from '../api/domain-info/[domain]/route';
import { LOADER_IDS, LoaderName, PROGRAM_INFO_BY_ID, SPECIAL_IDS, SYSVAR_IDS } from '../utils/programs';

interface SearchOptions {
label: string;
options: {
Expand All @@ -22,6 +22,10 @@ interface SearchOptions {
}[];
}

const hasDomainSyntax = (value: string) => {
return value.length > 4 && value.substring(value.length - 4) === '.sol';
};

export function SearchBar() {
const [search, setSearch] = React.useState('');
const searchRef = React.useRef('');
Expand All @@ -31,7 +35,7 @@ export function SearchBar() {
const selectRef = React.useRef<Select<any> | null>(null);
const router = useRouter();
const { tokenRegistry } = useTokenRegistry();
const { url, cluster, clusterInfo } = useCluster();
const { cluster, clusterInfo } = useCluster();
const searchParams = useSearchParams();
const onChange = ({ pathname }: ValueType<any, false>, meta: ActionMeta<any>) => {
if (meta.action === 'select-option') {
Expand All @@ -58,7 +62,7 @@ export function SearchBar() {
setSearchOptions(options);

// checking for non local search output
if (hasDomainSyntax(search)) {
if (hasDomainSyntax(search) && cluster === Cluster.MainnetBeta) {
// if search input is a potential domain we continue the loading state
domainSearch(options);
} else {
Expand All @@ -72,9 +76,8 @@ export function SearchBar() {
// 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);
const updatedOptions = await buildDomainOptions(search, options);
if (searchRef.current === searchTerm) {
setSearchOptions(updatedOptions);
// after attempting to fetch the domain name we can conclude the loading state
Expand Down Expand Up @@ -213,8 +216,9 @@ function buildTokenOptions(search: string, cluster: Cluster, tokenRegistry: Toke
}
}

async function buildDomainOptions(connection: Connection, search: string, options: SearchOptions[]) {
const domainInfo = await getDomainInfo(search, connection);
async function buildDomainOptions(search: string, options: SearchOptions[]) {
const domainInfoResponse = await fetch(`/api/domain-info/${search}`);
const domainInfo = await domainInfoResponse.json() as FetchedDomainInfo;
const updatedOptions: SearchOptions[] = [...options];
if (domainInfo && domainInfo.owner && domainInfo.address) {
updatedOptions.push({
Expand Down
4 changes: 3 additions & 1 deletion app/components/account/DomainsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import { Address } from '@components/common/Address';
import { ErrorCard } from '@components/common/ErrorCard';
import { LoadingCard } from '@components/common/LoadingCard';
import { DomainInfo, useUserDomains } from '@utils/name-service';
import { useUserDomains } from '@utils/name-service';
import React from 'react';

import { DomainInfo } from '@/app/utils/domain-info';

export function DomainsCard({ address }: { address: string }) {
const [domains, domainsLoading] = useUserDomains(address);

Expand Down
40 changes: 40 additions & 0 deletions app/utils/domain-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getHashedName, getNameAccountKey, getNameOwner } from "@bonfida/spl-name-service";
import { Connection, PublicKey } from "@solana/web3.js";

// Address of the SOL TLD
export const SOL_TLD_AUTHORITY = new PublicKey('58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx');

async function getDomainKey(name: string, nameClass?: PublicKey, nameParent?: PublicKey) {
const hashedDomainName = await getHashedName(name);
const nameKey = await getNameAccountKey(hashedDomainName, nameClass, nameParent);
return nameKey;
}

export interface DomainInfo {
name: string;
address: PublicKey;
}

export const hasDomainSyntax = (value: string) => {
return value.length > 4 && value.substring(value.length - 4) === '.sol';
};

// 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
? {
address: domainKey.toString(),
owner: registry.registry.owner.toString(),
}
: null;
} catch {
return null;
}
}
40 changes: 1 addition & 39 deletions app/utils/name-service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

import {
getFilteredProgramAccounts,
getHashedName,
getNameAccountKey,
getNameOwner,
NAME_PROGRAM_ID,
performReverseLookup,
} from '@bonfida/spl-name-service';
Expand All @@ -13,42 +10,7 @@ import { Connection, PublicKey } from '@solana/web3.js';
import { Cluster } from '@utils/cluster';
import { useEffect, useState } from 'react';

// Address of the SOL TLD
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
? {
address: domainKey.toString(),
owner: registry.registry.owner.toString(),
}
: null;
} catch {
return null;
}
}
import { DomainInfo, SOL_TLD_AUTHORITY } from './domain-info';

async function getUserDomainAddresses(connection: Connection, userAddress: string): Promise<PublicKey[]> {
const filters = [
Expand Down

1 comment on commit 47006f0

@vercel
Copy link

@vercel vercel bot commented on 47006f0 Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

explorer – ./

explorer.solana.com
explorer-git-master-solana-labs.vercel.app
explorer-solana-labs.vercel.app

Please sign in to comment.