Skip to content

Resolver middleware

Fetch ens information about the users, including Converse profile data.

Is already includes in MessageKit but you could import this one in your files for more customization

src/helpers/resolver.ts
import { isAddress } from "viem";
import { V2Client, V3Client } from "../index";
import { XMTPContext } from "../lib/xmtp";
 
export const converseEndpointURL = "https://converse.xyz/profile/";
 
export type InfoCache = Map<string, UserInfo>;
export type ConverseProfile = {
  address: string | null;
  onXmtp: boolean;
  avatar: string | null;
  formattedName: string | null;
  name: string | null;
};
export type UserInfo = {
  ensDomain?: string | undefined;
  address?: string | undefined;
  preferredName: string | undefined;
  converseUsername?: string | undefined;
  ensInfo?: EnsData | undefined;
  avatar?: string | undefined;
  converseEndpoint?: string | undefined;
};
 
export interface EnsData {
  address?: string;
  avatar?: string;
  avatar_small?: string;
  converse?: string;
  avatar_url?: string;
  contentHash?: string;
  description?: string;
  ens?: string;
  ens_primary?: string;
  github?: string;
  resolverAddress?: string;
  twitter?: string;
  url?: string;
  wallets?: {
    eth?: string;
  };
}
 
let infoCache: InfoCache = new Map();
 
export const clearInfoCache = (address?: string) => {
  if (address) {
    infoCache.delete(address);
  } else {
    infoCache.clear();
  }
};
export const getUserInfo = async (
  key: string,
  clientAddress?: string,
  context?: XMTPContext,
): Promise<UserInfo | null> => {
  let data: UserInfo = {
    ensDomain: undefined,
    address: undefined,
    converseUsername: undefined,
    ensInfo: undefined,
    avatar: undefined,
    converseEndpoint: undefined,
    preferredName: undefined,
  };
  if (typeof key !== "string") {
    console.error("userinfo key must be a string");
    return data;
  }
  if (infoCache.get(key)) return infoCache.get(key) as UserInfo;
  key = key?.toLowerCase();
  clientAddress = clientAddress?.toLowerCase();
  // Determine user information based on provided key
  if (isAddress(clientAddress || "")) {
    data.address = clientAddress;
  } else if (isAddress(key || "")) {
    data.address = key;
  } else if (key.includes(".eth")) {
    data.ensDomain = key;
  } else if (["@user", "@me", "@bot"].includes(key)) {
    data.address = clientAddress;
    data.ensDomain = key.replace("@", "") + ".eth";
    data.converseUsername = key.replace("@", "");
  } else if (key === "@alix") {
    data.address = "0x3a044b218BaE80E5b9E16609443A192129A67BeA";
    data.converseUsername = "alix";
  } else if (key === "@bo") {
    data.address = "0xbc3246461ab5e1682baE48fa95172CDf0689201a";
    data.converseUsername = "bo";
  } else {
    data.converseUsername = key;
  }
 
  data.preferredName = data.ensDomain || data.converseUsername || "Friend";
  const keyToUse = data.address || data.ensDomain || data.converseUsername;
 
  if (!keyToUse) {
    console.log("Unable to determine a valid key for fetching user info.");
    return data;
  } else {
    // Check cache for existing data
    const cacheData = infoCache.get(keyToUse);
    if (cacheData) {
      return cacheData;
    }
 
    // Notify user about the fetching process
    if (context) {
      await context.send(
        "Hey there! Give me a sec while I fetch info about you first...",
      );
    }
    // Fetch data based on ENS domain or Converse username
    if (keyToUse.includes(".eth")) {
      // Fetch ENS data
      try {
        const response = await fetch(`https://ensdata.net/${keyToUse}`);
        console.log(response);
        if (!response.ok) {
          console.error(
            `ENS data request failed with status or unable to resolve ${keyToUse}`,
          );
        } else {
          const ensData = (await response.json()) as EnsData;
          if (ensData) {
            data.ensInfo = ensData;
            data.ensDomain = ensData.ens || data.ensDomain;
            data.address = ensData.address || data.address;
            data.avatar = ensData.avatar_url || data.avatar;
          }
        }
      } catch (error) {
        //console.error(`Failed to fetch ENS data for ${keyToUse}`);
      }
    } else {
      // Fetch Converse profile data
      try {
        const username = keyToUse.replace("@", "");
        const converseEndpoint = `${converseEndpointURL}${username}`;
        const response = await fetchWithTimeout(
          converseEndpoint,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Accept: "application/json",
            },
            body: JSON.stringify({ peer: username }),
          },
          5000,
        );
        if (!response?.ok) {
          console.error(
            `Converse profile request failed with status ${response?.status}`,
          );
        }
        const converseData = (await response?.json()) as ConverseProfile;
        if (converseData) {
          data.converseUsername =
            converseData.formattedName ||
            converseData.name ||
            data.converseUsername;
          data.address = converseData.address || data.address;
          data.avatar = converseData.avatar || data.avatar;
          data.converseEndpoint = converseEndpoint;
        }
      } catch (error) {
        console.error("Failed to fetch Converse profile:", error);
      }
    }
 
    data.preferredName = data.ensDomain || data.converseUsername || "Friend";
    infoCache.set(keyToUse, data);
    return data;
  }
};
 
const fetchWithTimeout = async (
  url: string,
  options: RequestInit,
  timeout = 5000,
) => {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
    });
    clearTimeout(id);
    return response;
  } catch (error) {
    clearTimeout(id);
    console.error("fetching");
  }
};
export const isOnXMTP = async (
  v3client: V3Client,
  v2client: V2Client,
  address: string,
) => {
  try {
    const [v2, v3] = await Promise.all([
      v2client.canMessage(address || ""),
      v3client.canMessage([address || ""]),
    ]);
    return { v2, v3: v3.get(address || "") };
  } catch (error) {
    console.error("Error checking XMTP availability:", error);
  }
};
 
export const replaceUserContext = (userInfo: UserInfo) => {
  let { address, ensDomain, converseUsername, preferredName } = userInfo;
  let prompt = `## User context
- Start by fetch their domain from or Converse username
- Call the user by their name or domain, in case they have one
- Ask for a name (if they don't have one) so you can suggest domains.
- Users address is: ${address}`;
  if (preferredName) prompt += `\n- Users name is: ${preferredName}`;
  if (ensDomain) prompt += `\n- User ENS domain is: ${ensDomain}`;
  if (converseUsername)
    prompt += `\n- Converse username is: ${converseUsername}`;
  return prompt;
};