Skip to content

Commit 5fbe057

Browse files
committed
refactor domain resolution & optimize validateAddress
1 parent 80f6353 commit 5fbe057

File tree

8 files changed

+139
-84
lines changed

8 files changed

+139
-84
lines changed

src/app/profile/edit/components/WalletLinkForm.tsx

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,21 @@ import { Input } from "@/components/ui/input";
55
import { Label } from "@/components/ui/label";
66
import { Button } from "@/components/ui/button";
77
import { Loader2, Info, Shield, ArrowRight } from "lucide-react";
8-
import {
9-
getViemClient,
10-
isAddress as isEvmAddressAsync,
11-
normalizeEns,
12-
} from "@/lib/walletLinking/viem";
138
import { LinkedWallet } from "@/lib/walletLinking/readmeUtils";
14-
import { resolveSolDomain } from "@/lib/walletLinking/sns";
9+
import { validateAddress } from "@/lib/walletLinking/chainUtils";
10+
import {
11+
resolveSnsDomain,
12+
resolveEnsDomain,
13+
validateEnsFormat,
14+
validateSnsFormat,
15+
} from "@/lib/walletLinking/domain";
1516

1617
interface WalletLinkFormProps {
1718
wallets: LinkedWallet[];
1819
onSubmit: (wallets: LinkedWallet[]) => Promise<void>;
1920
isProcessing: boolean;
2021
}
2122

22-
// Basic regex for Solana address (Base58, 32-44 chars)
23-
// For more robust validation, consider @solana/web3.js PublicKey.isOnCurve or similar
24-
const SOL_ADDRESS_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
25-
26-
// ENS name regex (name.eth format)
27-
// Matches names that end with .eth and contain valid characters
28-
const ENS_NAME_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.eth$/;
29-
30-
// SNS name regex (name.sol format)
31-
// Matches names that end with .sol and contain valid characters
32-
const SNS_NAME_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.sol$/;
33-
3423
export function WalletLinkForm({
3524
wallets = [],
3625
onSubmit,
@@ -56,15 +45,15 @@ export function WalletLinkForm({
5645
// Validate initial addresses asynchronously
5746
const validateInitialAddresses = async () => {
5847
if (ethWallet?.address) {
59-
const isValid = await isEvmAddressAsync(ethWallet.address);
48+
const isValid = await validateAddress(ethWallet.address, "ethereum");
6049
setIsEthValid(isValid);
6150
if (!isValid) {
6251
setEthAddressError("Invalid Ethereum address");
6352
}
6453
}
6554

6655
if (solWallet?.address) {
67-
const isValid = SOL_ADDRESS_REGEX.test(solWallet.address);
56+
const isValid = await validateAddress(solWallet.address, "solana");
6857
setIsSolValid(isValid);
6958
if (!isValid) {
7059
setSolAddressError("Invalid Solana address");
@@ -83,8 +72,8 @@ export function WalletLinkForm({
8372
}
8473

8574
const validateEthAddress = async () => {
86-
const isEVMValid = await isEvmAddressAsync(ethAddress);
87-
const isENSValid = ENS_NAME_REGEX.test(ethAddress);
75+
const isEVMValid = await validateAddress(ethAddress, "ethereum");
76+
const isENSValid = validateEnsFormat(ethAddress);
8877
setIsEthValid(isEVMValid || isENSValid);
8978
setEthAddressError(
9079
isEVMValid || isENSValid ? "" : "Invalid Ethereum address or ENS name.",
@@ -101,12 +90,16 @@ export function WalletLinkForm({
10190
return;
10291
}
10392

104-
const isSOLValid = SOL_ADDRESS_REGEX.test(solAddress);
105-
const isSNSValid = SNS_NAME_REGEX.test(solAddress);
106-
setIsSolValid(isSNSValid || isSOLValid);
107-
setSolAddressError(
108-
isSNSValid || isSOLValid ? "" : "Invalid Solana address or SNS name.",
109-
);
93+
const validateSolAddress = async () => {
94+
const isSOLValid = await validateAddress(solAddress, "solana");
95+
const isSNSValid = validateSnsFormat(solAddress);
96+
setIsSolValid(isSNSValid || isSOLValid);
97+
setSolAddressError(
98+
isSNSValid || isSOLValid ? "" : "Invalid Solana address or SNS name.",
99+
);
100+
};
101+
102+
validateSolAddress();
110103
}, [solAddress]);
111104

112105
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
@@ -119,14 +112,10 @@ export function WalletLinkForm({
119112
const updatedWallets: LinkedWallet[] = [];
120113

121114
if (ethAddress) {
122-
const isENSValid = ENS_NAME_REGEX.test(ethAddress);
123-
let address: string | null = ethAddress;
124-
125-
if (isENSValid) {
126-
const viemClient = await getViemClient();
127-
const normalizedName = await normalizeEns(ethAddress);
128-
address = await viemClient.getEnsAddress({ name: normalizedName });
129-
}
115+
const isENSValid = validateEnsFormat(ethAddress);
116+
const address = isENSValid
117+
? await resolveEnsDomain(ethAddress)
118+
: ethAddress;
130119

131120
// If the address is not found, set the error and return
132121
if (!address) {
@@ -142,9 +131,9 @@ export function WalletLinkForm({
142131
}
143132

144133
if (solAddress) {
145-
const isSNSValid = SNS_NAME_REGEX.test(solAddress);
134+
const isSNSValid = validateSnsFormat(solAddress);
146135
const address = isSNSValid
147-
? await resolveSolDomain(solAddress)
136+
? await resolveSnsDomain(solAddress)
148137
: solAddress;
149138

150139
// If the address is not found, set the error and return

src/app/profile/edit/hooks/useProfileWallets.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export function useProfileWallets() {
6060
setReadmeContent(decodedReadmeText);
6161

6262
// parse Readme content for Wallet data
63-
const walletData = parseWalletLinkingDataFromReadme(decodedReadmeText);
63+
const walletData =
64+
await parseWalletLinkingDataFromReadme(decodedReadmeText);
6465
setWalletData(walletData);
6566
} catch (err: unknown) {
6667
console.error("Error in fetchProfileData:", err);

src/lib/pipelines/ingest/fetchWalletAddresses.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ const ingestWalletAddresses = createStep(
111111
for (const wallet of freshWalletData.wallets) {
112112
if (
113113
!SUPPORTED_CHAINS_NAMES.includes(wallet.chain.toLowerCase()) ||
114-
!validateAddress(wallet.address, wallet.chain)
114+
!(await validateAddress(wallet.address, wallet.chain))
115115
) {
116116
logger?.warn(
117117
`Skipping invalid wallet for ${username}: ${wallet.address} on chain ${wallet.chain}`,

src/lib/walletLinking/chainUtils.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import EthereumIcon from "@/components/icons/EthereumIcon";
22
import SolanaIcon from "@/components/icons/SolanaIcon";
3-
import { isAddress } from "viem";
3+
4+
// Module-level caches for imported validators to optimize repeated calls
5+
let viemValidator: ((address: string) => boolean) | null = null;
6+
let solanaValidator: ((address: string) => boolean) | null = null;
47

58
interface ChainConfig {
69
chainId: string;
7-
validator: (address: string) => boolean;
10+
validator: (address: string) => Promise<boolean>;
811
icon: React.ElementType;
912
}
1013

@@ -21,13 +24,44 @@ interface ChainConfig {
2124
export const SUPPORTED_CHAINS: Record<string, ChainConfig> = {
2225
ethereum: {
2326
chainId: "eip155:1",
24-
validator: (address: string) => isAddress(address),
27+
validator: async (address: string) => {
28+
// Use cached validator if available, otherwise import and cache
29+
if (!viemValidator) {
30+
const { isAddress } = await import("viem");
31+
viemValidator = isAddress;
32+
}
33+
34+
try {
35+
return viemValidator(address);
36+
} catch (error) {
37+
console.error(`Failed to validate Ethereum address ${address}:`, error);
38+
return false;
39+
}
40+
},
2541
icon: EthereumIcon,
2642
},
2743
solana: {
2844
chainId: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
29-
validator: (address: string) =>
30-
/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address),
45+
validator: async (address: string) => {
46+
// Use cached validator if available, otherwise import and cache
47+
if (!solanaValidator) {
48+
const { PublicKey } = await import("@solana/web3.js");
49+
solanaValidator = (addr: string) => {
50+
try {
51+
return PublicKey.isOnCurve(new PublicKey(addr).toBytes());
52+
} catch {
53+
return false;
54+
}
55+
};
56+
}
57+
58+
try {
59+
return solanaValidator(address);
60+
} catch (error) {
61+
console.error(`Failed to validate Solana address ${address}:`, error);
62+
return false;
63+
}
64+
},
3165
icon: SolanaIcon,
3266
},
3367
};
@@ -79,7 +113,10 @@ export function createAccountId(chainId: string, address: string): string {
79113
* @param chain The blockchain name (e.g., "ethereum", "solana")
80114
* @returns True if the address is valid for the chain, false otherwise
81115
*/
82-
export function validateAddress(address: string, chain: string): boolean {
116+
export async function validateAddress(
117+
address: string,
118+
chain: string,
119+
): Promise<boolean> {
83120
const chainConfig =
84121
SUPPORTED_CHAINS[chain.toLowerCase() as keyof typeof SUPPORTED_CHAINS];
85122
if (!chainConfig) {

src/lib/walletLinking/sns.ts renamed to src/lib/walletLinking/domain.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Array of RPC endpoints to try in order
1+
// Array of solana RPC endpoints to try in order
22
const RPC_ENDPOINTS = [
33
"https://api.mainnet-beta.solana.com", // Public Solana RPC
44
"https://solana-mainnet.g.alchemy.com/v2/lqe31XHZcBd-8FsgmYnHJ", // Alchemy endpoint, domain restricted to https://elizaos.github.io
@@ -11,7 +11,7 @@ const RPC_ENDPOINTS = [
1111
* @returns Promise<string> The first working RPC endpoint URL
1212
* @throws Will throw an error if no endpoints are accessible
1313
*/
14-
async function getWorkingRpcEndpoint(
14+
async function getWorkingRpcEndpointForSolana(
1515
Connection: typeof import("@solana/web3.js").Connection,
1616
): Promise<string> {
1717
for (const endpoint of RPC_ENDPOINTS) {
@@ -35,7 +35,7 @@ async function getWorkingRpcEndpoint(
3535
* @returns A Promise that resolves to the PublicKey of the domain owner
3636
* @throws Will throw an error if the domain doesn't exist or if there's a network issue
3737
*/
38-
export async function resolveSolDomain(domain: string): Promise<string | null> {
38+
export async function resolveSnsDomain(domain: string): Promise<string | null> {
3939
try {
4040
// Import all Solana dependencies dynamically
4141
const [{ Connection }, { getDomainKeySync, NameRegistryState }] =
@@ -44,7 +44,7 @@ export async function resolveSolDomain(domain: string): Promise<string | null> {
4444
import("@bonfida/spl-name-service"),
4545
]);
4646

47-
const workingEndpoint = await getWorkingRpcEndpoint(Connection);
47+
const workingEndpoint = await getWorkingRpcEndpointForSolana(Connection);
4848
const connection = new Connection(workingEndpoint);
4949

5050
const { pubkey } = getDomainKeySync(domain);
@@ -57,3 +57,45 @@ export async function resolveSolDomain(domain: string): Promise<string | null> {
5757
return null;
5858
}
5959
}
60+
61+
/**
62+
* Resolves an Ethereum Name Service (ENS) domain to its owner's address.
63+
* @param name The ENS domain name to resolve (e.g., "alice.eth")
64+
* @returns A Promise that resolves to the Ethereum address of the domain owner
65+
* @throws Will throw an error if the domain doesn't exist or if there's a network issue
66+
*/
67+
export async function resolveEnsDomain(name: string): Promise<string | null> {
68+
try {
69+
const { createPublicClient, http } = await import("viem");
70+
const { normalize } = await import("viem/ens");
71+
const { mainnet } = await import("viem/chains");
72+
73+
const viemClient = createPublicClient({
74+
chain: mainnet,
75+
transport: http(),
76+
});
77+
78+
const normalizedName = await normalize(name);
79+
const address = await viemClient.getEnsAddress({ name: normalizedName });
80+
return address;
81+
} catch (error) {
82+
console.error(`Failed to resolve ENS domain ${name}:`, error);
83+
return null;
84+
}
85+
}
86+
87+
// ENS name regex (name.eth format)
88+
// Matches names that end with .eth and contain valid characters
89+
const ENS_NAME_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.eth$/;
90+
91+
export function validateEnsFormat(name: string): boolean {
92+
return ENS_NAME_REGEX.test(name);
93+
}
94+
95+
// SNS name regex (name.sol format)
96+
// Matches names that end with .sol and contain valid characters
97+
const SNS_NAME_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.sol$/;
98+
99+
export function validateSnsFormat(name: string): boolean {
100+
return SNS_NAME_REGEX.test(name);
101+
}

src/lib/walletLinking/fetchWalletDataFromGithub.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ export async function batchFetchWalletDataFromGithub(
3737
const result = fileContents[i];
3838

3939
if (result.content) {
40-
const walletData = parseWalletLinkingDataFromReadme(result.content);
40+
const walletData = await parseWalletLinkingDataFromReadme(
41+
result.content,
42+
);
4143
results[username] = {
4244
walletData,
4345
profileRepoExists: true,

src/lib/walletLinking/readmeUtils.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ const WALLET_SECTION_END_MARKER = "WALLET-LINKING-END -->";
2929
* @param readmeContent The string content of the README file.
3030
* @returns The parsed and validated wallet linking data, or null if no valid data found.
3131
*/
32-
export function parseWalletLinkingDataFromReadme(
32+
export async function parseWalletLinkingDataFromReadme(
3333
readmeContent: string,
34-
): WalletLinkingData | null {
34+
): Promise<WalletLinkingData | null> {
3535
const startIndex = readmeContent.indexOf(WALLET_SECTION_BEGIN_MARKER);
3636
const endIndex = readmeContent.indexOf(WALLET_SECTION_END_MARKER);
3737

@@ -56,13 +56,23 @@ export function parseWalletLinkingDataFromReadme(
5656
}
5757

5858
// Make sure to only return wallets for supported chains
59+
const validWallets = [];
60+
for (const wallet of result.data.wallets) {
61+
const isChainSupported = SUPPORTED_CHAINS_NAMES.includes(
62+
wallet.chain.toLowerCase(),
63+
);
64+
const isAddressValid = await validateAddress(
65+
wallet.address,
66+
wallet.chain,
67+
);
68+
if (isAddressValid && isChainSupported) {
69+
validWallets.push(wallet);
70+
}
71+
}
72+
5973
const walletLinkingData: WalletLinkingData = {
6074
lastUpdated: result.data.lastUpdated,
61-
wallets: result.data.wallets.filter(
62-
(wallet) =>
63-
SUPPORTED_CHAINS_NAMES.includes(wallet.chain.toLowerCase()) &&
64-
validateAddress(wallet.address, wallet.chain),
65-
),
75+
wallets: validWallets,
6676
};
6777

6878
return walletLinkingData;

src/lib/walletLinking/viem.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)