Skip to content

Feature request: Resolve web3 cryptocurrency wallet address from social identity string #23

@technoplato

Description

@technoplato

Is your feature request related to a problem?

No response

Describe the solution to the problem

User Story: as a user, I want to search for someone by their web3 name and add them to a group chat.

Technical details: I need to give the backend an arbitrary query string and have it return in search results the metadata for that identity.

There can be various states here as seen below. If they aren't clear, please feel free to ping me.

mirror the code we previously had on the frontend:

      const searchForValue = async () => {
        setStatus(({ loading }) => ({
          loading,
          error: "",
          inviteToConverse: "",
          profileSearchResults: {},
        }));

        if (isSupportedPeer(value)) {
          setStatus(({ error }) => ({
            loading: true,
            error,
            inviteToConverse: "",
            profileSearchResults: {},
          }));
          searchingForValue.current = value;
          const resolvedAddress = await getAddressForPeer(value);
          if (searchingForValue.current === value) {
            // If we're still searching for this one
            if (!resolvedAddress) {
              const isLens = value.endsWith(config.lensSuffix);
              const isFarcaster = value.endsWith(".fc");
              setStatus({
                loading: false,
                profileSearchResults: {},
                inviteToConverse: "",
                error:
                  isLens || isFarcaster
                    ? "This handle does not exist. Please try again."
                    : "No address has been set for this domain.",
              });

              return;
            }
            const address = getCleanAddress(resolvedAddress);
            const addressIsOnXmtp = await accountCanMessagePeer({
              account: getSafeCurrentSender().ethereumAddress,
              peer: address,
            });
            if (searchingForValue.current === value) {
              if (addressIsOnXmtp) {
                // Let's search with the exact address!
                const profiles = await searchProfiles({
                  searchQuery: address,
                });

                if (!isEmptyObject(profiles)) {
                  // Let's save the profiles for future use
                  setStatus({
                    loading: false,
                    error: "",
                    inviteToConverse: "",
                    profileSearchResults: profiles.reduce((acc, profile) => {
                      acc[profile.xmtpId] = profile;
                      return acc;
                    }, {} as { [address: string]: ISearchProfilesResult }),
                  });
                } else {
                  setStatus({
                    loading: false,
                    error: "",
                    inviteToConverse: "",
                    profileSearchResults: {},
                  });
                }
              } else {
                setStatus({
                  loading: false,
                  error: `${value} does not use Converse or XMTP yet`,
                  inviteToConverse: value,
                  profileSearchResults: {},
                });
              }
            }
          }
        } else {
          setStatus({
            loading: true,
            error: "",
            inviteToConverse: "",
            profileSearchResults: {},
          });

          const profiles = await searchProfiles({
            searchQuery: value,
          });

          if (!isEmptyObject(profiles)) {
            // Let's save the profiles for future use
            setStatus({
              loading: false,
              error: "",
              inviteToConverse: "",
              profileSearchResults: profiles.reduce((acc, profile) => {
                acc[profile.xmtpId] = profile;
                return acc;
              }, {} as { [address: string]: ISearchProfilesResult }),
            });
          } else {
            setStatus({
              loading: false,
              error: "",
              inviteToConverse: "",
              profileSearchResults: {},
            });
          }
        }
      };
import { captureError } from "@/utils/capture-error";
import { isUNSAddress } from "@utils/uns";
import axios from "axios";
import { isAddress } from "ethers/lib/utils";
import { config } from "../../config";
import { ethers } from "ethers";
import logger from "@/utils/logger";

export const isSupportedPeer = (peer: string) => {
  // new backend is going to do all of this for us
  return false;
  const is0x = isAddress(peer.toLowerCase());
  const isUserName = peer.endsWith(config.usernameSuffix);
  const isENS = peer.endsWith(".eth");
  const isLens = peer.endsWith(config.lensSuffix);
  const isFarcaster = peer.endsWith(".fc");
  const isCbId = peer.endsWith(".cb.id");
  const isUNS = isUNSAddress(peer);
  return (
    isUserName ||
    is0x ||
    isLens ||
    isENS ||
    isENS ||
    isFarcaster ||
    isCbId ||
    isUNS
  );
};

export const getAddressForPeer = async (peer: string) => {
  // new backend is going to do all of this for us
  return undefined;
  if (!isSupportedPeer(peer)) {
    throw new Error(`Peer ${peer} is invalid`);
  }
  const isLens = peer.endsWith(config.lensSuffix);
  const isUserName = peer.endsWith(config.usernameSuffix);
  const isENS = peer.endsWith(".eth");
  const isFarcaster = peer.endsWith(".fc");
  const isCbId = peer.endsWith(".cb.id");
  const isUNS = isUNSAddress(peer);

  const isENSCompatible = isUserName || isCbId || isENS;

  const resolvedAddress = isENSCompatible
    ? await resolveEnsName(peer)
    : isUNS
    ? await resolveUnsDomain(peer)
    : isFarcaster
    ? await resolveFarcasterUsername(peer.slice(0, peer.length - 3))
    : isLens
    ? await getLensOwner(peer)
    : peer;
  return resolvedAddress || undefined;
};

export function getCleanEthAddress(address: string): string {
  return address.toLowerCase();
}

async function getLensOwner(handle: string) {
  try {
    const { data } = await axios.post(`https://${config.lensApiDomain}/`, {
      operationName: "Profile",
      query:
        "query Profile($handle: Handle) {\n  profile(request: {handle: $handle}) {\n    ownedBy\n  }\n}\n",
      variables: {
        handle,
      },
    });
    return data.data?.profile?.ownedBy || null;
  } catch (e) {
    captureError(e);
  }
  return null;
}

/**
 * Resolves a Coinbase ID to an Ethereum address using ENS resolution
 * @param cbId The Coinbase ID to resolve (e.g. "username.cb.id")
 * @returns The resolved Ethereum address or null if resolution fails
 */
export async function resolveCoinbaseId(cbId: string): Promise<string | null> {
  try {
    if (!cbId.endsWith(".cb.id")) {
      throw new Error("Invalid Coinbase ID format. Must end with .cb.id");
    }

    const provider = new ethers.providers.StaticJsonRpcProvider({
      url: config.evm.rpcEndpoint,
      skipFetchSetup: true,
    });

    const address = await provider.resolveName(cbId);
    return address;
  } catch (error) {
    logger.error(`Failed to resolve Coinbase ID ${cbId}:`, error);
    return null;
  }
}

Describe the uses cases for the feature

No response

Additional details

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions