diff --git a/src/app/(spaces)/PublicSpace.tsx b/src/app/(spaces)/PublicSpace.tsx index 08e52829a..380ec9076 100644 --- a/src/app/(spaces)/PublicSpace.tsx +++ b/src/app/(spaces)/PublicSpace.tsx @@ -1,20 +1,21 @@ "use client"; import React from "react"; -import { useAuthenticatorManager } from "@/authenticators/AuthenticatorManager"; import { useSidebarContext } from "@/common/components/organisms/Sidebar"; import TabBar from "@/common/components/organisms/TabBar"; import { useAppStore } from "@/common/data/stores/app"; +import { useCurrentFid } from "@/common/lib/hooks/useCurrentFid"; import { EtherScanChainName } from "@/constants/etherscanChainIds"; import { INITIAL_SPACE_CONFIG_EMPTY } from "@/config"; import Profile from "@/fidgets/ui/profile"; import Channel from "@/fidgets/ui/channel"; import { useWallets } from "@privy-io/react-auth"; -import { indexOf, isNil, mapValues, noop} from "lodash"; +import { isNil, mapValues, noop} from "lodash"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Address } from "viem"; import { SpaceConfigSaveDetails } from "./Space"; +import { toast } from "sonner"; import SpacePage from "./SpacePage"; import { SpacePageData, @@ -23,8 +24,6 @@ import { isProposalSpace, isChannelSpace, } from "@/common/types/spaceData"; -const FARCASTER_NOUNSPACE_AUTHENTICATOR_NAME = "farcaster:nounspace"; - interface PublicSpaceProps { spacePageData: SpacePageData; tabName: string; @@ -107,40 +106,9 @@ export default function PublicSpace({ const currentConfig = getConfig(); // Identity states - const [currentUserFid, setCurrentUserFid] = useState(null); - const [isSignedIntoFarcaster, setIsSignedIntoFarcaster] = useState(false); + const currentUserFid = useCurrentFid(); const { wallets } = useWallets(); - const { - lastUpdatedAt: authManagerLastUpdatedAt, - getInitializedAuthenticators: authManagerGetInitializedAuthenticators, - callMethod: authManagerCallMethod, - } = useAuthenticatorManager(); - - // Checks if the user is signed into Farcaster - useEffect(() => { - authManagerGetInitializedAuthenticators().then((authNames) => { - setIsSignedIntoFarcaster( - indexOf(authNames, FARCASTER_NOUNSPACE_AUTHENTICATOR_NAME) > -1, - ); - }); - }, [authManagerLastUpdatedAt]); - - // Loads the current user's FID if they're signed into Farcaster - useEffect(() => { - if (!isSignedIntoFarcaster) return; - authManagerCallMethod({ - requestingFidgetId: "root", - authenticatorId: FARCASTER_NOUNSPACE_AUTHENTICATOR_NAME, - methodName: "getAccountFid", - isLookup: true, - }).then((authManagerResp) => { - if (authManagerResp.result === "success") { - setCurrentUserFid(authManagerResp.value as number); - } - }); - }, [isSignedIntoFarcaster, authManagerLastUpdatedAt]); - // Load editable spaces when user signs in useEffect(() => { if (!currentUserFid) return; @@ -174,7 +142,7 @@ export default function PublicSpace({ ); return result; - }, [spacePageData, currentUserFid, wallets, isSignedIntoFarcaster]); + }, [spacePageData, currentUserFid, wallets]); // Config logic: // - If we have currentTabName and the tab is loaded in store, use it @@ -347,7 +315,8 @@ export default function PublicSpace({ const saveConfig = useCallback( async (spaceConfig: SpaceConfigSaveDetails) => { if (isNil(currentSpaceId) || isNil(currentTabName)) { - throw new Error("Cannot save config until space and tab are initialized"); + toast.error("Space is still initializing. Try again in a moment."); + return; } const saveableConfig = { ...spaceConfig, diff --git a/src/app/(spaces)/s/[handle]/ProfileSpace.tsx b/src/app/(spaces)/s/[handle]/ProfileSpace.tsx index 2d616b5f2..637a5d57e 100644 --- a/src/app/(spaces)/s/[handle]/ProfileSpace.tsx +++ b/src/app/(spaces)/s/[handle]/ProfileSpace.tsx @@ -24,10 +24,10 @@ * Integrates with: PublicSpace */ -import React, { useMemo } from "react"; import PublicSpace from "@/app/(spaces)/PublicSpace"; -import { ProfileSpacePageData } from "@/common/types/spaceData"; import { useCurrentSpaceIdentityPublicKey } from "@/common/lib/hooks/useCurrentSpaceIdentityPublicKey"; +import { ProfileSpacePageData } from "@/common/types/spaceData"; +import React ,{ useMemo } from "react"; export interface ProfileSpaceProps { spacePageData: Omit; @@ -43,19 +43,19 @@ const isProfileSpaceEditable = ( currentUserIdentityPublicKey?: string ): boolean => { // Require user to be logged in (have an identity key) - if (!currentUserIdentityPublicKey) { - console.log('[ProfileSpace] User not logged in - not editable'); - return false; - } + // if (!currentUserIdentityPublicKey) { + // console.log('[ProfileSpace] User not logged in - not editable'); + // return false; + // } // Check FID ownership (original logic) - const hasFidOwnership = - currentUserFid !== undefined && - spaceOwnerFid !== undefined && + const hasFidOwnership = + currentUserFid !== undefined && + spaceOwnerFid !== undefined && currentUserFid === spaceOwnerFid; // Check identity key ownership (only if space is registered) - const hasIdentityOwnership = !!(spaceId && spaceIdentityPublicKey && + const hasIdentityOwnership = !!(spaceId && spaceIdentityPublicKey && spaceIdentityPublicKey === currentUserIdentityPublicKey); console.log('[ProfileSpace] Editability check details:', { @@ -82,9 +82,9 @@ export default function ProfileSpace({ const spaceDataWithClientSideLogic = useMemo(() => ({ ...spaceData, spacePageUrl: (tabName: string) => `/s/${spaceData.spaceName}/${encodeURIComponent(tabName)}`, - isEditable: (currentUserFid: number | undefined) => + isEditable: (currentUserFid: number | undefined) => isProfileSpaceEditable( - spaceData.spaceOwnerFid, + spaceData.spaceOwnerFid, currentUserFid, spaceData.spaceId, spaceData.identityPublicKey, diff --git a/src/authenticators/AuthenticatorManager.tsx b/src/authenticators/AuthenticatorManager.tsx index 65382461e..2904f52cc 100644 --- a/src/authenticators/AuthenticatorManager.tsx +++ b/src/authenticators/AuthenticatorManager.tsx @@ -27,6 +27,7 @@ import { AuthenticatorMethods, } from "."; import authenticators from "./authenticators"; +import { SetupStep } from "@/common/data/stores/app/setup"; type AuthenticatorPermissions = { [fidgetId: string]: string[]; @@ -155,9 +156,10 @@ export const AuthenticatorManagerProvider: React.FC< }>(); const [initializationQueue, setInitializationQueue] = useState([]); - const { modalOpen, setModalOpen } = useAppStore((state) => ({ + const { modalOpen, setModalOpen, currentStep } = useAppStore((state) => ({ modalOpen: state.setup.modalOpen, - setModalOpen: state.setup.setModalOpen + setModalOpen: state.setup.setModalOpen, + currentStep: state.setup.currentStep, })); const authenticatorManager = useMemo( @@ -174,7 +176,9 @@ export const AuthenticatorManagerProvider: React.FC< // TO DO: When adding permissioning // Allow client requests to not open modal // While Fidget requests will - if (!modalOpen && !isLookup) { + // Only auto-open once the user is fully in-app; during login/setup this causes + // a confusing "blank" modal that closes later. + if (!modalOpen && !isLookup && currentStep === SetupStep.DONE) { setModalOpen(true); } return { @@ -242,20 +246,21 @@ export const AuthenticatorManagerProvider: React.FC< }); }, initializeAuthenticators: (authenticatorIds) => { - setInitializationQueue(concat(initializationQueue, authenticatorIds)); + setInitializationQueue((queue) => concat(queue, authenticatorIds)); }, - CurrentInitializerComponent: () => - currentInitializer && ( - - ), + CurrentInitializerComponent: currentInitializer + ? () => ( + + ) + : undefined, lastUpdatedAt: moment().toISOString(), }), [ @@ -274,7 +279,7 @@ export const AuthenticatorManagerProvider: React.FC< isUndefined(authenticator) || (await authenticator.methods.isReady()) ) { - setInitializationQueue(tail(initializationQueue)); + setInitializationQueue((queue) => tail(queue)); return; } else { setCurrentInitializer({ diff --git a/src/authenticators/farcaster/signers/NounspaceManagedSignerAuthenticator.tsx b/src/authenticators/farcaster/signers/NounspaceManagedSignerAuthenticator.tsx index 5aa2c2216..09c7ec68b 100644 --- a/src/authenticators/farcaster/signers/NounspaceManagedSignerAuthenticator.tsx +++ b/src/authenticators/farcaster/signers/NounspaceManagedSignerAuthenticator.tsx @@ -226,20 +226,17 @@ const initializer: AuthenticatorInitializer< }); function devSignin() { - // In development, generate test signing keys so Quick Auth can work - // These are random keys - not linked to a real Farcaster account const newPrivKey = ed25519.utils.randomPrivateKey(); const publicKeyHex = `0x${bytesToHex(ed25519.getPublicKey(newPrivKey))}`; - const privateKeyHex = `0x${bytesToHex(newPrivKey)}`; - saveData({ ...data, status: "completed", accountFid: Number(devFid), - accountType: "signer", // Use signer type so keys are available - publicKeyHex: publicKeyHex, - privateKeyHex: privateKeyHex, + accountType: "account", + publicKeyHex, + privateKeyHex: `0x${bytesToHex(newPrivKey)}`, }); + done(); } const warpcastSignerUrl = data.signerUrl diff --git a/src/common/components/templates/LoginModal.tsx b/src/common/components/templates/LoginModal.tsx index 873ecc977..613e1f48e 100644 --- a/src/common/components/templates/LoginModal.tsx +++ b/src/common/components/templates/LoginModal.tsx @@ -76,12 +76,11 @@ const LoginModal = ({ ) : null; } - if (currentStep === SetupStep.REQUIRED_AUTHENTICATORS_INSTALLED) - return CurrentInitializerComponent ? ( - - ) : ( - "One second..." - ); + // If an authenticator is requesting initialization (e.g. Farcaster signer), + // show its initializer regardless of the current setup step. + if (CurrentInitializerComponent) { + return ; + } return ; } @@ -89,7 +88,7 @@ const LoginModal = ({ return ( {getModalContent()} diff --git a/src/common/data/stores/app/accounts/farcasterStore.ts b/src/common/data/stores/app/accounts/farcasterStore.ts index 43a5f129a..97fcf3453 100644 --- a/src/common/data/stores/app/accounts/farcasterStore.ts +++ b/src/common/data/stores/app/accounts/farcasterStore.ts @@ -7,21 +7,25 @@ import { AppStore } from ".."; import { StoreGet, StoreSet } from "../../createStore"; import axiosBackend from "../../../api/backend"; import { concat, isUndefined } from "lodash"; -import { hashObject } from "@/common/lib/signedFiles"; +import { signSignable , hashObject} from "@/common/lib/signedFiles"; import moment from "moment"; import { bytesToHex } from "@noble/ciphers/utils"; + import { AnalyticsEvent } from "@/common/constants/analyticsEvents"; import { analytics } from "@/common/providers/AnalyticsProvider"; +import { InferFidLinkRequest, InferFidLinkResponse } from "@/pages/api/fid-link/infer"; + type FarcasterActions = { getFidsForCurrentIdentity: () => Promise; + inferFidForCurrentIdentity: (walletAddress: string) => Promise; registerFidForCurrentIdentity: ( fid: number, signingKey: string, // Takes in signMessage as it is a method // of the Authenticator and client doesn't // have direct access to the keys - signMessage: (messageHash: Uint8Array) => Promise, + signMessage: (messageHash: Uint8Array) => Promise ) => Promise; setFidsForCurrentIdentity: (fids: number[]) => void; addFidToCurrentIdentity: (fid: number) => void; @@ -29,35 +33,55 @@ type FarcasterActions = { export type FarcasterStore = FarcasterActions; -export const farcasterStore = ( - set: StoreSet, - get: StoreGet, -): FarcasterStore => ({ +export const farcasterStore = (set: StoreSet, get: StoreGet): FarcasterStore => ({ addFidToCurrentIdentity: (fid) => { - const currentFids = - get().account.getCurrentIdentity()?.associatedFids || []; + const currentFids = get().account.getCurrentIdentity()?.associatedFids || []; get().account.setFidsForCurrentIdentity(concat(currentFids, [fid])); }, setFidsForCurrentIdentity: (fids) => { set((draft) => { - draft.account.spaceIdentities[ - draft.account.getCurrentIdentityIndex() - ].associatedFids = fids; + draft.account.spaceIdentities[draft.account.getCurrentIdentityIndex()].associatedFids = fids; }, "setFidsForCurrentIdentity"); }, getFidsForCurrentIdentity: async () => { - const { data } = await axiosBackend.get( - "/api/fid-link", - { - params: { - identityPublicKey: get().account.currentSpaceIdentityPublicKey, - }, + const { data } = await axiosBackend.get("/api/fid-link", { + params: { + identityPublicKey: get().account.currentSpaceIdentityPublicKey, }, - ); + }); if (!isUndefined(data.value)) { get().account.setFidsForCurrentIdentity(data.value!.fids); } }, + inferFidForCurrentIdentity: async (walletAddress) => { + try { + const identity = get().account.getCurrentIdentity(); + const identityPublicKey = identity?.rootKeys?.publicKey; + const identityPrivateKey = identity?.rootKeys?.privateKey; + if (!identityPublicKey || !identityPrivateKey) return null; + + const unsigned: Omit = { + identityPublicKey, + walletAddress: walletAddress.toLowerCase(), + timestamp: moment().toISOString(), + }; + + // Use signSignable helper for consistency with the rest of the codebase + const signed = signSignable(unsigned, identityPrivateKey) as InferFidLinkRequest; + + const { data } = await axiosBackend.post("/api/fid-link/infer", signed); + if (!data.value) return null; + get().account.addFidToCurrentIdentity(data.value.fid); + analytics.track(AnalyticsEvent.LINK_FID, { + fid: data.value.fid, + inferred: true, + }); + return data.value.fid; + } catch (e) { + console.error("[inferFidForCurrentIdentity] failed", e); + return null; + } + }, registerFidForCurrentIdentity: async (fid, signingKey, signMessage) => { const request: Omit = { fid, @@ -69,10 +93,7 @@ export const farcasterStore = ( ...request, signature: bytesToHex(await signMessage(hashObject(request))), }; - const { data } = await axiosBackend.post( - "/api/fid-link", - signedRequest, - ); + const { data } = await axiosBackend.post("/api/fid-link", signedRequest); if (!isUndefined(data.value)) { get().account.addFidToCurrentIdentity(data.value!.fid); analytics.track(AnalyticsEvent.LINK_FID, { fid }); diff --git a/src/common/data/stores/app/space/spaceStore.ts b/src/common/data/stores/app/space/spaceStore.ts index 3c536ff47..814aea7b1 100644 --- a/src/common/data/stores/app/space/spaceStore.ts +++ b/src/common/data/stores/app/space/spaceStore.ts @@ -1027,6 +1027,7 @@ export const createSpaceStoreFunc = ( const { data } = await axiosBackend.post( "/api/space/registry", registration, + { timeout: 30000 }, ); const newSpaceId = data.value!.spaceId; @@ -1532,4 +1533,4 @@ export const partializedSpaceStore = (state: AppStore): SpaceState => ({ localSpaces: state.space.localSpaces, loadingTabs: state.space.loadingTabs, checkedTabs: state.space.checkedTabs, -}); \ No newline at end of file +}); diff --git a/src/common/lib/authenticators/waitForAuthenticatorReady.ts b/src/common/lib/authenticators/waitForAuthenticatorReady.ts new file mode 100644 index 000000000..9c9c92578 --- /dev/null +++ b/src/common/lib/authenticators/waitForAuthenticatorReady.ts @@ -0,0 +1,37 @@ +import type { AuthenticatorManager } from "@/authenticators/AuthenticatorManager"; + +/** + * Polls for an authenticator to be initialized. + * + * Repeatedly checks if the specified authenticator is in the list of initialized + * authenticators, waiting up to `timeoutMs` before giving up. + * + * Useful for deferred initialization flows where the authenticator may take time + * to become ready after being installed and initialized. + * + * @param authenticatorManager - The authenticator manager instance to query + * @param authenticatorId - The ID of the authenticator (e.g., "farcaster:nounspace") + * @param timeoutMs - Maximum time to wait in milliseconds (default: 60_000) + * @returns true if the authenticator was initialized within the timeout, false if timeout expired + * + * @example + * const ready = await waitForAuthenticatorReady(authenticatorManager, "farcaster:nounspace"); + * if (ready) { + * // authenticator is ready to use + * } else { + * // timeout expired; authenticator never became ready + * } + */ +export async function waitForAuthenticatorReady( + authenticatorManager: AuthenticatorManager, + authenticatorId: string, + timeoutMs = 60_000, +): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const initialized = await authenticatorManager.getInitializedAuthenticators(); + if (initialized.includes(authenticatorId)) return true; + await new Promise((r) => setTimeout(r, 500)); + } + return false; +} diff --git a/src/common/lib/hooks/useCurrentFid.ts b/src/common/lib/hooks/useCurrentFid.ts index daa0ef11b..5ab5b0920 100644 --- a/src/common/lib/hooks/useCurrentFid.ts +++ b/src/common/lib/hooks/useCurrentFid.ts @@ -1,9 +1,21 @@ import { useAppStore } from "@/common/data/stores/app"; +/** + * Get the current FID for the authenticated user. + * + * Prioritizes inferred FIDs (from associatedFids array) over authenticator FID. + * If a user has multiple FIDs, this always returns the first one in associatedFids. + * + * NOTE: If multiple FID selection is needed in the future, callers should implement + * that logic separately. This hook intentionally uses a single "current" FID. + */ export const useCurrentFid = (): number | null => { return useAppStore((state) => { - const fid = state.account.authenticatorConfig["farcaster:nounspace"]?.data - ?.accountFid as number | null | undefined; + const currentIdentity = state.account.getCurrentIdentity?.(); + const inferredFid = currentIdentity?.associatedFids?.[0]; + if (typeof inferredFid === "number" && inferredFid > 0) return inferredFid; + + const fid = state.account.authenticatorConfig["farcaster:nounspace"]?.data?.accountFid as number | null | undefined; return !fid || fid === 1 ? null : fid; }); }; diff --git a/src/common/providers/LoggedInStateProvider.tsx b/src/common/providers/LoggedInStateProvider.tsx index abca83355..55a093947 100644 --- a/src/common/providers/LoggedInStateProvider.tsx +++ b/src/common/providers/LoggedInStateProvider.tsx @@ -4,6 +4,7 @@ import { useAppStore, useLogout } from "@/common/data/stores/app"; import { useSignMessage } from "@/common/data/stores/app/accounts/privyStore"; import { SetupStep } from "@/common/data/stores/app/setup"; import useValueHistory from "@/common/lib/hooks/useValueHistory"; +import { waitForAuthenticatorReady } from "@/common/lib/authenticators/waitForAuthenticatorReady"; import requiredAuthenticators from "@/constants/requiredAuthenticators"; import { bytesToHex } from "@noble/ciphers/utils"; @@ -31,6 +32,7 @@ const LoggedInStateProvider: React.FC = ({ children }) => { loadPreKeys, loadFidsForCurrentIdentity, registerFidForCurrentIdentity, + inferFidForCurrentIdentity, modalOpen, setModalOpen, keepModalOpen, @@ -55,6 +57,7 @@ const LoggedInStateProvider: React.FC = ({ children }) => { // Register FIDs for account loadFidsForCurrentIdentity: state.account.getFidsForCurrentIdentity, registerFidForCurrentIdentity: state.account.registerFidForCurrentIdentity, + inferFidForCurrentIdentity: state.account.inferFidForCurrentIdentity, // Logout modalOpen: state.setup.modalOpen, setModalOpen: state.setup.setModalOpen, @@ -105,17 +108,10 @@ const LoggedInStateProvider: React.FC = ({ children }) => { const identities = getIdentitiesForWallet(wallet); try { if (identities.length > 0) { - await decryptIdentityKeys( - signMessage, - wallet, - identities[0].identityPublicKey, - ); + await decryptIdentityKeys(signMessage, wallet, identities[0].identityPublicKey); setCurrentIdentity(identities[0].identityPublicKey); } else { - const publicKey = await createIdentityForWallet( - signMessage, - wallet, - ); + const publicKey = await createIdentityForWallet(signMessage, wallet); setCurrentIdentity(publicKey); } } catch (e) { @@ -146,49 +142,103 @@ const LoggedInStateProvider: React.FC = ({ children }) => { const installRequiredAuthenticators = async () => { await authenticatorManager.installAuthenticators(requiredAuthenticators); authenticatorManager.initializeAuthenticators(requiredAuthenticators); + // If no authenticators are required for signup, skip this step entirely. + if (requiredAuthenticators.length === 0) { + setCurrentStep(SetupStep.AUTHENTICATORS_INITIALIZED); + return; + } setCurrentStep(SetupStep.REQUIRED_AUTHENTICATORS_INSTALLED); }; const registerAccounts = async () => { - let currentIdentity = getCurrentIdentity()!; - if (currentIdentity.associatedFids.length > 0) { - setCurrentStep(SetupStep.ACCOUNTS_REGISTERED); - } else { + try { + let currentIdentity = getCurrentIdentity()!; + if (currentIdentity.associatedFids.length > 0) { + setCurrentStep(SetupStep.ACCOUNTS_REGISTERED); + return; + } + await loadFidsForCurrentIdentity(); currentIdentity = getCurrentIdentity()!; - if (currentIdentity.associatedFids.length === 0) { - const fidResult = (await authenticatorManager.callMethod({ - requestingFidgetId: "root", - authenticatorId: "farcaster:nounspace", - methodName: "getAccountFid", - isLookup: true, - })) as { value: number }; - const publicKeyResult = (await authenticatorManager.callMethod({ - requestingFidgetId: "root", - authenticatorId: "farcaster:nounspace", - methodName: "getSignerPublicKey", - isLookup: true, - })) as { value: Uint8Array }; - const signForFid = async (messageHash) => { - const signResult = (await authenticatorManager.callMethod( - { - requestingFidgetId: "root", - authenticatorId: "farcaster:nounspace", - methodName: "signMessage", - isLookup: false, - }, - messageHash, - )) as { value: Uint8Array }; - return signResult.value; - }; - await registerFidForCurrentIdentity( - fidResult.value, - bytesToHex(publicKeyResult.value), - signForFid, - ); + if (currentIdentity.associatedFids.length > 0) { + setCurrentStep(SetupStep.ACCOUNTS_REGISTERED); + return; + } + + // First attempt: infer user's Farcaster FID from their connected wallet address (no signer required). + const walletAddress = user?.wallet?.address; + if (walletAddress) { + const inferredFid = await inferFidForCurrentIdentity(walletAddress); + if (inferredFid) { + setCurrentStep(SetupStep.ACCOUNTS_REGISTERED); + return; + } + } + + // Fallback: if we still can't infer an FID, prompt the user to connect to Farcaster. + await authenticatorManager.installAuthenticators(["farcaster:nounspace"]); + authenticatorManager.initializeAuthenticators(["farcaster:nounspace"]); + setModalOpen(true); + + const ready = await waitForAuthenticatorReady(authenticatorManager, "farcaster:nounspace"); + if (!ready) { + console.warn("[registerAccounts] Farcaster authenticator timed out after 60s"); + // User can retry by interacting with the modal; keep it open for recovery. + return; } + + const fidResult = await authenticatorManager.callMethod({ + requestingFidgetId: "root", + authenticatorId: "farcaster:nounspace", + methodName: "getAccountFid", + isLookup: true, + }); + if (fidResult.result !== "success") { + console.warn("[registerAccounts] Failed to get FID from authenticator", fidResult); + // User can retry; keep modal open. + return; + } + + const publicKeyResult = await authenticatorManager.callMethod({ + requestingFidgetId: "root", + authenticatorId: "farcaster:nounspace", + methodName: "getSignerPublicKey", + isLookup: true, + }); + if (publicKeyResult.result !== "success") { + console.warn("[registerAccounts] Failed to get signer public key from authenticator", publicKeyResult); + // User can retry; keep modal open. + return; + } + + const signForFid = async (messageHash: Uint8Array) => { + const signResult = await authenticatorManager.callMethod( + { + requestingFidgetId: "root", + authenticatorId: "farcaster:nounspace", + methodName: "signMessage", + isLookup: false, + }, + messageHash + ); + if (signResult.result !== "success") { + throw new Error("Failed to sign message"); + } + return signResult.value as Uint8Array; + }; + + await registerFidForCurrentIdentity( + fidResult.value as number, + bytesToHex(publicKeyResult.value as Uint8Array), + signForFid + ); + + setCurrentStep(SetupStep.ACCOUNTS_REGISTERED); + } catch (e) { + console.error("[registerAccounts] failed", e); + // Don't deadlock the user; show modal so they can recover/authorize if needed. + setModalOpen(true); } - setCurrentStep(SetupStep.ACCOUNTS_REGISTERED); }; // Has to be separate otherwise will cause retrigger chain @@ -204,26 +254,22 @@ const LoggedInStateProvider: React.FC = ({ children }) => { (initializedAuthNames) => { const initializedAuthenticators = new Set(initializedAuthNames); const requiredAuthSet = new Set(requiredAuthenticators); - if (isEqual(initializedAuthenticators, requiredAuthSet)) { + // Consider the step complete once all required authenticators are initialized, + // even if other authenticators are also installed/initialized. + const allRequiredInitialized = Array.from(requiredAuthSet).every((id) => + initializedAuthenticators.has(id), + ); + if (allRequiredInitialized) { setCurrentStep(SetupStep.AUTHENTICATORS_INITIALIZED); } }, ); } - }, [ - ready, - currentStep, - authenticated, - authenticatorManager.lastUpdatedAt, - walletsReady, - ]); + }, [ready, currentStep, authenticated, authenticatorManager.lastUpdatedAt, walletsReady]); useEffect(() => { if (ready && authenticated && user) { - if ( - currentStep === SetupStep.NOT_SIGNED_IN || - currentStep === SetupStep.UNINITIALIZED - ) { + if (currentStep === SetupStep.NOT_SIGNED_IN || currentStep === SetupStep.UNINITIALIZED) { setCurrentStep(SetupStep.SIGNED_IN); } else if (walletsReady) { if (currentStep === SetupStep.SIGNED_IN) { @@ -243,22 +289,14 @@ const LoggedInStateProvider: React.FC = ({ children }) => { setCurrentStep(SetupStep.DONE); } } - } else if ( - ready && - !authenticated && - currentStep !== SetupStep.NOT_SIGNED_IN - ) { + } else if (ready && !authenticated && currentStep !== SetupStep.NOT_SIGNED_IN) { setCurrentStep(SetupStep.NOT_SIGNED_IN); } }, [currentStep, walletsReady, ready, authenticated, user]); return ( <> - + {children} ); diff --git a/src/constants/requiredAuthenticators.ts b/src/constants/requiredAuthenticators.ts index 495a95cc1..12bc191a9 100644 --- a/src/constants/requiredAuthenticators.ts +++ b/src/constants/requiredAuthenticators.ts @@ -1 +1,3 @@ -export default ["farcaster:nounspace"]; +// No authenticators are required for initial signup. +// Farcaster signer authorization is requested only when a user performs a write action. +export default []; diff --git a/src/fidgets/farcaster/components/CastRow.tsx b/src/fidgets/farcaster/components/CastRow.tsx index 3d11f5ee2..b2da8201e 100644 --- a/src/fidgets/farcaster/components/CastRow.tsx +++ b/src/fidgets/farcaster/components/CastRow.tsx @@ -437,7 +437,7 @@ const CastAttributionSecondary = ({ cast }) => { const CastReactions = ({ cast }: { cast: CastWithInteractions }) => { const [didLike, setDidLike] = useState(cast.viewer_context?.liked ?? false); const [didRecast, setDidRecast] = useState(cast.viewer_context?.recasted ?? false); - const { signer, fid: userFid } = useFarcasterSigner("render-cast"); + const { fid: userFid, getOrCreateSigner, isLoadingSigner } = useFarcasterSigner("render-cast"); const { showToast } = useToastStore(); const { setModalOpen, getIsAccountReady } = useAppStore((state) => ({ setModalOpen: state.setup.setModalOpen, @@ -490,9 +490,14 @@ const CastReactions = ({ cast }: { cast: CastWithInteractions }) => { return; } - // We check if we have the signer before proceeding - if (isUndefined(signer)) { - console.error("NO SIGNER"); + if (isLoadingSigner) { + showToast("Connecting to Farcaster..."); + return; + } + + const signer = await getOrCreateSigner(); + if (!signer) { + showToast("Connect Farcaster to continue."); return; } diff --git a/src/fidgets/farcaster/components/CreateCast.tsx b/src/fidgets/farcaster/components/CreateCast.tsx index 54c05f041..fc7fad647 100644 --- a/src/fidgets/farcaster/components/CreateCast.tsx +++ b/src/fidgets/farcaster/components/CreateCast.tsx @@ -183,7 +183,8 @@ const CreateCast: React.FC = ({ const hasEmbeds = draft?.embeds && !!draft.embeds.length; const isReply = draft?.parentCastId !== undefined; - const { signer, isLoadingSigner, fid } = useFarcasterSigner("create-cast"); + const { signer, isLoadingSigner, fid, hasSigner, getOrCreateSigner } = + useFarcasterSigner("create-cast"); const [initialChannels, setInitialChannels] = useState() as any; const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); const [isEnhancing, setIsEnhancing] = useState(false); @@ -412,13 +413,14 @@ const CreateCast: React.FC = ({ ); const onSubmitPost = async (): Promise => { - if ((!draft?.text && !draft?.embeds?.length) || isUndefined(signer)) { + const activeSigner = signer ?? (await getOrCreateSigner()); + if ((!draft?.text && !draft?.embeds?.length) || !activeSigner) { console.error( "Submission failed: Missing text or embeds, or signer is undefined.", { draftText: draft?.text, draftEmbedsLength: draft?.embeds?.length, - signerUndefined: isUndefined(signer), + signerUndefined: !activeSigner, }, ); return false; @@ -437,7 +439,7 @@ const CreateCast: React.FC = ({ } try { - const result = await publishPost(draft, fid, signer); + const result = await publishPost(draft, fid, activeSigner); if (result.success) { setSubmitStatus("success"); @@ -745,7 +747,8 @@ const CreateCast: React.FC = ({ const getButtonText = () => { - if (isLoadingSigner) return "Not signed into Farcaster"; + if (!hasSigner && !isLoadingSigner) return "Connect Farcaster"; + if (isLoadingSigner) return "Connecting Farcaster..."; if (isPublishing) return "Publishing..."; if (submissionError) return "Retry"; if (isPublished) return "Published!"; @@ -996,7 +999,7 @@ const CreateCast: React.FC = ({ onBlur={(e) => { e.currentTarget.style.backgroundColor = castButtonColors.backgroundColor; }} - disabled={isPublishing || isLoadingSigner} + disabled={isPublishing} > {getButtonText()} @@ -1066,7 +1069,7 @@ const CreateCast: React.FC = ({ onBlur={(e) => { e.currentTarget.style.backgroundColor = castButtonColors.backgroundColor; }} - disabled={isPublishing || isLoadingSigner} + disabled={isPublishing} > {getButtonText()} diff --git a/src/fidgets/farcaster/index.tsx b/src/fidgets/farcaster/index.tsx index 66bfd6580..0b86604b7 100644 --- a/src/fidgets/farcaster/index.tsx +++ b/src/fidgets/farcaster/index.tsx @@ -1,18 +1,18 @@ -import { - AuthenticatorManager, - useAuthenticatorManager, -} from "@/authenticators/AuthenticatorManager"; +import { AuthenticatorManager, useAuthenticatorManager } from "@/authenticators/AuthenticatorManager"; import { HubError, SignatureScheme, Signer } from "@farcaster/core"; import { indexOf } from "lodash"; import { err, ok } from "neverthrow"; import { useEffect, useState } from "react"; +import { useAppStore } from "@/common/data/stores/app"; +import useCurrentFid from "@/common/lib/hooks/useCurrentFid"; +import { waitForAuthenticatorReady } from "@/common/lib/authenticators/waitForAuthenticatorReady"; export const FARCASTER_AUTHENTICATOR_NAME = "farcaster:nounspace"; const createFarcasterSignerFromAuthenticatorManager = async ( authenticatorManager: AuthenticatorManager, fidgetId: string, - authenticatorName: string = "farcaster:nounspace", + authenticatorName: string = "farcaster:nounspace" ): Promise => { const schemeResult = await authenticatorManager.callMethod({ requestingFidgetId: fidgetId, @@ -20,10 +20,7 @@ const createFarcasterSignerFromAuthenticatorManager = async ( methodName: "getSignerScheme", isLookup: true, }); - const scheme = - schemeResult.result === "success" - ? (schemeResult.value as SignatureScheme) - : SignatureScheme.NONE; + const scheme = schemeResult.result === "success" ? (schemeResult.value as SignatureScheme) : SignatureScheme.NONE; return { scheme, getSignerKey: async () => { @@ -46,7 +43,7 @@ const createFarcasterSignerFromAuthenticatorManager = async ( methodName: "signMessage", isLookup: false, }, - hash, + hash ); if (methodResult.result === "success") { return ok(methodResult.value as Uint8Array); @@ -56,31 +53,68 @@ const createFarcasterSignerFromAuthenticatorManager = async ( }; }; -export function useFarcasterSigner( - fidgetId: string, - authenticatorName: string = "farcaster:nounspace", -) { +export function useFarcasterSigner(fidgetId: string, authenticatorName: string = "farcaster:nounspace") { const authenticatorManager = useAuthenticatorManager(); - const [isLoadingSigner, setIsLoadingSigner] = useState(true); + const [isLoadingSigner, setIsLoadingSigner] = useState(false); + const [hasSigner, setHasSigner] = useState(false); + const currentFid = useCurrentFid(); + const { setModalOpen, isAuthenticatorInstalled } = useAppStore((state) => ({ + setModalOpen: state.setup.setModalOpen, + isAuthenticatorInstalled: !!state.account.authenticatorConfig[FARCASTER_AUTHENTICATOR_NAME], + })); + + const ensureSigner = async () => { + if (!isAuthenticatorInstalled) { + await authenticatorManager.installAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); + authenticatorManager.initializeAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); + } else { + authenticatorManager.initializeAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); + } + setModalOpen(true); + + setIsLoadingSigner(true); + try { + const ready = await waitForAuthenticatorReady(authenticatorManager, FARCASTER_AUTHENTICATOR_NAME); + if (ready) { + setHasSigner(true); + return true; + } + return false; + } finally { + setIsLoadingSigner(false); + } + }; + + const getOrCreateSigner = async (): Promise => { + if (!hasSigner) { + const ok = await ensureSigner(); + if (!ok) return null; + } + // Always create a fresh signer bound to the current authenticatorManager instance. + return createFarcasterSignerFromAuthenticatorManager(authenticatorManager, fidgetId, authenticatorName); + }; + useEffect(() => { authenticatorManager .getInitializedAuthenticators() - .then((initilizedAuths) => - setIsLoadingSigner( - indexOf(initilizedAuths, FARCASTER_AUTHENTICATOR_NAME) === -1, - ), - ); + .then((initilizedAuths) => setHasSigner(indexOf(initilizedAuths, FARCASTER_AUTHENTICATOR_NAME) !== -1)); }, [authenticatorManager.lastUpdatedAt]); const [signer, setSigner] = useState(); useEffect(() => { - createFarcasterSignerFromAuthenticatorManager( - authenticatorManager, - fidgetId, - authenticatorName, - ).then((signer) => setSigner(signer)); - }, [authenticatorManager.lastUpdatedAt]); + if (!hasSigner) { + setSigner(undefined); + return; + } + createFarcasterSignerFromAuthenticatorManager(authenticatorManager, fidgetId, authenticatorName).then((signer) => + setSigner(signer) + ); + }, [authenticatorManager.lastUpdatedAt, hasSigner, authenticatorManager, fidgetId, authenticatorName]); const [fid, setFid] = useState(-1); useEffect(() => { + if (!hasSigner) { + setFid(currentFid ?? -1); + return; + } authenticatorManager .callMethod({ requestingFidgetId: fidgetId, @@ -89,17 +123,18 @@ export function useFarcasterSigner( isLookup: true, }) .then((methodResult) => { - if (methodResult.result === "success") { - return setFid(methodResult.value as number); - } - return setFid(-1); + if (methodResult.result === "success") return setFid(methodResult.value as number); + return setFid(currentFid ?? -1); }); - }, [authenticatorManager.lastUpdatedAt]); + }, [authenticatorManager.lastUpdatedAt, hasSigner, authenticatorManager, fidgetId, currentFid]); return { authenticatorManager, isLoadingSigner, signer, fid, + hasSigner, + ensureSigner, + getOrCreateSigner, }; } diff --git a/src/fidgets/ui/profile.tsx b/src/fidgets/ui/profile.tsx index 8ec0ad3a3..85be99b5e 100644 --- a/src/fidgets/ui/profile.tsx +++ b/src/fidgets/ui/profile.tsx @@ -45,7 +45,8 @@ const Profile: React.FC> = ({ settings: { fid }, }) => { const isMobile = useIsMobile(); - const { fid: viewerFid, signer } = useFarcasterSigner("Profile"); + const { fid: viewerFid, signer, getOrCreateSigner, isLoadingSigner } = + useFarcasterSigner("Profile"); const { data: userData } = useLoadFarcasterUser( fid, viewerFid > 0 ? viewerFid : undefined @@ -65,7 +66,10 @@ const Profile: React.FC> = ({ // console.log("user", user); const toggleFollowing = async () => { - if (user && signer && viewerFid > 0) { + if (user && viewerFid > 0) { + if (isLoadingSigner) return; + const activeSigner = signer ?? (await getOrCreateSigner()); + if (!activeSigner) return; setActionStatus("loading"); // Optimistically update the user's following state @@ -80,9 +84,9 @@ const Profile: React.FC> = ({ try { let success; if (wasFollowing) { - success = await unfollowUser(fid, viewerFid, signer); + success = await unfollowUser(fid, viewerFid, activeSigner); } else { - success = await followUser(fid, viewerFid, signer); + success = await followUser(fid, viewerFid, activeSigner); } if (!success) { diff --git a/src/pages/api/fid-link.ts b/src/pages/api/fid-link.ts index 8892be26f..01cadc047 100644 --- a/src/pages/api/fid-link.ts +++ b/src/pages/api/fid-link.ts @@ -33,8 +33,8 @@ export type FidLinkToIdentityResponse = NounspaceResponse<{ fid: number; identityPublicKey: string; created: string; - signature: string; - signingPublicKey: string; + signature: string | null; + signingPublicKey: string | null; isSigningKeyValid: boolean; }>; @@ -71,13 +71,14 @@ async function linkFidToIdentity( }); return; } - if (!checkSigningKeyValidForFid) { + if (!(await checkSigningKeyValidForFid(reqBody.fid, reqBody.signingPublicKey))) { res.status(400).json({ result: "error", error: { message: `Signing key ${reqBody.signingPublicKey} is not valid for fid ${reqBody.fid}`, }, }); + return; } const { data: checkExistsData } = await createSupabaseServerClient() .from("fidRegistrations") @@ -173,7 +174,7 @@ async function lookUpFidsForIdentity( .from("fidRegistrations") .select("fid") .eq("identityPublicKey", identity) - .eq("isSigningKeyValid", true); + ; if (error) { res.status(500).json({ result: "error", diff --git a/src/pages/api/fid-link/infer.ts b/src/pages/api/fid-link/infer.ts new file mode 100644 index 000000000..cc9ecb83e --- /dev/null +++ b/src/pages/api/fid-link/infer.ts @@ -0,0 +1,225 @@ +import { ed25519 } from "@noble/curves/ed25519"; +import { first, isArray } from "lodash"; +import moment from "moment"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import neynar from "@/common/data/api/neynar"; +import requestHandler, { NounspaceResponse } from "@/common/data/api/requestHandler"; +import createSupabaseServerClient from "@/common/data/database/supabase/clients/server"; +import { hashObject } from "@/common/lib/signedFiles"; + +export type InferFidLinkRequest = { + identityPublicKey: string; + walletAddress: string; + timestamp: string; + signature: string; +}; + +export type InferFidLinkResponse = NounspaceResponse<{ + fid: number; + identityPublicKey: string; + created: string; + inferredFromAddress: string; +} | null>; + +function isInferFidLinkRequest(maybe: unknown): maybe is InferFidLinkRequest { + return ( + !!maybe && + typeof maybe === "object" && + typeof (maybe as any).identityPublicKey === "string" && + typeof (maybe as any).walletAddress === "string" && + typeof (maybe as any).timestamp === "string" && + typeof (maybe as any).signature === "string" + ); +} + +function validateRequestSignature(req: InferFidLinkRequest) { + const message = hashObject({ + identityPublicKey: req.identityPublicKey, + walletAddress: req.walletAddress, + timestamp: req.timestamp, + }); + try { + return ed25519.verify(req.signature, message, req.identityPublicKey); + } catch { + return false; + } +} + +async function inferAndLinkFid(req: NextApiRequest, res: NextApiResponse) { + const body = req.body; + if (!isInferFidLinkRequest(body)) { + return res.status(400).json({ + result: "error", + error: { message: "Request requires identityPublicKey, walletAddress, timestamp, signature" }, + }); + } + if (!validateRequestSignature(body)) { + return res.status(400).json({ + result: "error", + error: { message: "Invalid signature" }, + }); + } + + // If Neynar isn't configured, we can't infer an FID during signup. + // Treat as "no inferred FID" so the client can fall back to prompting Farcaster connection. + if (!process.env.NEYNAR_API_KEY) { + return res.status(200).json({ result: "success", value: null }); + } + + const walletAddress = body.walletAddress.toLowerCase(); + const supabase = createSupabaseServerClient(); + + // Ensure this identity is actually associated with the wallet address in our DB. + const { data: walletIdentity, error: walletIdentityError } = await supabase + .from("walletIdentities") + .select("identityPublicKey, walletAddress") + .eq("identityPublicKey", body.identityPublicKey) + .maybeSingle(); + + if (walletIdentityError) { + return res.status(500).json({ + result: "error", + error: { message: walletIdentityError.message }, + }); + } + if (!walletIdentity) { + return res.status(400).json({ + result: "error", + error: { message: "Identity is not linked to provided wallet address" }, + }); + } + + // wallet addresses may be checksummed; match case-insensitively + if (walletIdentity.walletAddress.toLowerCase() !== walletAddress) { + return res.status(400).json({ + result: "error", + error: { message: "Identity is not linked to provided wallet address" }, + }); + } + + let inferredFid: number | null = null; + try { + const response = await neynar.fetchBulkUsersByEthOrSolAddress({ addresses: [walletAddress] }); + const mapping = (response as any)?.[walletAddress]; + const user = isArray(mapping) ? first(mapping) : undefined; + if (user && typeof user.fid === "number") inferredFid = user.fid; + } catch (e: any) { + const status = e?.response?.status; + // If Neynar rejects due to auth/config, treat as "can't infer" rather than blocking signup. + if (status === 401 || status === 403) { + return res.status(200).json({ result: "success", value: null }); + } + return res.status(500).json({ + result: "error", + error: { message: e?.message || "Failed to infer FID from address" }, + }); + } + + if (!inferredFid) { + return res.status(200).json({ result: "success", value: null }); + } + + // Use server time (not client-provided timestamp) to avoid clock skew attacks. + const serverNow = moment().toISOString(); + + // Check if a FID registration already exists. + // Selecting all needed fields to make the correct decision about whether to update. + const { data: existing, error: queryError } = await supabase + .from("fidRegistrations") + .select("fid, created, identityPublicKey") + .eq("fid", inferredFid); + + if (queryError) { + return res.status(500).json({ + result: "error", + error: { message: queryError.message }, + }); + } + + if (existing && existing.length > 0) { + const currentRecord = first(existing); + const existingIdentityPublicKey = currentRecord?.identityPublicKey as string | null | undefined; + + // SECURITY: If this FID is already linked to a different identity, reject the reassignment. + // This prevents an attacker from taking over a FID they don't own. + if (existingIdentityPublicKey && existingIdentityPublicKey !== body.identityPublicKey) { + return res.status(400).json({ + result: "error", + error: { message: "FID is already linked to a different identity" }, + }); + } + + // If the existing record was created after our request timestamp, + // the record is newer, so return the existing data without updating. + if (moment(currentRecord?.created).isAfter(body.timestamp)) { + return res.status(200).json({ + result: "success", + value: { + fid: inferredFid, + identityPublicKey: currentRecord!.identityPublicKey, + created: currentRecord!.created, + inferredFromAddress: walletAddress, + }, + }); + } + + // Record is older than the request, so update it with the new information. + // NOTE: Do NOT clear out signer fields here. If a signer was previously authorized, + // we should preserve it. Only infer/update the identity link. + const { error: updateError } = await supabase + .from("fidRegistrations") + .update({ + identityPublicKey: body.identityPublicKey, + // Do NOT set signer fields to null—preserve existing signer if present + }) + .eq("fid", inferredFid) + .select(); + + if (updateError) { + return res.status(500).json({ result: "error", error: { message: updateError.message } }); + } + + return res.status(200).json({ + result: "success", + value: { + fid: inferredFid, + identityPublicKey: body.identityPublicKey, + created: currentRecord!.created, + inferredFromAddress: walletAddress, + }, + }); + } + + // Persist the inferred link even if no Farcaster Signer is present. + // This allows the server to recognize the Identity Key as a valid owner of the Space (linked to the FID), + // enabling Space Editing to proceed. Farcaster Write Actions (Casting) will still fail or prompt for a signer + // because isSigningKeyValid is false. + const { error: insertError } = await supabase + .from("fidRegistrations") + .insert({ + fid: inferredFid, + identityPublicKey: body.identityPublicKey, + created: serverNow, + isSigningKeyValid: false, + signingPublicKey: null, + signature: null, + signingKeyLastValidatedAt: null, + }); + + if (insertError) { + return res.status(500).json({ result: "error", error: { message: insertError.message } }); + } + + return res.status(200).json({ + result: "success", + value: { + fid: inferredFid, + identityPublicKey: body.identityPublicKey, + created: serverNow, + inferredFromAddress: walletAddress, + }, + }); +} + +export default requestHandler({ post: inferAndLinkFid }); diff --git a/src/pages/api/space/identities.ts b/src/pages/api/space/identities.ts index 840c4f978..37a049993 100644 --- a/src/pages/api/space/identities.ts +++ b/src/pages/api/space/identities.ts @@ -52,7 +52,12 @@ async function handlePost( identityRequest: IdentityRequest; }; if (!validateRequestSignature(identityRequest)) { - throw Error("Invalid signature on request"); + return res.status(400).json({ + result: "error", + error: { + message: "Invalid signature on request", + }, + }); } if (identityRequest.type === "Create" && !isUndefined(file)) { if (!validateSignable(file)) { @@ -107,7 +112,8 @@ async function handleGet( const { data, error } = await supabase .from("walletIdentities") .select() - .eq("walletAddress", address); + // wallet addresses may be checksummed; match case-insensitively + .ilike("walletAddress", address); if (error) { res.status(500).json({ result: "error", diff --git a/src/supabase/database.d.ts b/src/supabase/database.d.ts index e6cfc7164..92ddc2ea6 100644 --- a/src/supabase/database.d.ts +++ b/src/supabase/database.d.ts @@ -268,9 +268,9 @@ export type Database = { id: number identityPublicKey: string isSigningKeyValid: boolean - signature: string - signingKeyLastValidatedAt: string - signingPublicKey: string + signature: string | null + signingKeyLastValidatedAt: string | null + signingPublicKey: string | null } Insert: { created?: string @@ -278,9 +278,9 @@ export type Database = { id?: number identityPublicKey: string isSigningKeyValid: boolean - signature: string - signingKeyLastValidatedAt: string - signingPublicKey: string + signature?: string | null + signingKeyLastValidatedAt?: string | null + signingPublicKey?: string | null } Update: { created?: string @@ -288,9 +288,9 @@ export type Database = { id?: number identityPublicKey?: string isSigningKeyValid?: boolean - signature?: string - signingKeyLastValidatedAt?: string - signingPublicKey?: string + signature?: string | null + signingKeyLastValidatedAt?: string | null + signingPublicKey?: string | null } Relationships: [] } diff --git a/supabase/migrations/20240614000356_setup_db.sql b/supabase/migrations/20240614000356_setup_db.sql index 1ae241231..4831e5bb4 100644 --- a/supabase/migrations/20240614000356_setup_db.sql +++ b/supabase/migrations/20240614000356_setup_db.sql @@ -36,10 +36,10 @@ CREATE TABLE IF NOT EXISTS "public"."fidRegistrations" ( "created" timestamp with time zone DEFAULT "now"() NOT NULL, "fid" bigint NOT NULL, "identityPublicKey" character varying NOT NULL, - "signature" character varying NOT NULL, - "signingPublicKey" character varying NOT NULL, - "signingKeyLastValidatedAt" timestamp without time zone NOT NULL, - "isSigningKeyValid" boolean NOT NULL + "signature" character varying, + "signingPublicKey" character varying, + "signingKeyLastValidatedAt" timestamp without time zone, + "isSigningKeyValid" boolean DEFAULT false ); ALTER TABLE "public"."fidRegistrations" OWNER TO "postgres";